Pull requests / #2740

#2740 Add a Nemotron-H MoE engine path (Nemotron 3.5 Super VL 120B-A12B)

open · @Rhonstin · 0 comments · View on GitHub

BenchmarksSetup & installServer & APIAMD / HIPNVIDIA / CUDAModels & quantsDocumentationWindows

Description

## What this adds

An engine path for **NVIDIA Nemotron 3.5 Super VL 120B-A12B** (unsloth's GGUFs, architecture `nemotron_h_moe`): `src/nemotron_h`, which `strata` switches to from the file's architecture the same way it switches to Qwen3.6's path, plus a standalone `strata-nh`. Text, images (GENI) and the GGUF's MTP block.

- **Model**: 88 flat sublayers — 40 Mamba-2 mixers (causal conv + bias + SiLU, selective scan, SiLU(z)-gated group RMS norm), 8 NoPE GQA attention mixers (32 query / 2 KV heads, head 128, FP16 KV cache), 40 latent MoE blocks (sigmoid router with selection bias, top-22 of 512, normalized ×5; relu² experts without a gate in the 1024-wide latent; shared expert), the MTP block (`blk.88`) for drafts.
- **Hybrid experts** (78 GB of experts, always hybrid): per-layer VRAM slots that follow the routing; a batch of ≤ `--hybrid-rows` rows splits resident experts (GPU) and the rest (CPU, ggml-cpu `vec_dot`); bigger prompt chunks stream non-resident experts through a VRAM ring into per-expert FP16 GEMMs.
- **Taken over from Flash-Next's design**: a pinned expert arena on 2 MiB pages (THP + `cudaHostRegister`, falls back to `cudaHostAlloc`, then to the mapped file), an expert profile (`--expert-profile` / `--expert-profile-save`), the adaptive tier, the PCIe share of misses (`--pcie-frac`) and next-layer prefetch (`--prefetch`) — both opt-in —, and turn checkpoints of the recurrent state (a regenerated / edited last answer reads only the rest of its prompt).
- **Kernels**: `native_expert_grouped_relu2` / `native_expert_layout_ungated` in `iq_kernels.cu` — new, opt-in functions; the gated path is not touched.
- **Tokenizer / server**: the `tekken` pre-tokenizer with `ignore_merges` in `tools/strata_tokenizer.py`; `serve/server.py` reads `pre` from the pack's `tokenizer.json`, keeps a stop marker only when it is one token (this vocabulary has no `<|endoftext|>`, whose pieces would have stopped every reply at `<`), writes `<img> <image>×n </img>` for this model's pictures, and knows the family name.
- **strata-vision**: a model without M-RoPE gets a 1-D grid (needs a llama.cpp with `PROJECTOR_TYPE_NEMOTRON_OMNI`, upstream since PR #30520).
- **Tools**: `tools/nemotron_h/nh_llama_dump.cpp` (llama.cpp side, `-cmoe`, sublayer dumps) + `parity.py`; `nh_cpu_test`, `nh_cpu_bench`; `serve/test_nemotron_serve.py`.
- Design and every number: `docs/NEMOTRON_H.md`.

## What it leaves alone

Flash-Next's and Qwen3.6's engine paths and defaults. The reader's 64-entry array cap is not changed (this path takes the sublayer kinds from the tensors present). Qwen tokenization (`qwen35` stays the default `pre`). The new CMake targets build only with `STRATA_NATIVE_EXPERTS` and not on HIP.

## Tests

Measured on one PC: RTX 3090 24 GB, i7-7800X (6 cores, AVX-512), 95 GiB DDR4-2133, `UD-Q4_K_XL`; llama.cpp 0.7.0 (`620d2225c`) as the reference.

- **Parity with llama.cpp** (`tools/nemotron_h/parity.py`, teacher-forced): 5 chat prompts × 32 steps — first token matched 5/5; answers only (122 steps up to `<|im_end|>`): mean KL 0.00106, median 1.5e-5, top-1 0.992. Final build (arena + profile, 2 chat prompts + a 981-token one): answers-only KL 0.00124, top-1 0.984. With the PCIe share 0.6 + prefetch 16 (82% of routed entries on the GPU): KL 0.00279, top-1 0.979.
- **Long prompt / streamed path** (981 tokens): KL 0.00328 vs llama.cpp CUDA while llama.cpp's own CPU backend is at 0.00277; per-sublayer drift equals llama.cpp CPU's (layer 40: 0.176 vs 0.174, layer 80: 0.311 vs 0.302).
- **MTP**: greedy text identical with 2 and 3 drafts (96 tokens), 85% / 82% accepted; no speedup on this PC (verify windows route ~3× the distinct experts, mostly CPU misses), so the docs recommend `--mtp 0` there.
- **CPU experts** vs a dequantized f64 reference (`nh_cpu_test`, all 41 MoE layers, Q4_K/Q5_K/Q6_K/Q8_0 × Q5_0/IQ4_NL/Q8_0): worst relative error 1.2% (activation rounding).
- **Tokenizer** vs HF `tokenizers` on the model's `tokenizer.json`: 23 strings (chat markers, tool-call XML, Cyrillic, CJK, emoji, combining marks) identical; round trip 16/16.
- **Server** (`serve/server.py --engine strata`): plain answer, thinking with `reasoning_content` split, a tool call (`get_weather {"city":"Kyiv"}`, `finish_reason: tool_calls`), an image (shapes, colours and text described correctly; 300 image tokens, 59 ms on the GPU encoder), streaming, a second turn reusing the first (45 cached tokens), a regenerated answer restoring its turn checkpoint (prompt 88 ms instead of 2011 ms, same text).
- **Unit tests**: `serve.test_server` 314/314; `serve.test_family_name`, `serve.test_nemotron_serve`, `serve.test_images`, `serve.test_vision_busy`, `serve.test_detok`, `serve.test_reasoning_tools`, `serve.test_parser_track` 63 OK (5 skipped); `tools/test_setup_choices.py` OK.
- **Speed** (`strata-nh --bench-pp 2048 --bench-tg 32`): prompt 337–350 tok/s, decode 12.2–13.1 tok/s; through the server 246 tok/s prompt (2795 tokens), 11.2 tok/s decode. 2 MiB arena pages vs 4 KiB: decode 11.75 vs 9.16 tok/s at equal slots; profile on a held-out prompt: 13.48 vs 11.83 tok/s (VRAM share 34% vs 16%).
- **Builds**: CUDA 13.4, sm_86 (`strata`, `strata-nh`, `nh_cpu_test`, `nh_cpu_bench`, `strata-vision` against llama.cpp 0.7.0).

## Not tested / not done

HIP and SYCL builds (`iq_kernels.cu`'s new functions follow the file's HIP conventions but were not compiled for HIP); other GPUs and CPUs; MMQ for the prompt's experts; video input; a setup entry. A server left running for several hours stopped once; the cause was not investigated yet.

Related on this site

Quick links to related pages.