Skip to content

Commit 17c6003

Browse files
justinchubyCopilot
andcommitted
Merge origin/main into feat/paged-cache; fold --paged-cache into --features
Update from main (brings in the cargo-style --features build option) and fold the paged / block-table KV cache toggle into it: - Add 'paged-cache' to _BUILD_FEATURES so 'mobius build --features paged-cache' enables the paged KV cache, matching static-cache / fp8-kv-cache / text-only. - Remove the standalone --paged-cache boolean flag (main dropped the other boolean feature flags); --page-size / --num-pages remain as tuning params. - Reword paged validation errors and docs (README, cli_reference, CHANGELOG) to reference --features paged-cache. - Add a cli_test covering --features paged-cache. Conflicts resolved in __main__.py, tasks/_causal_lm.py (keep both paged params and prune_lm_head), and CHANGELOG.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
2 parents 57f012e + 195087b commit 17c6003

62 files changed

Lines changed: 5634 additions & 141 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,5 @@ docs/feature-flags.md
228228
*.m4a
229229
*.wav
230230
.olive-cache/
231+
.scratch/
232+
uv.lock

CHANGELOG.md

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Paged / block-table KV cache export (`--paged-cache`)
10+
### Paged / block-table KV cache export (`--features paged-cache`)
1111

1212
#### Added
1313

