clapkit-coreml — fp16-safe CoreML conversions of LAION CLAP (both encoders)

CoreML conversions of 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 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 .mlpackages 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 see note
tokenizer (used by clapkit, not redistributed here) 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.

Downloads last month
10
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support