File size: 9,173 Bytes
97d631f 02a99c6 97d631f 02a99c6 97d631f 02a99c6 97d631f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | ---
license: cc-by-4.0
tags:
- coreml
- clap
- audio-text
- htsat
- roberta
- zero-shot-audio-classification
- apple-neural-engine
library_name: coremlit
---
# clapkit-coreml — fp16-safe CoreML conversions of LAION CLAP (both encoders)
CoreML conversions of **[laion/clap-htsat-unfused](https://huggingface.co/laion/clap-htsat-unfused)**
— both towers of the CLAP audio↔text model — converted from the PyTorch source
(`transformers` `ClapModel`) with the campaign's **fp16-survivable numerical guard
discipline**, so they are correct on the Apple GPU and Neural Engine, not just CPU.
Consumed by the `clapkit` crate of the
[coremlit](https://github.com/findit-studio/coremlit) workspace (Rust, sync,
sans-I/O). Each graph emits the final **512-d joint-space embedding pre-L2-norm**;
the caller performs the L2 normalization (this keeps the fp16 `rsqrt` guard class
out of the graph entirely).
## Contents
| artifact | form | what it is |
|---|---|---|
| `clap_audio.{mlpackage,mlmodelc}` | fp16 | HTSAT audio tower + `audio_projection` (spectrogram-input) |
| `clap_text.{mlpackage,mlmodelc}` | fp16 | RoBERTa text tower + `text_projection` |
| `clap_audio_int8.{mlpackage,mlmodelc}` | int8 | audio tower — 8-bit k-means-palettized weights (weight-only; activations fp16) |
| `clap_text_int8.{mlpackage,mlmodelc}` | int8 | text tower — 8-bit k-means-palettized weights (weight-only; activations fp16) |
`.mlpackage` is the canonical distributable; `.mlmodelc` is the compiled form the
test suite loads directly. `CHECKSUMS.sha256` covers every file. The `_int8`
siblings are an **optional ~2× smaller** tier (see "int8 weight-only tier" below);
the fp16 graphs remain the default.
### I/O contract (pinned from the artifact metadata)
| encoder | input(s) | output |
|---|---|---|
| audio | `input_features` fp32 `[1, 1, 1001, 64]` (log-mel) | `audio_embeds` fp32 `[1, 512]` (pre-norm) |
| text | `input_ids` int32 `[1, 512]`, `attention_mask` int32 `[1, 512]` | `text_embeds` fp32 `[1, 512]` (pre-norm) |
- **Audio is 48 kHz mono**, a documented deviation from the workspace's 16 kHz
convention (CLAP's native rate). One inference = one fixed 480,000-sample (10 s)
window → the `[1, 1, 1001, 64]` mel.
- **Text length is fixed at 512** (the model's max). Padding a shorter prompt to
512 with the attention mask reproduces the natural-length embedding exactly
(verified cosine `1.0`), because RoBERTa derives positions from `input_ids` and
the mask zeroes the padding.
## The mel frontend lives in Rust (spectrogram-input), by measurement
The audio graph takes the **log-mel spectrogram**, not raw audio. Riding the
STFT + Slaney-mel + `power_to_dB` frontend *inside* the graph was attempted and
rejected on measurement:
- a faithful in-graph STFT reproduces HF's `ClapFeatureExtractor` mel only in
**float64** (the reference promotes to f64; an in-graph f32 STFT lands **0.55–0.90
cosine** away from the correct path end-to-end) — f64 is hostile to an fp16 ANE
graph;
- the `power_to_dB` floor `amin = 1e-10` is **1680× below fp16's smallest subnormal
(`2^-24`)** — exactly the vanishing-guard class this campaign guards against.
So the mel is a **Rust port validated bit-for-bit against textclap's `mel.rs`**
(the frontend oracle). Its parameters: `n_fft = 1024`, `hop = 480`, `n_mels = 64`,
`fmin = 50`, `fmax = 14000`, periodic Hann, Slaney scale + Slaney norm,
`center=True` reflect padding, `10·log10(max(·, 1e-10))`, HTSAT input-norm `none`,
time-major `[1001, 64]` → reshaped to `[1, 1, 1001, 64]`.
## Verification (measured, this conversion)
- **MIL-level fp16-guard audit CLEAN** — 55 guard sites total (audio 30, text 25),
every effective floor ≥ `2^-24`; no decomposed `softmax→log`; no unresolved
guards. The text tower's LayerNorm `eps = 1e-12` (below fp16 subnormal) is raised
to `2^-24` by the fp16 conversion; audio LayerNorm/BatchNorm `eps ≈ 1e-5` survive
as-is. Normalization is out of the graph, so there is no `rsqrt`/`real_div` guard.
- **PyTorch fp32 vs CoreML fp32 (CPU)** on real inputs: worst cosine **1.00000000**
(audio, 10 real clips: music / speech / SFX / ambient) and **1.00000000** (text,
12 varied prompts). The graph conversion is faithful; the HTSAT `reshape_mel2img`
bicubic resize is reproduced by an exact baked-constant matmul.
- **CoreML fp16 vs fp32**, worst cosine per compute unit:
| encoder | ALL | CPU+GPU | CPU |
|---|---|---|---|
| audio | 0.99999573 | 0.99999390 | 0.99996626 |
| text | 0.99994979 | 0.99999733 | 0.99987553 |
fp16 is clean on every placement — **fp16 is shipped** (no fp32 fallback needed).
## int8 weight-only tier (optional, ~2× smaller)
Alongside the fp16 graphs this repo ships an **8-bit weight-only** tier of both
towers — `clap_audio_int8` / `clap_text_int8`. Weights are stored 8-bit and
dequantized to fp16 at runtime; **activations stay fp16** (identical graph maths,
only the constant weights are compressed). Produced from the shipped fp16
`.mlpackage`s by coremltools post-training compression — no reconversion, same
source weights.
- **Method: 8-bit k-means palettization, per-tensor**
(`coremltools.optimize.coreml.palettize_weights`, `nbits=8`, `mode="kmeans"`,
`weight_threshold=2048`). Chosen over linear-symmetric int8
(`linear_quantize_weights`) **by measurement**: on the text tower linear int8
costs up to **0.43%** cosine (worst 0.99571 vs fp16) while k-means costs
**0.13%** (worst 0.99873); on audio the two are close (0.99965 vs 0.99971).
k-means wins on both towers and is decisive on text, so it is the shipped `_int8`
tier. Tiny bias / LayerNorm-gain tensors (< 2048 elements) stay fp16; one RoBERTa
attention-mask fill constant (±inf) is likewise left uncompressed.
- **int8-vs-fp16 embedding cosine** (identical inputs, CPU; the conversion
verification set — 10 real clips + 12 varied prompts including CJK — plus the
committed `golden_mel` fixture):
| tower | worst | mean |
|---|---|---|
| audio | 0.99970584 | 0.99982787 |
| text | 0.99873250 | 0.99965990 |
Well inside a 0.5% cosine budget on both towers. (int8-vs-fp32-**source** worst:
audio 0.99967, text 0.99897 — barely further from the PyTorch source than fp16
itself, which is 0.99997 / 0.99988.)
- **Zero-shot ranking unchanged.** The end-to-end gate (a ~192 s speech clip → 20
windows → aggregate → 4-anchor zero-shot score) returns the **identical ranking**
on int8 as on fp16: top label "This is a sound of a person speaking" at logit
**8.88** (fp16 8.82), same order for music / dog / rain, same margins.
- **Placement unchanged.** Cross-compute-unit agreement holds (audio 0.99998, text
0.99994 — ≥ the 0.9999 band on All / CPU+NE / CPU+GPU / CPU). Audio still fails
`ANECCompile()` and falls back to GPU/CPU; text still compiles for the ANE — the
palettized weights change neither.
- **Size** (`weight.bin` bytes, fp16 → int8):
| tower | fp16 | int8 | ratio |
|---|---|---|---|
| audio | 60,562,432 | 30,409,920 | 1.99× |
| text | 251,665,792 | 125,746,688 | 2.00× |
Compiled `.mlmodelc`: audio 58 MB → 29 MB, text 240 MB → 120 MB.
Same toolchain pin as the fp16 conversion (coremltools 9.0; scikit-learn provides
the k-means step). The int8 tier is a pure recompression of the pinned fp16
artifacts, so its provenance and licensing are exactly those of the fp16 graphs
below.
## Placement guidance (measured, never marketed)
- **text**: compiles for and runs on the ANE/GPU/CPU; fp16-clean on all.
- **audio**: the HTSAT graph currently **fails ANE compilation** (`ANECCompile()`)
and falls back to GPU/CPU — still fp16-clean there. Consumers should not assume
ANE placement for the audio tower; `clapkit` selects the compute unit and does
not assert ANE.
## Toolchain (pinned)
coremltools 9.0 · torch 2.5.1 · transformers 5.14.0 · numpy 1.26.4 · python 3.11.15.
Source revision: `laion/clap-htsat-unfused@8fa0f1c6d0433df6e97c127f64b2a1d6c0dcda8a`.
## Upstream provenance & licensing
These are **derivative conversions**: the same LAION CLAP weights, re-emitted as
CoreML graphs (projection heads in-graph, L2-norm out, mel frontend externalized).
| component | upstream | license |
|---|---|---|
| both encoders | [laion/clap-htsat-unfused](https://huggingface.co/laion/clap-htsat-unfused) | see note |
| tokenizer (used by `clapkit`, not redistributed here) | [Xenova/clap-htsat-unfused](https://huggingface.co/Xenova/clap-htsat-unfused) `@c28f2883…` (`tokenizer.json` sha `dc239041…`) | derived from the RoBERTa tokenizer |
> **License note.** Attribution to `laion/clap-htsat-unfused` is provided as
> required. The LAION CLAP checkpoints are commonly attributed as **CC-BY-4.0**
> (the position taken by the consuming project and reflected in the front-matter),
> while the current upstream HF model card declares **apache-2.0**. Both licenses
> require attribution, which this repository provides; downstream users should
> honor whichever the upstream author intends. If you are the upstream author and
> want changes to this redistribution, please open a discussion.
|