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