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.