Skip to content

Commit 17e7d39

Browse files
authored
[examples] Enable Nemotron 3 Diarization on CUDA (#23167)
Lower the neural network methods to CUDA with BF16 or FP32 computation, while retaining CPU preprocessing. Handle CUDA attention constraints, serialize the external kernel data, and add runner loading support and CUDA build presets. Validated the native Release build and lint, 26 numerical comparisons against PyTorch, and 75 audio runs covering all four streaming presets. Authored with OpenAI Codex.
1 parent 8a8829e commit 17e7d39

8 files changed

Lines changed: 137 additions & 19 deletions

File tree

‎Makefile‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# - whisper: Speech recognition model (CPU, CUDA, Metal)
2020
# - parakeet: Speech recognition model (CPU, CUDA, Metal, MLX)
2121
# - sortformer: Speaker diarization model (CPU, CUDA)
22-
# - nemotron3-diarization: Speaker diarization model (MLX, XNNPACK)
22+
# - nemotron3-diarization: Speaker diarization model (MLX, XNNPACK, CUDA)
2323
# - supertonic: Text-to-speech model (MLX)
2424
# - silero_vad: Voice activity detection model (CPU)
2525
# - llama: Text generation model (CPU)
@@ -124,6 +124,7 @@ help:
124124
@echo " sortformer-cpu - Build Sortformer runner with CPU backend"
125125
@echo " nemotron3-diarization-mlx - Build Nemotron 3 Diarization runner with MLX backend"
126126
@echo " nemotron3-diarization-cpu - Build Nemotron 3 Diarization runner with XNNPACK backend"
127+
@echo " nemotron3-diarization-cuda - Build Nemotron 3 Diarization runner with CUDA backend"
127128
@echo " supertonic-mlx - Build Supertonic runner with MLX backend"
128129
@echo " silero-vad-cpu - Build Silero VAD runner with CPU backend"
129130
@echo " llama-cuda - Build Llama runner with CUDA backend"
@@ -299,6 +300,11 @@ nemotron3-diarization-cpu:
299300
cmake --workflow --preset llm-release
300301
cd examples/models/nemotron3_diarization && cmake --workflow --preset nemotron3-diarization-cpu
301302

303+
.PHONY: nemotron3-diarization-cuda
304+
nemotron3-diarization-cuda:
305+
cmake --workflow --preset llm-release-cuda
306+
cd examples/models/nemotron3_diarization && cmake --workflow --preset nemotron3-diarization-cuda
307+
302308
sortformer-cuda:
303309
@echo "==> Building and installing ExecuTorch with CUDA..."
304310
cmake --workflow --preset llm-release-cuda

‎examples/models/nemotron3_diarization/CMakeLists.txt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ if(TARGET kleidiai)
4242
target_link_libraries(nemotron3_diarization PUBLIC kleidiai)
4343
endif()
4444
executorch_target_link_options_shared_lib(optimized_native_cpu_ops_lib)
45+
if(EXECUTORCH_BUILD_CUDA)
46+
find_package(CUDAToolkit REQUIRED)
47+
target_link_libraries(nemotron3_diarization PUBLIC aoti_cuda_backend)
48+
if(NOT MSVC)
49+
executorch_target_link_options_shared_lib(aoti_cuda_backend)
50+
endif()
51+
endif()
4552
add_executable(nemotron3_diarization_runner main.cpp)
4653
target_include_directories(
4754
nemotron3_diarization_runner
@@ -50,6 +57,11 @@ target_include_directories(
5057
target_link_libraries(
5158
nemotron3_diarization_runner PRIVATE nemotron3_diarization gflags
5259
)
60+
if(EXECUTORCH_BUILD_CUDA)
61+
target_compile_definitions(
62+
nemotron3_diarization_runner PRIVATE EXECUTORCH_BUILD_CUDA
63+
)
64+
endif()
5365
if(EXECUTORCH_BUILD_MLX)
5466
if(NOT TARGET mlxdelegate OR NOT TARGET mlx)
5567
message(
@@ -63,3 +75,12 @@ if(EXECUTORCH_BUILD_MLX)
6375
)
6476
executorch_target_copy_mlx_metallib(nemotron3_diarization_runner)
6577
endif()
78+
if(MSVC AND EXECUTORCH_BUILD_CUDA)
79+
add_custom_command(
80+
TARGET nemotron3_diarization_runner
81+
POST_BUILD
82+
COMMAND
83+
${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:aoti_cuda_shims>
84+
$<TARGET_FILE_DIR:nemotron3_diarization_runner>
85+
)
86+
endif()

‎examples/models/nemotron3_diarization/CMakePresets.json‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"hidden": true,
77
"binaryDir": "${sourceDir}/../../../cmake-out/examples/models/nemotron3_diarization",
88
"cacheVariables": {
9+
"EXECUTORCH_BUILD_CUDA": "OFF",
910
"EXECUTORCH_BUILD_MLX": "OFF",
1011
"CMAKE_BUILD_TYPE": "Release",
1112
"CMAKE_FIND_ROOT_PATH": "${sourceDir}/../../../cmake-out",
@@ -28,6 +29,21 @@
2829
"rhs": "Darwin"
2930
}
3031
},
32+
{
33+
"name": "nemotron3-diarization-cuda",
34+
"displayName": "Nemotron 3 Diarization runner (CUDA)",
35+
"inherits": [
36+
"nemotron3-diarization-base"
37+
],
38+
"cacheVariables": {
39+
"EXECUTORCH_BUILD_CUDA": "ON"
40+
},
41+
"condition": {
42+
"type": "inList",
43+
"string": "${hostSystemName}",
44+
"list": ["Linux", "Windows"]
45+
}
46+
},
3147
{
3248
"name": "nemotron3-diarization-cpu",
3349
"displayName": "Nemotron 3 Diarization runner (XNNPACK)",
@@ -37,6 +53,11 @@
3753
}
3854
],
3955
"buildPresets": [
56+
{
57+
"name": "nemotron3-diarization-cuda",
58+
"configurePreset": "nemotron3-diarization-cuda",
59+
"configuration": "Release"
60+
},
4061
{
4162
"name": "nemotron3-diarization-mlx",
4263
"configurePreset": "nemotron3-diarization-mlx",
@@ -49,6 +70,19 @@
4970
}
5071
],
5172
"workflowPresets": [
73+
{
74+
"name": "nemotron3-diarization-cuda",
75+
"steps": [
76+
{
77+
"type": "configure",
78+
"name": "nemotron3-diarization-cuda"
79+
},
80+
{
81+
"type": "build",
82+
"name": "nemotron3-diarization-cuda"
83+
}
84+
]
85+
},
5286
{
5387
"name": "nemotron3-diarization-mlx",
5488
"steps": [

‎examples/models/nemotron3_diarization/README.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
# From the repository root:
55
python -m pip install git+https://github.com/huggingface/transformers "librosa>=0.10"
66

7-
# Choose mlx (Apple Silicon) or xnnpack (CPU).
7+
# Choose mlx (Apple Silicon), xnnpack (CPU), or cuda (NVIDIA GPU).
88
BACKEND=mlx
9-
# Export: BF16 for MLX/XNNPACK. Add --dtype fp32 to use FP32.
9+
# Export: BF16 by default. Add --dtype fp32 to use FP32.
1010
python -m executorch.examples.models.nemotron3_diarization.export_nemotron \
1111
--hf-model nvidia/Nemotron-3-Diarization \
1212
--backend "$BACKEND" \
@@ -17,9 +17,20 @@ make "nemotron3-diarization-${BACKEND/xnnpack/cpu}"
1717

1818
# Convert to mono 16 kHz PCM16 and run
1919
ffmpeg -i audio.wav -ac 1 -ar 16000 -c:a pcm_s16le audio_16khz_mono.wav
20+
DATA_PATH=""
21+
if [ "$BACKEND" = cuda ]; then
22+
DATA_PATH="nemotron_exports/cuda/aoti_cuda_blob.ptd"
23+
fi
2024
cmake-out/examples/models/nemotron3_diarization/nemotron3_diarization_runner \
2125
--model_path="nemotron_exports/$BACKEND/nemotron3_diarization.pte" \
26+
--data_path="$DATA_PATH" \
2227
--audio_path=audio_16khz_mono.wav \
2328
--preset=offline \
2429
--output=segments.json
2530
```
31+
32+
CUDA export requires a CUDA-enabled PyTorch installation and a CUDA toolkit.
33+
The preprocessor runs on CPU with XNNPACK; `pre_encode` and `encode` run on CUDA,
34+
with float32 inputs and outputs and BF16 (or FP32) model computation. Export
35+
produces `nemotron3_diarization.pte` and `aoti_cuda_blob.ptd`; pass the latter
36+
through `--data_path` as shown above.

‎examples/models/nemotron3_diarization/export_nemotron.py‎

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,26 @@ def forward(self, features):
6363
class Encode(nn.Module):
6464
"""Adapt the stateless Transformers network to the native streaming ABI."""
6565

66-
def __init__(self, model):
66+
def __init__(self, model, *, expand_attention_mask=False):
6767
super().__init__()
6868
self.model = model.model
6969
self.classifier = model.classifier
7070
self.dtype = model.dtype
71+
self.expand_attention_mask = expand_attention_mask
7172

7273
def forward(self, embeddings, lengths):
7374
positions = torch.arange(embeddings.shape[1], device=embeddings.device)
7475
mask = (positions[None, :] < lengths[:, None])[:, None, None, :]
76+
if self.expand_attention_mask:
77+
# CUDA SDPA requires the full query dimension in the mask.
78+
mask = mask.expand(-1, -1, embeddings.shape[1], -1)
7579
encoded = self.model(
7680
inputs_embeds=embeddings.to(self.dtype), attention_mask=mask
7781
)
7882
return self.classifier(encoded.last_hidden_state).sigmoid().float()
7983

8084

81-
def capture_model(model, feature_extractor):
85+
def capture_model(model, feature_extractor, *, expand_attention_mask=False):
8286
"""Capture backend-independent programs; streaming state stays in the runner."""
8387
audio = model.config.audio_config
8488
head = model.config.head_config
@@ -145,7 +149,7 @@ def capture_model(model, feature_extractor):
145149
strict=False,
146150
),
147151
"encode": export(
148-
Encode(model),
152+
Encode(model, expand_attention_mask=expand_attention_mask),
149153
(
150154
torch.zeros(1, 128, audio.hidden_size),
151155
torch.tensor([128], dtype=torch.int64),
@@ -169,27 +173,49 @@ def export_model(
169173
from executorch.backends.mlx.partitioner import MLXPartitioner
170174
from executorch.backends.mlx.passes import get_default_passes
171175

172-
partitioners = [MLXPartitioner()]
176+
partitioners = {name: [MLXPartitioner()] for name in ("pre_encode", "encode")}
173177
passes = get_default_passes()
174178
elif backend == "xnnpack":
175-
partitioners = [XnnpackPartitioner(enable_bf16=dtype == torch.bfloat16)]
179+
partitioners = {
180+
name: [XnnpackPartitioner(enable_bf16=dtype == torch.bfloat16)]
181+
for name in ("pre_encode", "encode")
182+
}
183+
elif backend == "cuda":
184+
from executorch.backends.cuda.cuda_backend import CudaBackend
185+
from executorch.backends.cuda.cuda_partitioner import CudaPartitioner
186+
from executorch.exir.backend.compile_spec_schema import CompileSpec
187+
188+
# The custom Triton SDPA kernel supports BF16 only.
189+
triton_mode = b"ON" if dtype == torch.bfloat16 else b"OFF"
190+
partitioners = {
191+
name: [
192+
CudaPartitioner(
193+
[
194+
CudaBackend.generate_method_name_compile_spec(name),
195+
CompileSpec("triton_kernel_mode", triton_mode),
196+
]
197+
)
198+
]
199+
for name in ("pre_encode", "encode")
200+
}
176201
else:
177202
raise ValueError(f"Unknown backend: {backend}")
203+
partitioners["preprocessor"] = [XnnpackPartitioner()]
178204

179205
model = Nemotron3DiarizationForAudioFrameClassification.from_pretrained(
180206
model_id, revision=revision, dtype=dtype
181207
)
182208
processor = Nemotron3DiarizationProcessor.from_pretrained(
183209
model_id, revision=revision
184210
)
185-
programs, metadata = capture_model(model, processor.feature_extractor)
211+
programs, metadata = capture_model(
212+
model,
213+
processor.feature_extractor,
214+
expand_attention_mask=backend == "cuda",
215+
)
186216
edge = to_edge_transform_and_lower(
187217
programs,
188-
partitioner={
189-
"preprocessor": [XnnpackPartitioner()],
190-
"pre_encode": partitioners,
191-
"encode": partitioners,
192-
},
218+
partitioner=partitioners,
193219
transform_passes={
194220
"preprocessor": [],
195221
"pre_encode": passes,
@@ -212,6 +238,9 @@ def export_model(
212238
with path.open("wb") as output:
213239
program.write_to_file(output)
214240
print(f"Saved {path} ({path.stat().st_size / 2**20:.1f} MiB)")
241+
if backend == "cuda":
242+
program.write_tensor_data_to_file(str(output_dir))
243+
print(f"Saved {output_dir / 'aoti_cuda_blob.ptd'}")
215244
return path
216245

217246

@@ -223,7 +252,7 @@ def main():
223252
help="Hugging Face model ID or local Transformers checkpoint directory",
224253
)
225254
parser.add_argument("--revision", help="Hugging Face model revision")
226-
parser.add_argument("--backend", choices=("mlx", "xnnpack"), default="mlx")
255+
parser.add_argument("--backend", choices=("mlx", "xnnpack", "cuda"), default="mlx")
227256
parser.add_argument("--output-dir", type=Path, default=Path("nemotron_exports"))
228257
parser.add_argument(
229258
"--dtype",

‎examples/models/nemotron3_diarization/main.cpp‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ DEFINE_string(
2727
model_path,
2828
"nemotron_exports/nemotron3_diarization.pte",
2929
"Exported model");
30+
DEFINE_string(data_path, "", "External CUDA data file (aoti_cuda_blob.ptd)");
3031
DEFINE_string(audio_path, "", "Mono 16 kHz WAV file (PCM16 or float32)");
3132
DEFINE_string(preset, "offline", "offline, low, very_low, or ultra_low");
3233
DEFINE_int32(feed_samples, 4096, "Samples per streaming feed");
@@ -46,6 +47,12 @@ int main(int argc, char** argv) {
4647
throw std::invalid_argument(
4748
"Provide --audio_path, positive --feed_samples, and --threshold in [0,1]");
4849
}
50+
#ifdef EXECUTORCH_BUILD_CUDA
51+
if (FLAGS_data_path.empty()) {
52+
throw std::invalid_argument(
53+
"Provide --data_path pointing to aoti_cuda_blob.ptd for CUDA");
54+
}
55+
#endif
4956
#ifdef EXECUTORCH_BUILD_MLX
5057
if (FLAGS_mlx_cache_limit_mb < 0 || FLAGS_mlx_memory_limit_mb <= 0) {
5158
throw std::invalid_argument(
@@ -58,7 +65,8 @@ int main(int argc, char** argv) {
5865
#endif
5966
nemotron3::Runner runner(
6067
FLAGS_model_path,
61-
nemotron3::StreamingConfig::from_preset(FLAGS_preset));
68+
nemotron3::StreamingConfig::from_preset(FLAGS_preset),
69+
FLAGS_data_path);
6270
auto header = executorch::extension::llm::load_wav_header(FLAGS_audio_path);
6371
if (!header || header->NumOfChan != 1 ||
6472
header->SamplesPerSec != runner.sample_rate()) {

‎examples/models/nemotron3_diarization/runner.cpp‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ StreamingConfig StreamingConfig::from_preset(const std::string& preset) {
9393
throw std::invalid_argument("Unknown streaming preset: " + preset);
9494
}
9595

96-
Runner::Runner(const std::string& model_path, StreamingConfig config)
97-
: model_(model_path, executorch::extension::Module::LoadMode::Mmap),
96+
Runner::Runner(
97+
const std::string& model_path,
98+
StreamingConfig config,
99+
const std::string& data_path)
100+
: model_(
101+
model_path,
102+
data_path,
103+
executorch::extension::Module::LoadMode::Mmap),
98104
config_(config) {
99105
if (model_.load() != Error::Ok) {
100106
throw std::runtime_error("Could not load " + model_path);

‎examples/models/nemotron3_diarization/runner.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ struct StreamingConfig {
2727

2828
class Runner {
2929
public:
30-
explicit Runner(const std::string& model_path, StreamingConfig config = {});
30+
explicit Runner(
31+
const std::string& model_path,
32+
StreamingConfig config = {},
33+
const std::string& data_path = "");
3134

3235
// Returns new 10 ms frames, with num_speakers() probabilities per frame.
3336
// Audio is mono at sample_rate(). final=true flushes the remaining lookahead.

0 commit comments

Comments
 (0)