File size: 8,738 Bytes
e919ff6 a55e046 e919ff6 8dc6258 e919ff6 | 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 | ---
license: openmdw-1.1
library_name: coreai
pipeline_tag: voice-activity-detection
base_model: nvidia/Nemotron-3-Diarization
tags: [coreai, core-ai, coreaikit, nemotron, sortformer, speaker-diarization, diarization, streaming, on-device, apple]
base_model_relation: quantized
---
Core AI is Apple's on-device ML runtime in iOS 27 / macOS 27 and the successor to Core ML: PyTorch models are exported with Apple's `coreai-torch` (LLMs: `coreai.llm.export`) into `.aimodel` bundles that run on the GPU or the Neural Engine.
<!-- gen-cards:devicemark begin (managed by scripts/gen-cards + tools/devicemark_row.py — edit cards.json, not this block) -->
This model has no row on [DeviceMark](https://devicemark.github.io/), the on-device LLM leaderboard.
<!-- gen-cards:devicemark end -->
# Nemotron-3-Diarization — Core AI
[`nvidia/Nemotron-3-Diarization`](https://huggingface.co/nvidia/Nemotron-3-Diarization) (OpenMDW-1.1,
99.2M parameters) converted to **Apple Core AI**. It is streaming speaker diarization, "who spoke when",
for up to **8 speakers** at 10 ms resolution, on-device. The graph is the per-chunk network. The host
keeps the state: the log-mel front end, the speaker cache and its FIFO.
It succeeds the 4-speaker [Streaming-Sortformer-Diar-CoreAI](https://huggingface.co/mlboydaisuke/Streaming-Sortformer-Diar-CoreAI).
Another Core AI conversion of this checkpoint exists:
[smdesai/Nemotron-3-Diarization-CoreAI](https://huggingface.co/smdesai/Nemotron-3-Diarization-CoreAI).
## Files
| file | what |
|---|---|
| `n3d_streaming_float16.aimodel` | the graph, fp16 weights, T = 541 packed rows; all three streaming modes; macOS (198.0 MB) |
| `n3d_offline_float16.aimodel` | the same graph at T = 684, for the offline profile; macOS (198.0 MB) |
| `n3d_streaming_float16.h18p.aimodelc` | the streaming graph compiled ahead of time for the iPhone 17 Pro GPU (h18p, 198.2 MB) |
| `n3d_offline_float16.h18p.aimodelc` | the offline graph, same target (198.3 MB) |
| `embedder_projection.f32le` | [512, 1024]: the projection of 8 stacked mel frames (a host matmul) |
| `silence_embeds.f32le` | [512]: the learned silence row of the speaker cache |
| `mel_filters_128x257.f32le` | [128, 257]: the slaney mel filterbank, bit-identical to the transformers feature extractor's |
| `hann_window_400.f32le` | [400]: `torch.hann_window(400, periodic=False)`, bit for bit |
| `metadata.json` | the graph contract, the host constants of each profile, the mel parameters, and for every asset and bundle its sha256 (bundles also: size, target, compute preference) |
| `host/` | the four `.f32le` files and `metadata.json` again, in one directory: what a client that fetches a directory (CoreAIKit's catalog) takes |
| `config.json`, `processor_config.json` | the source repo's, unchanged (revision `f667ed7`) |
| `LICENSE`, `NOTICE` | the OpenMDW-1.1 agreement; the origin of the weights and what was converted |
| `SHA256SUMS` | every file above |
`.f32le` files are raw little-endian float32 in C order.
## Graph contract
```
inputs: packed [1, T, 512] float32 [speaker cache | FIFO | chunk + look-ahead] rows, left-packed in [0, L); rows [L, T) zero
valid [1, T] float32 1.0 for rows < L, 0.0 after
outputs: logits [1, T*8, 8] float32 speaker logits at 10 ms; rows [0, L*8) are real
T = 541 for the three streaming modes, 684 offline. Both fp16 bundles take and return float32.
```
## Host
1. Log-mel: pre-emphasis 0.97 (the first sample kept), 512-point STFT with hop 160 and the 400-sample
window centered, |X|², 128 slaney mel bins, log(x + 2⁻²⁴). No normalization. Each streaming chunk
computes its mel from its own audio slice, as the source card's `inputs_generator` does.
2. Every 8 mel frames are stacked (1,024 values) and projected with `embedder_projection.f32le` in
float32: one 512-d row per 80 ms.
3. Each step packs [cache | FIFO | chunk + look-ahead] and runs the graph. It keeps `logits[:L*8]` and
average-pools the sigmoid 8× to the cache rate, then emits the chunk's frames.
4. The speaker cache follows transformers' `Nemotron3DiarizationSpeakerCache.update` / `_compress`
line by line. The one change is precision: the compression scores are float64 (see Verification).
5. Turns: per speaker, every run of probability > 0.5. Overlapping speech stays overlapping.
| profile | chunk + look-ahead (encoder frames of 80 ms) | FIFO / update period | graph |
|---|---|---|---|
| low latency | 9 + 4 (0.72 s + 0.32 s) | 264 / 222 | T = 541 |
| very low latency | 6 + 2 (0.48 s + 0.16 s) | 264 / 222 | T = 541 |
| ultra low latency | 3 + 1 (0.24 s + 0.08 s) | 264 / 222 | T = 541 |
| offline | 340 + 40, over the whole recording's mel | 40 / 300 | T = 684 |
The speaker cache holds 264 rows in every profile. `metadata.json` carries all of these constants.
## Verification
The reference is transformers `Nemotron3DiarizationForAudioFrameClassification` in fp32. Two clips were
used: 97.6 s (the transformers integration-test audio, where the cache compresses 4 times in low latency)
and 21.5 s. The score is speaker-activity agreement at 0.5 over every 10 ms frame × 8 speakers, with a bar
of 99.9 %. Offline leaves out the last 16 frames, as the transformers integration test does.
fp16 graph on the Mac GPU, full streaming loop:
| clip | profile | agreement@0.5 (differing / elements) | max \|Δp\| | turns: reference / ours / matched |
|---|---|---|---|---|
| 97.6 s | low latency | 99.9987 % (1 / 78,072) | 0.025 | 37 / 38 / 36 |
| 97.6 s | very low latency | 99.9424 % (45 / 78,072) | 0.362 | 43 / 43 / 42 |
| 97.6 s | ultra low latency | 99.9949 % (4 / 78,072) | 0.011 | 61 / 60 / 59 |
| 97.6 s | offline | 99.9987 % (1 / 77,952) | 0.029 | 29 / 29 / 29 |
| 21.5 s | low / very low / ultra low | 100 % (0 / 17,192 each) | ≤ 0.0031 | 10 / 10 / 10 |
| 21.5 s | offline | 99.9941 % (1 / 17,072) | 0.0037 | 9 / 9 / 9 |
- The Python host and the Swift host give these numbers with bit-identical logits in all 8 runs.
- The fp32 bundle on the CPU alone, 97.6 s low latency: 100 % (max |Δp| 5.5e-6).
- The compression scores are float64 because transformers' own fp32 top-k choice sits 2 ulp from its
boundary in 3 of the 16 compressions of these clips. A float32 host flips one of them in low latency.
- The Mac Neural Engine (the same fp16 graph, one ANE region) reaches 99.7746 % on 97.6 s low latency.
That is below the bar, so no ANE bundle is shipped. On 21.5 s it reaches 100 %.
- iPhone 17 Pro (iOS 27.0, build 24A437, thermal state nominal throughout, 2026-09-24), the h18p GPU bundles: 97.6 s
low latency 99.9987 % (1 / 78,072), 21.5 s 100 %; offline 99.9987 % and 99.9941 %. Its decisions equal the
Mac GPU's on 78,070 of 78,072 elements. The h18p Neural Engine compile reaches 99.7758 % on 97.6 s low
latency, the same as on the Mac, so it is not shipped.
Speed. One chunk is 0.72 s of audio in low latency; the offline graph takes 30.4 s per chunk.
| device | ms per chunk, median / p90 | 97.6 s end to end (real-time factor) | load, first / second |
|---|---|---|---|
| iPhone 17 Pro GPU, streaming graph (h18p, iOS 27.0) | 30.1 / 30.5 | 4.16 s (0.043) | 0.68 s / 0.12 s |
| iPhone 17 Pro GPU, offline graph (h18p) | 34.5 / 35.8 | 0.17 s (0.0018) | 0.55 s / 0.06 s |
| M4 Max GPU, macOS 27.0, shared with other jobs | 15.4–15.9 / 15.6–16.1 | 2.1–2.3 s (0.022); one run of six took 6.2 s | 0.12 s warm |
On the iPhone the first graph call after a load took 2.7 s (streaming) and 0.8 s (offline); the numbers
above are the calls after it.
## Use
The Swift host is the `NemotronDiarizer` package in
[`conversion/nemotron3_diar/swift`](https://github.com/john-rocky/coreai-model-zoo/tree/main/conversion/nemotron3_diar/swift)
(no dependencies; it links the system CoreAI framework):
```swift
let assets = try N3DAssets(directory: repoDir) // this repo, downloaded
let diarizer = try await N3DDiarizer(assets: assets, computeUnits: .gpu,
profile: .streamingProfile(mode: .lowLatency))
let out = try await diarizer.process(samples: pcm16kMono) // out.probs: [frames, 8]
let turns = N3DDiarizer.segments(from: out.probs) // speaker, start, end
```
On iOS it picks the `.h18p.aimodelc` beside the `.aimodel`. The Python host is `host_loop.py` in the same
directory. Conversion, gates and the lessons of this port:
[`conversion/nemotron3_diar`](https://github.com/john-rocky/coreai-model-zoo/tree/main/conversion/nemotron3_diar).
## License
OpenMDW-1.1, the source model's license. `LICENSE` (the agreement) and `NOTICE` (origin, revision, what was
converted) ship in this repo. Keep both with any part of it you redistribute.
<!-- funnel:v1 -->
<!-- /funnel:v1 -->
|