14-
- `CausalLMTask(paged_cache=True)` and the `mobius build --paged-cache` CLI flag
14+
- `CausalLMTask(paged_cache=True)` and `mobius build --features paged-cache`
1515
export a **paged / block-table KV cache** (onnx-genai `docs/DESIGN.md` §39.4
1616
Option C — vLLM PagedAttention / SGLang RadixAttention layout). KV lives in a
1717
shared per-layer **page pool** `key_pool.{i}` / `value_pool.{i}`
@@ -20,31 +20,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
for each newly written token. Attention writes new K/V into the pool via
2121
`ScatterND`, assembles the sequence's pages contiguously via
2222
`Gather(pool, block_table)`, then runs the opset-24 `Attention` op with
23-
`nonpad_kv_seqlen` (input #6) — identical op contract to `--static-cache`, but
24-
over non-contiguous pages. Because sequences can list the *same* physical page
25-
in their `block_table`, the same graph expresses RadixAttention shared-prefix
26-
pages with no change. Paging uses only standard ONNX ops (no custom op).
27-
New tuning flags `--page-size` (default 16) and `--num-pages` (dynamic when
28-
omitted). Requires `DecoderLayer` / `MoEDecoderLayer` models; mutually
29-
exclusive with `--static-cache`. Targets a single active sequence
30-
(`batch == 1`); multi-sequence batching is a documented TODO.
23+
`nonpad_kv_seqlen` (input #6) — identical op contract to `--features
24+
static-cache`, but over non-contiguous pages. Because sequences can list the
25+
*same* physical page in their `block_table`, the same graph expresses
26+
RadixAttention shared-prefix pages with no change. Paging uses only standard
27+
ONNX ops (no custom op). New tuning flags `--page-size` (default 16) and
28+
`--num-pages` (dynamic when omitted). Requires `DecoderLayer` /
29+
`MoEDecoderLayer` models; mutually exclusive with `--features static-cache`.
30+
Targets a single active sequence (`batch == 1`); multi-sequence batching is a
31+
documented TODO.
32+
33+
### NVIDIA Cosmos 3 Edge vision-language model (`cosmos3_edge`)
3134

32-
---
35+
#### Added
36+
37+
- Support for the **full `cosmos3_edge` vision-language model**
38+
(`nvidia/Cosmos3-Edge`, `Cosmos3EdgeForConditionalGeneration`) as a 3-model
39+
onnxruntime-genai split (`decoder` + `vision_encoder` + `embedding`):
40+
- **decoder**: grouped-query-attention text reasoner with a **non-gated
41+
squared-ReLU FFN** (`hidden_act="relu2"`, `up_proj → relu2 → down_proj`)
42+
and 3D multimodal RoPE (`mrope_section=[24, 20, 20]`); takes
43+
`inputs_embeds`.
44+
- **vision_encoder**: SigLIP vision tower + a new
45+
`Cosmos3EdgeMultiModalProjector` (pre-shuffle `LayerNorm` → 2×2
46+
pixel-shuffle → `linear_fc1` → GELU → `linear_fc2`).
47+
- **embedding**: token embedding + image-feature fusion at
48+
`image_token_id=19`.
49+
`preprocess_weights` routes the single HF checkpoint to the three
50+
sub-models: `model.visual.*` / `model.projector.*` → vision (with SigLIP
51+
`mlp.fc1/fc2``up_proj/down_proj`), `embed_tokens` → embedding, the
52+
top-level text tower (`layers.*` / `norm` / `lm_head`) → decoder (renaming
53+
`self_attn.to_{q,k,v,out}``{q,k,v,o}_proj`), and drops the
54+
generator-tower `k_norm_und_for_gen` key-norm. Built via a new
55+
`Cosmos3EdgeVLTask` (`cosmos3-edge-vl`). The decoder-only text reasoner
56+
remains available as `cosmos3_edge_text`.
57+
- **L1 graph-build tested only.** NVIDIA does not publish modeling code for
58+
`cosmos3_edge` (not in `transformers`, no remote-code module), so the exact
59+
pixel-shuffle ordering and numerical parity are unverifiable; L4/L5 parity
60+
is deferred. The `cosmos3_omni` variants (`Cosmos3-Nano`/`-Super`) are
61+
two-tower diffusion world models tracked separately.
62+
63+
### Cargo-style `--features` build option
64+
65+
#### Added
66+
67+
- `mobius build --features <a,b,...>` collects the build-mode toggles under a
68+
single Rust/cargo-style option. Accepts a comma-separated list and may be
69+
repeated (`--features fp8-kv-cache,static-cache` or `--features fp8-kv-cache
70+
--features static-cache`). Available features: `static-cache`, `fp8-kv-cache`,
71+
`prune-lm-head`, `text-only`. Unknown feature names are rejected with an error
72+
listing the valid set.
73+
74+
#### Changed
75+
76+
- The boolean flags `--static-cache`, `--fp8-kv-cache`, and `--text-only` have
77+
been **removed** in favor of the equivalent `--features` value. Companion
78+
value args (`--max-seq-len`, `--kv-cache-scale-file`) are unchanged.
79+
80+
### Final-token LM-head pruning (`--features prune-lm-head`)
81+
82+
#### Added
83+
84+
- `build(prune_lm_head=True)` and `mobius build --features prune-lm-head`
85+
select the final hidden-state position before the LM-head projection, reducing
86+
prefill logits from `[B, S, vocab]` to `[B, 1, vocab]`. This avoids computing
87+
unused per-token logits for single-token autoregressive generation. Models
88+
with custom forward paths that do not support pre-projection pruning fail
89+
explicitly instead of silently producing an unoptimized graph.
90+
91+
### FP8 (E4M3) KV-cache export (`--features fp8-kv-cache`)
92+
93+
#### Added
94+
95+
- `build(fp8_kv_cache=True)` and `mobius build --features fp8-kv-cache` retype the
96+
fused `GroupQueryAttention` KV cache to `FLOAT8E4M3FN` (per-tensor E4M3) after
97+
GQA fusion, adding `k_scale`/`v_scale` initializers and the
98+
`k_quant_type`/`v_quant_type="PER_TENSOR"`, `kv_cache_bit_width=8` attributes.
99+
Halves KV-cache memory at long context on ORT runtimes with the FP8 KV kernel
100+
(SM89+). `--kv-cache-scale-file` supplies calibrated per-layer scales
101+
(onnxruntime-genai format); without it all layers use a unit scale of 1.0.
102+
Only graph-input or empty-placeholder caches are retyped — a non-empty
103+
initializer cache is skipped with a warning.
33104

34-
### Text-only export for multimodal Gemma 4 (`--text-only`)
105+
### Text-only export for multimodal Gemma 4 (`--features text-only`)
35106

36107
#### Added
37108

38-
- `build(text_only=True)` and the `mobius build --text-only` CLI flag export the
109+
- `build(text_only=True)` and `mobius build --features text-only` export the
39110
**text backbone** of a unified multimodal checkpoint as a standalone
40111
decoder-only LLM. For `gemma4_unified` (`google/gemma-4-12B`) this remaps the
41112
model type to its text sibling (`gemma4_unified_text`) and strips the
42113
vision/audio config so the decoder fuses to `GroupQueryAttention` on
43114
GQA-capable execution providers (CUDA/DML) instead of the float-bias
44115
`Attention` path forced by the multimodal bidirectional vision-block overlay.
45-
`--text-only` is rejected with `--config` / `--component` and now also bypasses
46-
diffusers autodetect so `build()` validation runs (a diffusers/unsupported repo
47-
raises instead of silently exporting a pipeline).
116+
The `text-only` feature is rejected with `--config` / `--component` and now
117+
also bypasses diffusers autodetect so `build()` validation runs (a
118+
diffusers/unsupported repo raises instead of silently exporting a pipeline).
48119

49120
#### Changed
50121

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ mobius build --model Qwen/Qwen-Image-2512 output_dir/
112112
mobius build --model openai/whisper-tiny output_dir/
113113
```
114114

115+
Build-mode toggles use the cargo-style `--features` option. Available features
116+
are `static-cache`, `fp8-kv-cache`, `paged-cache`, `prune-lm-head`, and
117+
`text-only`. Pass them as a comma-separated list or repeat the option:
118+
119+
```sh
120+
mobius build --model meta-llama/Llama-3.2-1B output_dir/ \
121+
--features static-cache,prune-lm-head --max-seq-len 2048
122+
```
123+
115124
See the [CLI Reference](https://onnxruntime.github.io/mobius/cli_reference.html) for all subcommands and flags.
116125

117126
### Examples

docs/cli_reference.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,31 +168,67 @@ mobius build --model Qwen/Qwen2.5-0.5B output/ \
168168
--ep cuda --dtype f16 --runtime ort-genai
169169
```
170170

171-
### Static Cache (`--static-cache`)
171+
### Build Features (`--features`)
172172

173+
Build-mode toggles are collected under a single cargo-style `--features`
174+
option. Pass a comma-separated list (and/or repeat the flag):
175+
176+
```
177+
--features fp8-kv-cache,static-cache
178+
--features prune-lm-head
179+
--features text-only
173180
```
174-
--static-cache
181+
182+
Available features:
183+
184+
| Feature | Effect |
185+
|---------|--------|
186+
| `static-cache` | Pre-allocate fixed-size KV cache buffers using `TensorScatter` (pair with `--max-seq-len N`). Requires `DecoderLayer` / `MoEDecoderLayer` models. Cannot combine with `--task`. |
187+
| `fp8-kv-cache` | Store the `GroupQueryAttention` KV cache as `FLOAT8E4M3FN` (per-tensor E4M3), halving KV-cache memory. Requires a GQA build (e.g. `--ep cuda --dtype f16`) and an ORT runtime with the FP8 KV-cache kernel (SM89+). Pair with `--kv-cache-scale-file` for calibrated scales. |
188+
| `paged-cache` | Export a paged / block-table KV cache (vLLM PagedAttention / SGLang RadixAttention layout): a shared page pool plus `block_table` / `slot_mapping`, using only standard ONNX ops. Tune with `--page-size N` (default 16) and `--num-pages N` (dynamic when omitted). Requires `DecoderLayer` / `MoEDecoderLayer` models. Cannot combine with `--task` or the `static-cache` feature. |
189+
| `prune-lm-head` | Select the final hidden-state position before the LM-head projection and emit logits shaped `[B, 1, vocab]`. Supported by models using the base `CausalLMModel.forward()` path; unsupported custom forwards fail explicitly. Use only when the downstream workflow does not need per-token logits. |
190+
| `text-only` | Export the text backbone of a multimodal checkpoint as a standalone decoder-only LLM (see below). |
191+
192+
The legacy boolean flags `--static-cache`, `--fp8-kv-cache`, and
193+
`--text-only` have been removed in favor of `--features`.
194+
195+
```bash
196+
mobius build --model meta-llama/Llama-3.2-1B output/ \
197+
--features static-cache --max-seq-len 2048
198+
199+
mobius build --model Qwen/Qwen2.5-0.5B output/ \
200+
--ep cuda --dtype f16 --features fp8-kv-cache
201+
202+
mobius build --model meta-llama/Llama-3.2-1B output/ \
203+
--features prune-lm-head
204+
```
205+
206+
### Static Cache (`--features static-cache`)
207+
208+
```
209+
--features static-cache
175210
--max-seq-len N
176211
```
177212

178213
Pre-allocate fixed-size KV cache buffers using TensorScatter. Useful when
179214
the maximum sequence length is known up front.
180215

181-
- `--static-cache` enables static cache mode. Requires models using
216+
- `--features static-cache` enables static cache mode. Requires models using
182217
`DecoderLayer` or `MoEDecoderLayer`.
183218
- `--max-seq-len N` sets the maximum sequence length for static cache
184-
buffers. Only valid with `--static-cache`. Defaults to
219+
buffers. Only valid with static cache. Defaults to
185220
`max_position_embeddings` from the model config.
186221

187222
Cannot be combined with `--task`.
188223

189224
#### Example
190225

191226
```bash
192-
mobius build --model meta-llama/Llama-3.2-1B output/ --static-cache
227+
mobius build --model meta-llama/Llama-3.2-1B output/ --features static-cache
193228

194229
# With explicit max sequence length
195-
mobius build --model meta-llama/Llama-3.2-1B output/ --static-cache --max-seq-len 2048
230+
mobius build --model meta-llama/Llama-3.2-1B output/ \
231+
--features static-cache --max-seq-len 2048
196232
```
197233

198234
### Other Flags
@@ -206,13 +242,13 @@ mobius build --model meta-llama/Llama-3.2-1B output/ --static-cache --max-seq-le
206242
| `--max-shard-size SIZE` | Maximum shard size for safetensors external data (e.g. `5GB`). Only used with `--external-data safetensors`. |
207243
| `--trust-remote-code` | Trust remote code when loading the HuggingFace model config. |
208244
| `--component NAME` | Build only one component from a diffusers pipeline (e.g. `--component vae_decoder`). |
209-
| `--text-only` | Export the text backbone of a multimodal checkpoint as a standalone decoder-only LLM. Strips vision/audio routing so the decoder uses `GroupQueryAttention` on GQA-capable EPs (build with `--ep cuda`/`dml`). Currently supported for `gemma4_unified` (`google/gemma-4-12B`). Not compatible with `--config` or `--component`. |
245+
| `--kv-cache-scale-file PATH` | Optional JSON file of calibrated per-layer FP8 KV-cache scales (onnxruntime-genai format). Only used with the `fp8-kv-cache` feature; without it all layers use a unit scale of 1.0. |
210246

211247
#### Text-only example
212248

213249
```bash
214250
# Export gemma-4-12B's text backbone as a GQA decoder-only LLM
215-
mobius build --model google/gemma-4-12B output/ --text-only --ep cuda --dtype f16
251+
mobius build --model google/gemma-4-12B output/ --features text-only --ep cuda --dtype f16
216252
```
217253

218254
For a full ORT-GenAI text-only package (with `genai_config.json`), use

examples/gemma4_12b_text_ort_genai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
5757
The equivalent raw-ONNX (no genai_config) build is::
5858
59-
mobius build --model google/gemma-4-12B --text-only --ep cuda --dtype f16 \
59+
mobius build --model google/gemma-4-12B --features text-only --ep cuda --dtype f16 \
6060
out/gemma4_12b_text_onnx/
6161
"""
6262

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ requires-python = ">=3.10"
1414
license = "MIT"
1515
dependencies = [
1616
"huggingface_hub",
17+
"ml_dtypes",
1718
"numpy>=1.24.0",
18-
"onnx_ir>=0.1.0",
19+
"onnx_ir>=0.2.1",
1920
"onnx-shape-inference>=0.3.1",
2021
"onnxscript>=0.7.1",
2122
"safetensors",
@@ -136,6 +137,7 @@ ignore = [
136137
"RUF031",
137138
"RUF052",
138139
"RUF067",
140+
"RUF105",
139141
"SIM102",
140142
"SIM108",
141143
"SIM114",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
lintrunner-adapters>=0.14.0
22
# RUFF, RUFF-FIX
3-
ruff==0.15.14
3+
ruff==0.16.1

scripts/generate_dashboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def l5_passes(self) -> bool:
145145

146146
@property
147147
def confidence_level(self) -> int:
148-
"""Return the highest confidence level achieved (0-5)."""
148+
"""The highest confidence level achieved (0-5)."""
149149
if self.l5_passes:
150150
return 5
151151
if self.l4_passes:
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Regenerate the Sortformer diarization golden reference used by the L4/L5 tests.
5+
6+
This produces ``testdata/golden/speech/sortformer_diarization.npz`` by running
7+
the *real* NeMo Sortformer model through the NeMo toolkit (the ground-truth
8+
reference implementation). It must be run inside an environment that has
9+
``nemo_toolkit`` installed (it is **not** a mobius runtime dependency)::
10+
11+
python -m venv /tmp/nemo_ref_venv
12+
source /tmp/nemo_ref_venv/bin/activate
13+
pip install "nemo_toolkit[asr]"
14+
python scripts/generate_sortformer_golden.py \
15+
--model nvidia/diar_streaming_sortformer_4spk-v2.1 \
16+
--revision fafaab5faa1617a0ca52d38dd3dc4bd636800d3d \
17+
--out testdata/golden/speech/sortformer_diarization.npz
18+
19+
The offline forward path is ``frontend_encoder`` (mel features -> embedding
20+
sequence) followed by ``forward_infer`` (embeddings -> per-frame speaker
21+
activity sigmoids). The committed ``.npz`` stores the mel input, the encoder
22+
embeddings, and the speaker probabilities, plus a ``meta`` JSON blob (model id,
23+
revision, NeMo version, dtype, seed) so the reference is self-describing and
24+
auditable.
25+
"""
26+
27+
from __future__ import annotations
28+
29+
import argparse
30+
import json
31+
32+
import numpy as np
33+
import torch
34+
35+
# Deterministic mel-feature fixture (also recorded in metadata).
36+
_SEED = 0
37+
_T = 400 # mel frames; with 8x subsampling -> 50 output diarization frames.
38+
39+
40+
def main() -> None:
41+
parser = argparse.ArgumentParser(description=__doc__)
42+
parser.add_argument("--model", default="nvidia/diar_streaming_sortformer_4spk-v2.1")
43+
parser.add_argument(
44+
"--revision",
45+
default="fafaab5faa1617a0ca52d38dd3dc4bd636800d3d",
46+
help="HuggingFace Hub commit SHA to pin the reference model.",
47+
)
48+
parser.add_argument(
49+
"--out",
50+
default="testdata/golden/speech/sortformer_diarization.npz",
51+
)
52+
args = parser.parse_args()
53+
54+
import nemo # type: ignore[import-not-found]
55+
from huggingface_hub import hf_hub_download
56+
from nemo.collections.asr.models import ( # type: ignore[import-not-found]
57+
SortformerEncLabelModel,
58+
)
59+
60+
torch.manual_seed(_SEED)
61+
62+
nemo_path = hf_hub_download(
63+
repo_id=args.model,
64+
filename="diar_streaming_sortformer_4spk-v2.1.nemo",
65+
revision=args.revision,
66+
)
67+
model = SortformerEncLabelModel.restore_from(nemo_path, map_location="cpu")
68+
model.eval()
69+
# Offline (non-streaming) forward path: full-context attention.
70+
model.streaming_mode = False
71+
72+
feat_dim = int(model.cfg.encoder.feat_in)
73+
mel = torch.randn(1, feat_dim, _T)
74+
mel_len = torch.tensor([_T], dtype=torch.long)
75+
76+
with torch.no_grad():
77+
emb_seq, emb_len = model.frontend_encoder(
78+
processed_signal=mel, processed_signal_length=mel_len
79+
)
80+
preds = model.forward_infer(emb_seq, emb_len)
81+
82+
num_spks = int(preds.shape[-1])
83+
meta = {
84+
"model_id": args.model,
85+
"revision": args.revision,
86+
"nemo_version": nemo.__version__,
87+
"dtype": "float32",
88+
"seed": _SEED,
89+
"feat_dim": feat_dim,
90+
"input_frames": _T,
91+
"num_spks": num_spks,
92+
}
93+
94+
np.savez_compressed(
95+
args.out,
96+
mel=mel.numpy().astype(np.float32),
97+
emb_seq=emb_seq.numpy().astype(np.float32),
98+
emb_len=emb_len.numpy().astype(np.int64),
99+
preds=preds.numpy().astype(np.float32),
100+
meta=np.array(json.dumps(meta)),
101+
)
102+
print(f"saved {args.out}\n{json.dumps(meta, indent=2)}")
103+
104+
105+
if __name__ == "__main__":
106+
main()

0 commit comments

Comments
 (0)