| --- |
| 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. |
|
|