Devansh Batham commited on
Commit
d0a349e
·
1 Parent(s): 39d1566

Release compact nyx GGUF

Browse files
.gitattributes CHANGED
@@ -9,3 +9,4 @@ tokenizer.json filter=lfs diff=lfs merge=lfs -text
9
  *.tar filter=lfs diff=lfs merge=lfs -text
10
  *.tar.* filter=lfs diff=lfs merge=lfs -text
11
  *.zip filter=lfs diff=lfs merge=lfs -text
 
 
9
  *.tar filter=lfs diff=lfs merge=lfs -text
10
  *.tar.* filter=lfs diff=lfs merge=lfs -text
11
  *.zip filter=lfs diff=lfs merge=lfs -text
12
+ *.gguf filter=lfs diff=lfs merge=lfs -text
NOTICE CHANGED
@@ -1,12 +1,16 @@
1
  nyx
2
- Copyright 2026 devanshbatham
3
 
4
- Derived from Qwen/Qwen3.5-35B-A3B, revision
5
- 59d61f3ce65a6d9863b86d2e96597125219dc754.
6
 
7
  Qwen3.5 was created by the Qwen team at Alibaba Cloud and is distributed under
8
- the Apache License 2.0. nyx adds task-specific decision adaptation, packaging,
9
- runtime integration, and INT4+FP8 quantization. The original Qwen license is
10
- retained in LICENSE.
11
 
12
- Qwen and Alibaba Cloud do not endorse and are not affiliated with nyx.
 
 
 
 
 
1
  nyx
2
+ Copyright 2026 Devansh Batham
3
 
4
+ Derived from Qwen/Qwen3.5-4B, revision
5
+ 851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a.
6
 
7
  Qwen3.5 was created by the Qwen team at Alibaba Cloud and is distributed under
8
+ the Apache License 2.0. nyx adds decision-task adaptation, task-vector
9
+ composition, quantization, packaging, and runtime integration. The original
10
+ Qwen license is retained in LICENSE.
11
 
12
+ The benchmark SVG embeds an Iosevka font subset licensed separately under the
13
+ SIL Open Font License 1.1; see assets/IOSEVKA-OFL.md.
14
+
15
+ Qwen, Alibaba Cloud, TypeSafe, and Jev do not endorse and are not affiliated
16
+ with nyx.
README.md CHANGED
@@ -1,127 +1,95 @@
1
  ---
2
  license: apache-2.0
3
- library_name: sglang
4
  pipeline_tag: text-generation
5
  base_model:
6
- - Qwen/Qwen3.5-35B-A3B
7
  tags:
8
  - nyx
9
  - qwen3.5
10
- - mixture-of-experts
11
- - int4
12
- - fp8
13
- - rocm
14
- - sglang
15
  ---
16
 
17
  # nyx
18
 
19
- nyx is a task-specialized Qwen3.5-35B-A3B model for Choice, Score, and Noul decisions. It combines task-specific adaptation with INT4 routed-expert storage and FP8 expert compute.
20
 
21
- ![Six classification benchmarks comparing nyx and Jev](assets/nyx-vs-jev-benchmarks.png)
22
 
23
  ## Model
24
 
25
  | Property | Value |
26
  |---|---|
27
- | Base model | [`Qwen/Qwen3.5-35B-A3B`](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) |
28
- | Base revision | `59d61f3ce65a6d9863b86d2e96597125219dc754` |
29
- | Architecture | Qwen3.5-35B-A3B text MoE |
30
- | Quantization | Per-channel INT4 routed-expert storage; FP8 expert compute; dynamic FP8 dense layers |
31
- | Runtime precision | BF16 activations and KV cache; FP32 SSM state |
32
- | Measured model memory | 19.43 GB |
33
- | Tested GPU | AMD Instinct MI325X (`gfx942`) |
34
 
35
- The architecture, tokenizer, and base weights originate from Qwen. nyx adds decision-task adaptation, quantization, and its runtime integration. See [NOTICE](NOTICE) and [provenance.json](provenance.json).
36
-
37
- This is not a standard Transformers checkpoint. The weights use the included SGLang presharded loader and runtime patches; `AutoModel.from_pretrained` is not supported.
38
-
39
- ## Requirements
40
-
41
- - x86_64 Linux
42
- - AMD ROCm GPU with at least 32 GB VRAM; 64 GB or more is recommended for useful KV-cache and batching headroom
43
- - 64 GB system RAM minimum
44
- - 50 GB free disk
45
- - Docker, ROCm host drivers, and the Hugging Face `hf` CLI
46
-
47
- AMD Instinct MI325X is tested and recommended for high concurrency and long contexts. MI300X shares the `gfx942` target and is expected to be compatible but was not benchmarked here. Other ROCm devices depend on support in the pinned SGLang/AITER image. The bundled launcher does not support NVIDIA CUDA, Apple Silicon, or CPU inference.
48
 
49
  ## Run
50
 
51
  ```bash
52
- hf download devanshbatham/nyx --local-dir nyx
53
- cd nyx
54
- python3 verify.py
55
- bash serve.sh
56
- ```
57
-
58
- The launcher binds the SGLang model process to `127.0.0.1:30000`. That port is an unauthenticated private backend—not the public API. Keep it on loopback or a private network.
59
 
60
- Install the production gateway from [`github.com/devanshbatham/nyx`](https://github.com/devanshbatham/nyx). It exposes the supported `POST /v1/systemone` API on port `8000` with bearer authentication, typed validation, admission controls, deadlines, and cancellation.
 
61
 
62
- ```bash
63
- curl http://127.0.0.1:8000/v1/systemone \
64
- -H "Authorization: Bearer $(cat /etc/nyx/api-key)" \
65
- -H 'Content-Type: application/json' \
66
- -d '{"model":"nyx","state":"I loved it","questions":{"sentiment":{"type":"choice","instructions":"Classify sentiment","criteria":{"positive":null,"negative":null}}}}'
67
  ```
68
 
69
- ## TypeSafe drop-in
70
-
71
- The gateway provides 1:1 TypeSafe System One request/response wire compatibility for Choice, Score, Noul, model discovery, authentication, and request IDs. The unchanged official TypeSafe Python SDK `0.7.0` is tested end to end against the gateway; the official JavaScript SDK `0.6.0` is contract-tested for request construction and response parsing. Existing applications only change their base URL and API key; `jev-latest`, `jev-preview`, and `jev-1.13.0` are accepted as migration aliases, while responses identify the model as `nyx`.
72
 
73
- ```bash
74
- export TYPESAFE_BASE_URL=https://nyx.example.com
75
- export TYPESAFE_API_KEY="$(cat /etc/nyx/api-key)"
76
- ```
77
-
78
- Wire compatibility does not mean behavioral identity: decisions, probabilities, confidence, token accounting, limits, and latency can differ from Jev.
79
 
80
  ## Evaluation
81
 
82
- The frozen comparison used 2,277 identical requests across six public classification benchmarks at client concurrency 8. Both models completed every case with zero errors and zero retries.
83
 
84
- | Benchmark | nyx INT4+FP8 | Jev 1.13 |
85
- |---|---:|---:|
86
- | AG News | 84.75% | 86.50% |
87
- | TREC coarse | 92.00% | 93.00% |
88
- | CoLA | 79.00% | 77.75% |
89
- | RTE | 93.14% | 91.34% |
90
- | Emotion | 43.33% | 47.00% |
91
- | IMDb | 96.50% | 97.00% |
92
- | **Dataset-macro accuracy** | **81.45%** | **82.10%** |
93
- | **Dataset-macro F1** | **81.06%** | **81.85%** |
94
- | **Pooled accuracy** | **82.96%** | **83.62%** |
95
-
96
- | Timing | nyx INT4+FP8 | Jev 1.13 |
97
- |---|---:|---:|
98
- | Backend inference median / p95 | 62.97 / 85.30 ms | not exposed |
99
- | Round trip median / p95 | 66.25 / 89.22 ms | 182.33 / 245.12 ms |
100
- | Throughput | 112.67 req/s | 42.43 req/s |
101
 
102
- nyx ran on a local MI325X/API stack. Jev ran over remote HTTPS on unmatched infrastructure, so its latency is descriptive rather than a controlled model-speed comparison. The local runtime used continuous batching with `max-running-requests=64`; the benchmark's eight concurrent callers limited this run to eight active requests. See [the evaluation record](benchmarks/random-six-report.md).
103
 
104
- ### Before quantization
105
 
106
- The shared H1 development slice below compares the official Qwen base in BF16, the pre-quantization `nyx` BF16 checkpoint, and Jev 1.13.0 on the same 1,500 scored cases with identical labels and task semantics.
107
 
108
- | H1 task | Qwen base BF16 | `nyx` BF16 | Jev 1.13.0 |
109
- |---|---:|---:|---:|
110
- | Comment triage | 295/500 (59.00%) | **462/500 (92.40%)** | 455/500 (91.00%) |
111
- | Report completeness | 439/500 (87.80%) | **488/500 (97.60%)** | 468/500 (93.60%) |
112
- | Broad assets, exact set | 285/500 (57.00%) | **298/500 (59.60%)** | 284/500 (56.80%) |
113
- | **Overall** | **1,019/1,500 (67.93%)** | **1,248/1,500 (83.20%)** | **1,207/1,500 (80.47%)** |
114
 
115
- This is authored development evidence, not an independent public holdout. Qwen and `nyx` ran locally in BF16 on the same MI325X/SGLang stack; Jev ran through its remote API, so latency is intentionally omitted from this table. See the [pre-quantization evaluation record](benchmarks/prequant-h1.md).
116
 
117
  ## Limitations
118
 
119
  - Optimized for structured classification and decision workloads; general assistant quality was not established.
120
- - Only the included SGLang/ROCm runtime on one MI325X has been tested end to end.
121
- - Public benchmark overlap with model pretraining is unknown.
122
- - Outputs can be incorrect or biased. Use independent validation and human review for consequential decisions.
123
- - No bit-exact or behavioral parity with Jev, BF16, or the original Qwen model is claimed.
124
 
125
  ## License and attribution
126
 
127
- Apache License 2.0. nyx is derived from [`Qwen/Qwen3.5-35B-A3B`](https://huggingface.co/Qwen/Qwen3.5-35B-A3B), also distributed under Apache-2.0. Qwen and Alibaba Cloud are credited as the original model authors. This project is not endorsed by or affiliated with the Qwen authors or TypeSafe AI.
 
1
  ---
2
  license: apache-2.0
 
3
  pipeline_tag: text-generation
4
  base_model:
5
+ - Qwen/Qwen3.5-4B
6
  tags:
7
  - nyx
8
  - qwen3.5
9
+ - gguf
10
+ - llama-cpp
11
+ - quantized
12
+ - classification
 
13
  ---
14
 
15
  # nyx
16
 
17
+ nyx is a compact 4B decision model for Choice, Score, and Noul workloads. The release is one 3.29 GB GGUF that runs with llama.cpp on CPU, Apple Metal, NVIDIA CUDA, and AMD HIP.
18
 
19
+ ![Animated light-theme chart comparing the nyx reference and Jev across six classification benchmarks](assets/nyx-benchmarks.svg)
20
 
21
  ## Model
22
 
23
  | Property | Value |
24
  |---|---|
25
+ | Base | [`Qwen/Qwen3.5-4B`](https://huggingface.co/Qwen/Qwen3.5-4B) |
26
+ | Base revision | `851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a` |
27
+ | File | `nyx.gguf` |
28
+ | Size | 3,288,582,784 bytes (3.29 GB / 3.06 GiB) |
29
+ | Quantization | Importance-matrix IQ4_XS with selected Q5_K/Q6_K tensors |
30
+ | SHA-256 | `39d64fb19863cfc067e2411fcb2a0444113dda13798b15de5617736d91914359` |
31
+ | Runtime | Pinned llama.cpp build in [`devanshbatham/nyx`](https://github.com/devanshbatham/nyx) |
32
 
33
+ The architecture, tokenizer, and base weights originate from Qwen. nyx adds decision-task adaptation, task-vector composition, quantization, and a production API integration.
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  ## Run
36
 
37
  ```bash
38
+ git clone https://github.com/devanshbatham/nyx.git /opt/nyx
39
+ hf download devanshbatham/nyx --local-dir /opt/nyx-model
 
 
 
 
 
40
 
41
+ # auto-detects CPU, Metal, CUDA, or HIP
42
+ /opt/nyx/scripts/build-llama-cpp.sh /opt/llama.cpp
43
 
44
+ export LLAMA_SERVER_BIN=/opt/llama.cpp/build/bin/llama-server
45
+ export NYX_GGUF_PATH=/opt/nyx-model/nyx.gguf
46
+ /opt/nyx/scripts/serve-model.sh
 
 
47
  ```
48
 
49
+ Budget at least 8 GB of free RAM, VRAM, or unified memory for one 4,096-token slot and 6 GB of free disk. More context and concurrent slots require more memory. The model backend binds to unauthenticated loopback port `30000`; do not expose it publicly.
 
 
50
 
51
+ For the authenticated `POST /v1/systemone` API and typed Python/TypeScript clients, use [`github.com/devanshbatham/nyx`](https://github.com/devanshbatham/nyx). The gateway implements the TypeSafe System One wire contract for Choice, Score, Noul, model discovery, bearer authentication, and request IDs.
 
 
 
 
 
52
 
53
  ## Evaluation
54
 
55
+ The public comparison below is for the pre-quantization nyx reference. The pretrained Qwen base and nyx reference used the same local runtime; all three systems received the same 2,277 frozen requests.
56
 
57
+ | Benchmark | Qwen3.5-4B base | nyx reference | Jev 1.13 |
58
+ |---|---:|---:|---:|
59
+ | AG News | **87.75%** | 85.75% | 86.50% |
60
+ | TREC coarse | 86.60% | 84.20% | **93.00%** |
61
+ | CoLA | 77.75% | **81.50%** | 77.75% |
62
+ | RTE | 84.12% | 88.09% | **91.34%** |
63
+ | Emotion | **47.67%** | 46.33% | 47.00% |
64
+ | IMDb | 95.75% | 95.75% | **97.00%** |
65
+ | Dataset-macro accuracy | 79.94% | 80.27% | **82.10%** |
66
+ | Pooled accuracy | 81.42% | 81.51% | **83.62%** |
67
+ | Round trip, median / p95 | 54.85 / 67.96 ms | 55.56 / 72.77 ms | 182.33 / 245.12 ms |
68
+ | Throughput | 17.44 req/s | 17.15 req/s | 42.43 req/s |
 
 
 
 
 
69
 
70
+ Local timings used one AMD Instinct MI325X, one execution slot, Q8 KV cache, Flash Attention, and a 4,096-token context. Jev used remote HTTPS at concurrency 8, so its timing is descriptive rather than a hardware-controlled speed comparison.
71
 
72
+ ### Compact-release fidelity
73
 
74
+ The released GGUF was compared with the 8.42 GB reference on a separate frozen 1,024-case set.
75
 
76
+ | Metric | Reference | Released GGUF |
77
+ |---|---:|---:|
78
+ | Correct | 873/1,024 | 874/1,024 |
79
+ | Accuracy | 85.25% | 85.35% |
80
+ | Decision agreement | — | 98.73% (1,011/1,024) |
81
+ | Decision disagreements | — | 13 |
82
 
83
+ The compact model gained seven decisions and lost six relative to the reference. It did not meet the experiment's original 99.5% agreement target; publication followed an explicit decision to accept that tradeoff. See the [evaluation record](benchmarks/random-six-report.md).
84
 
85
  ## Limitations
86
 
87
  - Optimized for structured classification and decision workloads; general assistant quality was not established.
88
+ - The released GGUF has not been rerun on the full public 2,277-case suite.
89
+ - NVIDIA and Apple performance and cross-vendor decision parity have not been measured.
90
+ - Public-benchmark pretraining overlap is unknown.
91
+ - Outputs can be incorrect or biased. Validate consequential decisions independently.
92
 
93
  ## License and attribution
94
 
95
+ Apache License 2.0. nyx is derived from [`Qwen/Qwen3.5-4B`](https://huggingface.co/Qwen/Qwen3.5-4B), also distributed under Apache-2.0. Qwen and Alibaba Cloud are credited as the original model authors. This project is not endorsed by or affiliated with Qwen, Alibaba Cloud, TypeSafe, or Jev.
assets/IOSEVKA-OFL.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2015-2026, Renzhi Li (aka. Belleve Invis, belleve@typeof.net)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+
5
+ This license is copied below, and is also available with a FAQ at:
6
+ http://scripts.sil.org/OFL
7
+
8
+ --------------------------
9
+
10
+
11
+ SIL Open Font License v1.1
12
+ ====================================================
13
+
14
+
15
+ Preamble
16
+ ----------
17
+
18
+ The goals of the Open Font License (OFL) are to stimulate worldwide
19
+ development of collaborative font projects, to support the font creation
20
+ efforts of academic and linguistic communities, and to provide a free and
21
+ open framework in which fonts may be shared and improved in partnership
22
+ with others.
23
+
24
+ The OFL allows the licensed fonts to be used, studied, modified and
25
+ redistributed freely as long as they are not sold by themselves. The
26
+ fonts, including any derivative works, can be bundled, embedded,
27
+ redistributed and/or sold with any software provided that any reserved
28
+ names are not used by derivative works. The fonts and derivatives,
29
+ however, cannot be released under any other type of license. The
30
+ requirement for fonts to remain under this license does not apply
31
+ to any document created using the fonts or their derivatives.
32
+
33
+
34
+ Definitions
35
+ -------------
36
+
37
+ `"Font Software"` refers to the set of files released by the Copyright
38
+ Holder(s) under this license and clearly marked as such. This may
39
+ include source files, build scripts and documentation.
40
+
41
+ `"Reserved Font Name"` refers to any names specified as such after the
42
+ copyright statement(s).
43
+
44
+ `"Original Version"` refers to the collection of Font Software components as
45
+ distributed by the Copyright Holder(s).
46
+
47
+ `"Modified Version"` refers to any derivative made by adding to, deleting,
48
+ or substituting -- in part or in whole -- any of the components of the
49
+ Original Version, by changing formats or by porting the Font Software to a
50
+ new environment.
51
+
52
+ `"Author"` refers to any designer, engineer, programmer, technical
53
+ writer or other person who contributed to the Font Software.
54
+
55
+
56
+ Permission & Conditions
57
+ ------------------------
58
+
59
+ Permission is hereby granted, free of charge, to any person obtaining
60
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
61
+ redistribute, and sell modified and unmodified copies of the Font
62
+ Software, subject to the following conditions:
63
+
64
+ 1. Neither the Font Software nor any of its individual components,
65
+ in Original or Modified Versions, may be sold by itself.
66
+
67
+ 2. Original or Modified Versions of the Font Software may be bundled,
68
+ redistributed and/or sold with any software, provided that each copy
69
+ contains the above copyright notice and this license. These can be
70
+ included either as stand-alone text files, human-readable headers or
71
+ in the appropriate machine-readable metadata fields within text or
72
+ binary files as long as those fields can be easily viewed by the user.
73
+
74
+ 3. No Modified Version of the Font Software may use the Reserved Font
75
+ Name(s) unless explicit written permission is granted by the corresponding
76
+ Copyright Holder. This restriction only applies to the primary font name as
77
+ presented to the users.
78
+
79
+ 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
80
+ Software shall not be used to promote, endorse or advertise any
81
+ Modified Version, except to acknowledge the contribution(s) of the
82
+ Copyright Holder(s) and the Author(s) or with their explicit written
83
+ permission.
84
+
85
+ 5. The Font Software, modified or unmodified, in part or in whole,
86
+ must be distributed entirely under this license, and must not be
87
+ distributed under any other license. The requirement for fonts to
88
+ remain under this license does not apply to any document created
89
+ using the Font Software.
90
+
91
+
92
+
93
+ Termination
94
+ -----------
95
+
96
+ This license becomes null and void if any of the above conditions are
97
+ not met.
98
+
99
+
100
+ DISCLAIMER
101
+
102
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
103
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
104
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
105
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
106
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
107
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
108
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
109
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
110
+ OTHER DEALINGS IN THE FONT SOFTWARE.
111
+ Source font: Iosevka Regular webfont 34.8.0 from https://github.com/iosevka-webfonts/iosevka/tree/main/woff2.
112
+ This repository bundles an ASCII subset for reproducible chart generation.\n
assets/nyx-benchmarks.svg ADDED
benchmarks/prequant-h1.md DELETED
@@ -1,24 +0,0 @@
1
- # Pre-quantization H1 comparison
2
-
3
- This record isolates the exact intersection shared by three completed evaluations: the official `Qwen/Qwen3.5-35B-A3B` base in BF16, the pre-quantization `nyx` BF16 checkpoint, and Jev 1.13.0.
4
-
5
- ## Results
6
-
7
- | Task | Cases | Qwen base BF16 | `nyx` BF16 | Jev 1.13.0 |
8
- |---|---:|---:|---:|---:|
9
- | Comment triage | 500 | 295 (59.00%) | 462 (92.40%) | 455 (91.00%) |
10
- | Report completeness | 500 | 439 (87.80%) | 488 (97.60%) | 468 (93.60%) |
11
- | Broad assets, exact set | 500 | 285 (57.00%) | 298 (59.60%) | 284 (56.80%) |
12
- | **Overall** | **1,500** | **1,019 (67.93%)** | **1,248 (83.20%)** | **1,207 (80.47%)** |
13
-
14
- ## Protocol
15
-
16
- - The intersection contains comment fixtures 501–1000 plus all 500 authored report fixtures, scored once for completeness and once for broad-asset exact-set classification.
17
- - Case IDs and expected labels were matched across the three result sets. All 1,500 expected labels matched exactly; no case was excluded after matching.
18
- - Qwen and `nyx` used BF16 before any FP8 or INT4 conversion, the same AMD Instinct MI325X, the same SGLang/API stack, and server concurrency 8.
19
- - The base was the official `Qwen/Qwen3.5-35B-A3B` revision `59d61f3ce65a6d9863b86d2e96597125219dc754`.
20
- - Jev was evaluated as `jev-1.13.0` through its remote API with the same typed requests, prompts, thresholds, and reference labels. Because the serving infrastructure differs, this record makes no speed comparison.
21
-
22
- ## Scope
23
-
24
- These are synthetic, authored H1 development fixtures with known scenario-family overlap. They are useful for measuring task adaptation on this workload, but they are not an independent public holdout and do not establish general model superiority.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
benchmarks/random-six-report.md CHANGED
@@ -1,38 +1,71 @@
1
- # Six public classification benchmarks: nyx vs Jev
2
 
3
- Frozen paired cohort: **2,277 cases**; SHA-256 `046a38c24747fd724003aca7e5f1d1b6e148d599092222361c0717ecff84ecad`; client concurrency 8; every failure counts as wrong.
4
 
5
- | Dataset | Cases | nyx INT4+FP8 | Jev 1.13 |
6
- |---|---:|---:|---:|
7
- | AG News | 400 | 84.75% (339/400) | 86.50% (346/400) |
8
- | TREC coarse | 500 | 92.00% (460/500) | 93.00% (465/500) |
9
- | CoLA | 400 | 79.00% (316/400) | 77.75% (311/400) |
10
- | RTE | 277 | 93.14% (258/277) | 91.34% (253/277) |
11
- | Emotion | 300 | 43.33% (130/300) | 47.00% (141/300) |
12
- | IMDb | 400 | 96.50% (386/400) | 97.00% (388/400) |
13
 
14
- | Aggregate | nyx INT4+FP8 | Jev 1.13 |
15
- |---|---:|---:|
16
- | Dataset-macro accuracy | 81.45% | 82.10% |
17
- | Dataset-macro F1 | 81.06% | 81.85% |
18
- | Pooled micro accuracy | 82.96% | 83.62% |
 
 
 
 
 
 
19
 
20
- ## Paired comparison
21
 
22
- The exact two-sided McNemar test uses identical case-level predictions. Jev-only versus nyx-only correct cases were 76 to 61 (`p=0.23154`).
23
 
24
- | Timing | nyx INT4+FP8 | Jev 1.13 |
25
- |---|---:|---:|
26
- | Backend inference median / p95 | 62.97 / 85.30 ms | not exposed |
27
- | Round trip median / p95 | 66.25 / 89.22 ms | 182.33 / 245.12 ms |
28
- | Wall time | 20,209 ms | 53,666 ms |
29
- | Throughput | 112.67 req/s | 42.43 req/s |
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- ## Method and caveats
 
 
 
 
32
 
33
- - Dataset choices, examples, order, and labels were frozen before provider scoring; request bodies were byte-equivalent across providers.
 
 
 
 
34
  - AG News, CoLA, Emotion, and IMDb are fixed class-stratified samples; TREC test and RTE validation are complete.
35
- - Every request contained one Choice question. Eight asynchronous clients used persistent HTTP/1.1 connections. The local SGLang runtime used continuous batching with `max-running-requests=64`, while client concurrency limited this run to eight active requests.
36
- - Backend inference is parsed from the local `Server-Timing` header. Jev did not expose server timing.
37
- - nyx ran locally on an AMD Instinct MI325X and API stack. Jev ran over remote HTTPS on unmatched infrastructure, so cross-service latency is descriptive only.
38
- - Public-benchmark pretraining contamination is unknown for both models. This finite suite does not establish universal classification quality.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Six public classification benchmarks
2
 
3
+ These results describe the 8.42 GB pre-quantization nyx reference, not the 3.29 GB release GGUF. The compact file was selected using the separate fidelity evaluation reported below.
4
 
5
+ Frozen paired cohort: 2,277 cases; case SHA-256 `046a38c24747fd724003aca7e5f1d1b6e148d599092222361c0717ecff84ecad`; every failure counts as wrong.
 
 
 
 
 
 
 
6
 
7
+ | Dataset | Cases | Qwen3.5-4B base | nyx | Jev 1.13 |
8
+ |---|---:|---:|---:|---:|
9
+ | AG News | 400 | 87.75% (351/400) | 85.75% (343/400) | 86.50% (346/400) |
10
+ | TREC coarse | 500 | 86.60% (433/500) | 84.20% (421/500) | 93.00% (465/500) |
11
+ | CoLA | 400 | 77.75% (311/400) | 81.50% (326/400) | 77.75% (311/400) |
12
+ | RTE | 277 | 84.12% (233/277) | 88.09% (244/277) | 91.34% (253/277) |
13
+ | Emotion | 300 | 47.67% (143/300) | 46.33% (139/300) | 47.00% (141/300) |
14
+ | IMDb | 400 | 95.75% (383/400) | 95.75% (383/400) | 97.00% (388/400) |
15
+ | Dataset-macro accuracy | 2,277 | 79.94% | 80.27% | 82.10% |
16
+ | Dataset-macro F1 | 2,277 | 79.83% | 80.48% | 81.85% |
17
+ | Pooled micro accuracy | 2,277 | 81.42% (1,854/2,277) | 81.51% (1,856/2,277) | 83.62% (1,904/2,277) |
18
 
19
+ Jev-only versus nyx-only correct cases were 121 to 73; the exact two-sided McNemar p-value is `0.0006995`. nyx-only versus Qwen-only correct cases were 112 to 110 (`p=0.9465`).
20
 
21
+ ## Runtime
22
 
23
+ The release profile uses one llama.cpp execution slot because it was the fastest measured configuration for this latency-oriented workload.
24
+
25
+ | Measurement | Result |
26
+ |---|---:|
27
+ | Reference model file | 8,424,393,056 bytes |
28
+ | Measured VRAM | 9,927,208,960 bytes |
29
+ | Round trip, median | 55.56 ms |
30
+ | Round trip, p95 | 72.77 ms |
31
+ | Round trip, mean | 57.92 ms |
32
+ | Full-run wall time | 132,780.73 ms |
33
+ | Sustained throughput | 17.15 requests/s |
34
+
35
+ The nyx timing is direct loopback HTTP to llama.cpp on one AMD Instinct MI325X, with full GPU offload, Flash Attention, Q8 KV cache, 4,096 tokens per slot, and client/server concurrency 1. It excludes the authenticated gateway and external network. The Qwen base used the identical local profile and measured 54.85 ms median, 67.96 ms p95, and 17.44 requests/s.
36
+
37
+ The previously observed Jev remote-HTTPS run used client concurrency 8 and reported 182.33 ms median, 245.12 ms p95, and 42.43 requests/s. Different hardware, network topology, and concurrency make that timing descriptive rather than a controlled speed comparison.
38
+
39
+ ## Batching check
40
+
41
+ The first 120 frozen cases were repeated at three concurrency levels. The server had the same number of 4,096-token slots as the client concurrency.
42
 
43
+ | Concurrency | Correct | Median / p95 | Throughput |
44
+ |---:|---:|---:|---:|
45
+ | 1 | 94/120 | 57.20 / 89.01 ms | 15.79 requests/s |
46
+ | 2 | 94/120 | 162.80 / 219.25 ms | 11.77 requests/s |
47
+ | 8 | 94/120 | 606.22 / 887.68 ms | 11.61 requests/s |
48
 
49
+ All 120 class decisions were identical across the three runs. Batching was rejected because it reduced throughput and increased latency.
50
+
51
+ ## Protocol and limits
52
+
53
+ - Dataset selection, sampled examples, request order, and labels were frozen before provider scoring.
54
  - AG News, CoLA, Emotion, and IMDb are fixed class-stratified samples; TREC test and RTE validation are complete.
55
+ - Each request contained one Choice question. Both systems completed 2,277/2,277 cases with zero errors.
56
+ - Public-benchmark pretraining overlap is unknown. This finite suite does not establish universal classification quality.
57
+ - The NVIDIA RTX 4000 Ada target still requires an on-device latency and class-decision parity run before production rollout.
58
+
59
+ ## Compact release fidelity
60
+
61
+ The released importance-matrix IQ4_XS candidate was evaluated against the reference on a frozen 1,024-case selection spanning Banking77, BoolQ, CLINC150, code-defect annotation, commit type, DBpedia14, email spam, SNLI, structured rules, and tweet sentiment.
62
+
63
+ | Metric | Reference | Released GGUF |
64
+ |---|---:|---:|
65
+ | File size | 8,424,393,056 bytes | 3,288,582,784 bytes |
66
+ | Correct | 873/1,024 | 874/1,024 |
67
+ | Accuracy | 85.25% | 85.35% |
68
+ | Decision agreement | — | 98.73% (1,011/1,024) |
69
+ | Decision disagreements | — | 13 |
70
+
71
+ The released candidate gained seven decisions and lost six relative to the reference. This is fidelity evidence, not a fresh public-benchmark run. The candidate did not meet the experiment's original 99.5% agreement target and was released only after that threshold was explicitly waived.
benchmarks/summary.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "format_version": 1,
3
- "cases_sha256": "046a38c24747fd724003aca7e5f1d1b6e148d599092222361c0717ecff84ecad",
4
- "cases": 2277,
5
- "concurrency": 8,
6
- "errors": {
7
- "nyx-int4fp8": 0,
8
- "jev-1.13": 0
9
- },
10
- "dataset_macro_accuracy": {
11
- "nyx-int4fp8": 0.8145402125952668,
12
- "jev-1.13": 0.8209762334536702
13
- },
14
- "pooled_accuracy": {
15
- "nyx-int4fp8": 0.8296003513394817,
16
- "jev-1.13": 0.8361879666227492
17
- },
18
- "paired_mcnemar_two_sided_p": {
19
- "nyx-int4fp8_vs_jev-1.13": 0.23153716588439505
20
- },
21
- "limitations": [
22
- "Public benchmark contamination is unknown for both models.",
23
- "Fixed samples are not complete official-suite scores except TREC test and RTE validation.",
24
- "Jev timing used unmatched remote infrastructure."
25
- ]
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build_manifest.py CHANGED
@@ -32,10 +32,9 @@ for path in sorted(ROOT.rglob("*")):
32
  payload = {
33
  "format_version": 1,
34
  "repository": "devanshbatham/nyx",
35
- "artifact": "nyx-35b-a3b-int4fp8",
36
  "files": files,
37
  "total_bytes": sum(item["bytes"] for item in files.values()),
38
  }
39
  (ROOT / "release-manifest.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
40
  print(json.dumps({"files": len(files), "total_bytes": payload["total_bytes"]}))
41
-
 
32
  payload = {
33
  "format_version": 1,
34
  "repository": "devanshbatham/nyx",
35
+ "artifact": "nyx-4b-compact-iq4xs-mixed",
36
  "files": files,
37
  "total_bytes": sum(item["bytes"] for item in files.values()),
38
  }
39
  (ROOT / "release-manifest.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
40
  print(json.dumps({"files": len(files), "total_bytes": payload["total_bytes"]}))
 
chat_template.jinja DELETED
@@ -1,154 +0,0 @@
1
- {%- set image_count = namespace(value=0) %}
2
- {%- set video_count = namespace(value=0) %}
3
- {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
- {%- if content is string %}
5
- {{- content }}
6
- {%- elif content is iterable and content is not mapping %}
7
- {%- for item in content %}
8
- {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
- {%- if is_system_content %}
10
- {{- raise_exception('System message cannot contain images.') }}
11
- {%- endif %}
12
- {%- if do_vision_count %}
13
- {%- set image_count.value = image_count.value + 1 %}
14
- {%- endif %}
15
- {%- if add_vision_id %}
16
- {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
- {%- endif %}
18
- {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
- {%- elif 'video' in item or item.type == 'video' %}
20
- {%- if is_system_content %}
21
- {{- raise_exception('System message cannot contain videos.') }}
22
- {%- endif %}
23
- {%- if do_vision_count %}
24
- {%- set video_count.value = video_count.value + 1 %}
25
- {%- endif %}
26
- {%- if add_vision_id %}
27
- {{- 'Video ' ~ video_count.value ~ ': ' }}
28
- {%- endif %}
29
- {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
- {%- elif 'text' in item %}
31
- {{- item.text }}
32
- {%- else %}
33
- {{- raise_exception('Unexpected item type in content.') }}
34
- {%- endif %}
35
- {%- endfor %}
36
- {%- elif content is none or content is undefined %}
37
- {{- '' }}
38
- {%- else %}
39
- {{- raise_exception('Unexpected content type.') }}
40
- {%- endif %}
41
- {%- endmacro %}
42
- {%- if not messages %}
43
- {{- raise_exception('No messages provided.') }}
44
- {%- endif %}
45
- {%- if tools and tools is iterable and tools is not mapping %}
46
- {{- '<|im_start|>system\n' }}
47
- {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
- {%- for tool in tools %}
49
- {{- "\n" }}
50
- {{- tool | tojson }}
51
- {%- endfor %}
52
- {{- "\n</tools>" }}
53
- {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
- {%- if messages[0].role == 'system' %}
55
- {%- set content = render_content(messages[0].content, false, true)|trim %}
56
- {%- if content %}
57
- {{- '\n\n' + content }}
58
- {%- endif %}
59
- {%- endif %}
60
- {{- '<|im_end|>\n' }}
61
- {%- else %}
62
- {%- if messages[0].role == 'system' %}
63
- {%- set content = render_content(messages[0].content, false, true)|trim %}
64
- {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
- {%- endif %}
66
- {%- endif %}
67
- {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
- {%- for message in messages[::-1] %}
69
- {%- set index = (messages|length - 1) - loop.index0 %}
70
- {%- if ns.multi_step_tool and message.role == "user" %}
71
- {%- set content = render_content(message.content, false)|trim %}
72
- {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
- {%- set ns.multi_step_tool = false %}
74
- {%- set ns.last_query_index = index %}
75
- {%- endif %}
76
- {%- endif %}
77
- {%- endfor %}
78
- {%- if ns.multi_step_tool %}
79
- {{- raise_exception('No user query found in messages.') }}
80
- {%- endif %}
81
- {%- for message in messages %}
82
- {%- set content = render_content(message.content, true)|trim %}
83
- {%- if message.role == "system" %}
84
- {%- if not loop.first %}
85
- {{- raise_exception('System message must be at the beginning.') }}
86
- {%- endif %}
87
- {%- elif message.role == "user" %}
88
- {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
- {%- elif message.role == "assistant" %}
90
- {%- set reasoning_content = '' %}
91
- {%- if message.reasoning_content is string %}
92
- {%- set reasoning_content = message.reasoning_content %}
93
- {%- else %}
94
- {%- if '</think>' in content %}
95
- {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
- {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
- {%- endif %}
98
- {%- endif %}
99
- {%- set reasoning_content = reasoning_content|trim %}
100
- {%- if loop.index0 > ns.last_query_index %}
101
- {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
- {%- else %}
103
- {{- '<|im_start|>' + message.role + '\n' + content }}
104
- {%- endif %}
105
- {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
- {%- for tool_call in message.tool_calls %}
107
- {%- if tool_call.function is defined %}
108
- {%- set tool_call = tool_call.function %}
109
- {%- endif %}
110
- {%- if loop.first %}
111
- {%- if content|trim %}
112
- {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
- {%- else %}
114
- {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
- {%- endif %}
116
- {%- else %}
117
- {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
- {%- endif %}
119
- {%- if tool_call.arguments is defined %}
120
- {%- for args_name, args_value in tool_call.arguments|items %}
121
- {{- '<parameter=' + args_name + '>\n' }}
122
- {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
- {{- args_value }}
124
- {{- '\n</parameter>\n' }}
125
- {%- endfor %}
126
- {%- endif %}
127
- {{- '</function>\n</tool_call>' }}
128
- {%- endfor %}
129
- {%- endif %}
130
- {{- '<|im_end|>\n' }}
131
- {%- elif message.role == "tool" %}
132
- {%- if loop.previtem and loop.previtem.role != "tool" %}
133
- {{- '<|im_start|>user' }}
134
- {%- endif %}
135
- {{- '\n<tool_response>\n' }}
136
- {{- content }}
137
- {{- '\n</tool_response>' }}
138
- {%- if not loop.last and loop.nextitem.role != "tool" %}
139
- {{- '<|im_end|>\n' }}
140
- {%- elif loop.last %}
141
- {{- '<|im_end|>\n' }}
142
- {%- endif %}
143
- {%- else %}
144
- {{- raise_exception('Unexpected message role.') }}
145
- {%- endif %}
146
- {%- endfor %}
147
- {%- if add_generation_prompt %}
148
- {{- '<|im_start|>assistant\n' }}
149
- {%- if enable_thinking is defined and enable_thinking is false %}
150
- {{- '<think>\n\n</think>\n\n' }}
151
- {%- else %}
152
- {{- '<think>\n' }}
153
- {%- endif %}
154
- {%- endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config.json DELETED
@@ -1,96 +0,0 @@
1
- {
2
- "architectures": [
3
- "Qwen3_5MoeForCausalLM"
4
- ],
5
- "attention_bias": false,
6
- "attention_dropout": 0.0,
7
- "attn_output_gate": true,
8
- "bos_token_id": null,
9
- "dtype": "bfloat16",
10
- "eos_token_id": 248044,
11
- "full_attention_interval": 4,
12
- "head_dim": 256,
13
- "hidden_act": "silu",
14
- "hidden_size": 2048,
15
- "initializer_range": 0.02,
16
- "layer_types": [
17
- "linear_attention",
18
- "linear_attention",
19
- "linear_attention",
20
- "full_attention",
21
- "linear_attention",
22
- "linear_attention",
23
- "linear_attention",
24
- "full_attention",
25
- "linear_attention",
26
- "linear_attention",
27
- "linear_attention",
28
- "full_attention",
29
- "linear_attention",
30
- "linear_attention",
31
- "linear_attention",
32
- "full_attention",
33
- "linear_attention",
34
- "linear_attention",
35
- "linear_attention",
36
- "full_attention",
37
- "linear_attention",
38
- "linear_attention",
39
- "linear_attention",
40
- "full_attention",
41
- "linear_attention",
42
- "linear_attention",
43
- "linear_attention",
44
- "full_attention",
45
- "linear_attention",
46
- "linear_attention",
47
- "linear_attention",
48
- "full_attention",
49
- "linear_attention",
50
- "linear_attention",
51
- "linear_attention",
52
- "full_attention",
53
- "linear_attention",
54
- "linear_attention",
55
- "linear_attention",
56
- "full_attention"
57
- ],
58
- "linear_conv_kernel_dim": 4,
59
- "linear_key_head_dim": 128,
60
- "linear_num_key_heads": 16,
61
- "linear_num_value_heads": 32,
62
- "linear_value_head_dim": 128,
63
- "mamba_ssm_dtype": "float32",
64
- "max_position_embeddings": 262144,
65
- "mlp_only_layers": [],
66
- "model_type": "qwen3_5_moe_text",
67
- "moe_intermediate_size": 512,
68
- "mtp_num_hidden_layers": 1,
69
- "mtp_use_dedicated_embeddings": false,
70
- "num_attention_heads": 16,
71
- "num_experts": 256,
72
- "num_experts_per_tok": 8,
73
- "num_hidden_layers": 40,
74
- "num_key_value_heads": 2,
75
- "output_router_logits": false,
76
- "pad_token_id": null,
77
- "partial_rotary_factor": 0.25,
78
- "rms_norm_eps": 1e-06,
79
- "rope_parameters": {
80
- "mrope_interleaved": true,
81
- "mrope_section": [
82
- 11,
83
- 11,
84
- 10
85
- ],
86
- "partial_rotary_factor": 0.25,
87
- "rope_theta": 10000000,
88
- "rope_type": "default"
89
- },
90
- "router_aux_loss_coef": 0.001,
91
- "shared_expert_intermediate_size": 512,
92
- "tie_word_embeddings": false,
93
- "transformers_version": "5.17.0",
94
- "use_cache": true,
95
- "vocab_size": 248320
96
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
generation_config.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "bos_token_id": 248044,
3
- "do_sample": true,
4
- "eos_token_id": [
5
- 248046,
6
- 248044
7
- ],
8
- "pad_token_id": 248044,
9
- "temperature": 1.0,
10
- "top_k": 20,
11
- "top_p": 0.95,
12
- "transformers_version": "5.17.0"
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/nyx-vs-jev-benchmarks.png → nyx.gguf RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9ce0a836719964925223bc94e29a299aebfe01b81c85fc85866e05dac101ee47
3
- size 1362162
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39d64fb19863cfc067e2411fcb2a0444113dda13798b15de5617736d91914359
3
+ size 3288582784
provenance.json CHANGED
@@ -1,28 +1,33 @@
1
  {
2
  "format_version": 1,
3
  "model": "devanshbatham/nyx",
4
- "base_model": "Qwen/Qwen3.5-35B-A3B",
5
- "base_revision": "59d61f3ce65a6d9863b86d2e96597125219dc754",
6
  "license": "Apache-2.0",
7
- "quantization": {
8
- "routed_expert_storage": "per-channel INT4",
9
- "expert_compute": "FP8",
10
- "dense_layers": "dynamic FP8",
11
- "activations": "BF16",
12
- "kv_cache": "BF16",
13
- "ssm_state": "FP32"
14
  },
15
- "tested_runtime": {
16
- "hardware": "AMD MI325X gfx942",
17
- "sglang_image": "lmsysorg/sglang@sha256:a0ffcdd013af6d79c9b7c4348c42b14a79a48f6c43dd3ca0235ec3802da45f75",
18
- "tensor_parallel_size": 1
 
 
 
 
 
 
 
 
19
  },
20
- "accepted_evidence": {
21
- "model_memory_gb": 19.43,
22
- "package_bytes": 19657745446,
23
- "h1_correct": 1713,
24
- "h1_total": 2000,
25
- "h1_result_sha256": "b10697d6a03fb19c56e7193eb85b932c92aee2a27d4b6a088f684051dff1c01e",
26
- "source_checksum_manifest_sha256": "829aa54dd4f440c10d85c6253a41891a69ba1717ace2d193cbfab3c1a7546c6e"
27
  }
28
  }
 
1
  {
2
  "format_version": 1,
3
  "model": "devanshbatham/nyx",
4
+ "base_model": "Qwen/Qwen3.5-4B",
5
+ "base_revision": "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a",
6
  "license": "Apache-2.0",
7
+ "artifact": {
8
+ "path": "nyx.gguf",
9
+ "bytes": 3288582784,
10
+ "sha256": "39d64fb19863cfc067e2411fcb2a0444113dda13798b15de5617736d91914359",
11
+ "format": "GGUF",
12
+ "quantization": "importance-matrix IQ4_XS with selected Q5_K/Q6_K tensors"
 
13
  },
14
+ "reference": {
15
+ "bytes": 8424393056,
16
+ "sha256": "b225f62bb79e2f0bc0bcd9fa00dba4579bade89e6b9581537da7fa2482a6b500"
17
+ },
18
+ "fidelity_evaluation": {
19
+ "cases": 1024,
20
+ "reference_correct": 873,
21
+ "artifact_correct": 874,
22
+ "decision_agreement": 0.9873046875,
23
+ "decision_disagreements": 13,
24
+ "gains": 7,
25
+ "losses": 6
26
  },
27
+ "tested_runtime": {
28
+ "llama_cpp_revision": "e6cef8152f6e8351a870d8e1a98627139c9c379a",
29
+ "cpu_load_smoke_test": true,
30
+ "full_public_benchmark_on_release_artifact": false,
31
+ "nvidia_qualification": false
 
 
32
  }
33
  }
release-manifest.json CHANGED
@@ -1,120 +1,44 @@
1
  {
2
- "artifact": "nyx-35b-a3b-int4fp8",
3
  "files": {
4
  ".gitattributes": {
5
- "bytes": 490,
6
- "sha256": "d91fb28be6eecb4324941d243d6231ea343cffba5f986e66e3223791ddc0889c"
7
  },
8
  "LICENSE": {
9
  "bytes": 11544,
10
  "sha256": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a"
11
  },
12
  "NOTICE": {
13
- "bytes": 449,
14
- "sha256": "94d30a32935469a150faba2820dfa13bd34c67ae5d2c8adb390778666a4a8443"
15
  },
16
  "README.md": {
17
- "bytes": 6627,
18
- "sha256": "c37620861bbcd51d63dcc298865a7ffe8f7257617c692a1ec55280a1486099e2"
19
  },
20
- "assets/nyx-vs-jev-benchmarks.png": {
21
- "bytes": 1362162,
22
- "sha256": "9ce0a836719964925223bc94e29a299aebfe01b81c85fc85866e05dac101ee47"
23
  },
24
- "benchmarks/prequant-h1.md": {
25
- "bytes": 1708,
26
- "sha256": "fab5d5d4db40d14a0ae6d877bbf1a5f2fa72f586d5b282d37ee2456c293c8fad"
27
  },
28
  "benchmarks/random-six-report.md": {
29
- "bytes": 2183,
30
- "sha256": "19dff7a297bd379897b393cb3601b91fe2ed9cfabfa6e5c8eaf38601d4a12ff1"
31
  },
32
- "benchmarks/summary.json": {
33
- "bytes": 751,
34
- "sha256": "90d3be0514f431979f75fba1d0ee43f47d5b7b5ce9ec3d8132a9929ca1492d33"
35
- },
36
- "chat_template.jinja": {
37
- "bytes": 7756,
38
- "sha256": "a4aee8afcf2e0711942cf848899be66016f8d14a889ff9ede07bca099c28f715"
39
- },
40
- "config.json": {
41
- "bytes": 2336,
42
- "sha256": "27e3575d049c5a0187b783aecff914135be455439134368aba8cdcc6d678ebb7"
43
- },
44
- "generation_config.json": {
45
- "bytes": 214,
46
- "sha256": "df6f86c3fdce573ecdb55cec35f502ddda70e70abe78288a78164aef120232e7"
47
  },
48
  "provenance.json": {
49
- "bytes": 944,
50
- "sha256": "33be2c518a6a21599a09b971f6cc3511fe0da61df6522fad0e7b7084170701ce"
51
- },
52
- "runtime/entrypoint.sh": {
53
- "bytes": 276,
54
- "sha256": "742824765cb2f5361a476674e16a5d347abb9ae905e2798a2183121ea1ee4b07"
55
- },
56
- "runtime/label-token-ids.json": {
57
- "bytes": 761,
58
- "sha256": "8cc5610b02d5ba14358ebef59cd16f567767632fb6ea03de1565b18f1c003a40"
59
- },
60
- "runtime/patches/qwen35-text-quark-generated-scales.patch": {
61
- "bytes": 1680,
62
- "sha256": "920a8cabd8d2f6565e4f0c3296b1b0e154e48c227e4fd12f019033792cbe567f"
63
- },
64
- "runtime/patches/sglang-batch-result-processor.py": {
65
- "bytes": 57803,
66
- "sha256": "f7f58ef7f377e3df111c87d1803b6e0008bdf76726d80cbf1012275839e77da7"
67
- },
68
- "runtime/patches/sglang-logits-processor.py": {
69
- "bytes": 54217,
70
- "sha256": "aed498368a4402a84a4ef71a5962f1ff78cad265d4becbe5b3b677f1cb8ad9e2"
71
- },
72
- "runtime/patches/sglang-qwen35-text.py": {
73
- "bytes": 12308,
74
- "sha256": "52cdbda541ff612353adf5d5b04a6668954040b48cfa5209f59abbd662a023e1"
75
- },
76
- "runtime/patches/sglang-qwen35.py": {
77
- "bytes": 106934,
78
- "sha256": "f6975a9b3303002c25418b0d5b32c6bd40502ba6568ab7d5c847c6983705c01e"
79
- },
80
- "serve.sh": {
81
- "bytes": 2065,
82
- "sha256": "5f308855054cd6dd86fde051cc9771e02e1e160dc0ddbad1f5eed8e7b81a4b65"
83
- },
84
- "tokenizer.json": {
85
- "bytes": 19989325,
86
- "sha256": "06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523"
87
- },
88
- "tokenizer_config.json": {
89
- "bytes": 1123,
90
- "sha256": "bee8eba30f0eb4af73c0fe2cd06d0f89b657d7819941c438157ec42f7c80ea87"
91
- },
92
- "weights/TP-1-sig-e793a9432f30cd4e/READY": {
93
- "bytes": 70,
94
- "sha256": "b20cd5cb19a0b44c0189bb79b85ee0300f0453a31bfef7b39ac825b409cbb578"
95
- },
96
- "weights/TP-1-sig-e793a9432f30cd4e/checksum.json": {
97
- "bytes": 359616,
98
- "sha256": "829aa54dd4f440c10d85c6253a41891a69ba1717ace2d193cbfab3c1a7546c6e"
99
- },
100
- "weights/TP-1-sig-e793a9432f30cd4e/model-00000-common.safetensor": {
101
- "bytes": 5268047400,
102
- "sha256": "16e3906e9a161a6337a4a0f6d06e352d09360b37a9d4c4ea341ea978192ee289"
103
- },
104
- "weights/TP-1-sig-e793a9432f30cd4e/model-00001-common.safetensor": {
105
- "bytes": 5120198752,
106
- "sha256": "7ad7affbcc8912528ff2859568b51ee9427cc17119312ea8750cf118544ea5d3"
107
- },
108
- "weights/TP-1-sig-e793a9432f30cd4e/model-00002-common.safetensor": {
109
- "bytes": 5254941984,
110
- "sha256": "fa4a08a1e0e0de30543d343a629c6710e59575cd6f98f5beae789f34a59b79b7"
111
- },
112
- "weights/TP-1-sig-e793a9432f30cd4e/model-00003-common.safetensor": {
113
- "bytes": 4014197624,
114
- "sha256": "dfb557a70a97b91208837dac145569c844971062970f41466738e61e77d53eb3"
115
  }
116
  },
117
  "format_version": 1,
118
  "repository": "devanshbatham/nyx",
119
- "total_bytes": 19679369102
120
  }
 
1
  {
2
+ "artifact": "nyx-4b-compact-iq4xs-mixed",
3
  "files": {
4
  ".gitattributes": {
5
+ "bytes": 533,
6
+ "sha256": "ce45845c47d05a5fe3881ff4fcf541f1cbdd3d8d9fdb33bb395e6bdbbf240074"
7
  },
8
  "LICENSE": {
9
  "bytes": 11544,
10
  "sha256": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a"
11
  },
12
  "NOTICE": {
13
+ "bytes": 601,
14
+ "sha256": "a7c85805e2a02824e60d752e1d543649918c081b45b150615a926ea2f79f2e91"
15
  },
16
  "README.md": {
17
+ "bytes": 4518,
18
+ "sha256": "dcb056c79af04ccbc3662af1bee3a480885cac0451d3322f2399bfca6cdead35"
19
  },
20
+ "assets/IOSEVKA-OFL.md": {
21
+ "bytes": 4674,
22
+ "sha256": "4fbfa00e9fb9e9a774ab4c099bc6e8b2ac1a46f6492a2c260f958b004d172da6"
23
  },
24
+ "assets/nyx-benchmarks.svg": {
25
+ "bytes": 121931,
26
+ "sha256": "d0096f7d804d89f1cebec9e1e966cb05c855382b9842f5741cb162e3f7b884d5"
27
  },
28
  "benchmarks/random-six-report.md": {
29
+ "bytes": 4372,
30
+ "sha256": "68f53d0f47f25a3cf92fe73be76537164270b18b10d374e2247456f7daf3036f"
31
  },
32
+ "nyx.gguf": {
33
+ "bytes": 3288582784,
34
+ "sha256": "39d64fb19863cfc067e2411fcb2a0444113dda13798b15de5617736d91914359"
 
 
 
 
 
 
 
 
 
 
 
 
35
  },
36
  "provenance.json": {
37
+ "bytes": 1000,
38
+ "sha256": "4e3775066d90006cf3376a3d64f72751d01eeb8de24a35e3badc3ac08866cef2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  },
41
  "format_version": 1,
42
  "repository": "devanshbatham/nyx",
43
+ "total_bytes": 3288731957
44
  }
runtime/entrypoint.sh DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- target=/sgl-workspace/sglang/python/sglang/srt/models/qwen3_5_text.py
5
- cp /model/runtime/patches/sglang-qwen35-text.py "$target"
6
- patch --batch "$target" < /model/runtime/patches/qwen35-text-quark-generated-scales.patch
7
- exec python3 "$@"
8
-
 
 
 
 
 
 
 
 
 
runtime/label-token-ids.json DELETED
@@ -1 +0,0 @@
1
- [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, 5840, 1803, 1646, 1745, 13276, 8018, 1825, 28946, 15015, 29595, 11568, 939, 1354, 1058, 18183, 2456, 88898, 905, 1846, 802, 33869, 7839, 14006, 2860, 2926, 22828, 6844, 9798, 4738, 9265, 11261, 19278, 36513, 93801, 8335, 14544, 85266, 9110, 28000, 15137, 4525, 25261, 12717, 7116, 17078, 14497, 57339, 74909, 52072, 19305, 4887, 12607, 3580, 6281, 2036, 9362, 8533, 2080, 10911, 2925, 3040, 9690, 27731, 8023, 6901, 8702, 6211, 1123, 16307, 18990, 64045, 63037, 33380, 6151, 3392, 5449, 3967, 1113, 5095, 51923, 49600, 17099, 51483, 17756, 16037, 8135, 30237, 5683, 9992, 7444, 5751, 10284, 20887, 59884, 52396, 16103, 67547, 18535, 8006, 7263, 1425, 6878]
 
 
runtime/patches/qwen35-text-quark-generated-scales.patch DELETED
@@ -1,32 +0,0 @@
1
- --- qwen3_5_text.py
2
- +++ qwen3_5_text.py
3
- @@ -242,17 +242,24 @@
4
- loaded_params.add("lm_head.weight")
5
-
6
- missing = set(params_dict) - loaded_params
7
- - if (self.quant_config is not None
8
- - and self.quant_config.get_name() == "fp8"
9
- + quant_name = (
10
- + self.quant_config.get_name() if self.quant_config is not None else None
11
- + )
12
- + if (quant_name in {"fp8", "quark_int4fp8_moe"}
13
- and not self.quant_config.is_checkpoint_fp8_serialized):
14
- - # Online BF16->FP8 creates these scales itself. They are deliberately
15
- - # absent from the source BF16 checkpoint; all model weights still
16
- - # have to load. Do not apply this exception to serialized FP8.
17
- + # Online quantizers create their scales from source weights. They are
18
- + # deliberately absent from the BF16 checkpoint; all source model
19
- + # weights still have to load. Never apply this to serialized FP8.
20
- generated_suffixes = (
21
- ".mlp.experts.w13_weight_scale", ".mlp.experts.w2_weight_scale",
22
- ".mlp.experts.w13_weight_scale1", ".mlp.experts.w2_weight_scale1",
23
- ".attn.k_scale", ".attn.v_scale",
24
- )
25
- + if quant_name == "quark_int4fp8_moe":
26
- + generated_suffixes += (
27
- + ".mlp.experts.w13_fp8_scale", ".mlp.experts.w2_fp8_scale",
28
- + ".mlp.experts.w13_int4_scale", ".mlp.experts.w2_int4_scale",
29
- + )
30
- generated = {name for name in missing if name.endswith(generated_suffixes)}
31
- missing -= generated
32
- if missing:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runtime/patches/sglang-batch-result-processor.py DELETED
@@ -1,1378 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from dataclasses import dataclass
5
- from typing import (
6
- TYPE_CHECKING,
7
- Callable,
8
- List,
9
- Optional,
10
- Tuple,
11
- Union,
12
- )
13
-
14
- import torch
15
-
16
- from sglang.srt.disaggregation.utils import DisaggregationMode
17
- from sglang.srt.environ import envs
18
- from sglang.srt.layers.logits_processor import LogitsProcessorOutput
19
- from sglang.srt.managers.schedule_batch import (
20
- FINISH_ABORT,
21
- FINISH_MATCHED_TOKEN,
22
- Req,
23
- ScheduleBatch,
24
- mamba_lazy_spec_in_window,
25
- )
26
- from sglang.srt.mem_cache.common import (
27
- maybe_cache_unfinished_req,
28
- release_kv_cache,
29
- )
30
- from sglang.srt.model_executor.forward_batch_info import (
31
- CaptureHiddenMode,
32
- get_required_capture_hidden_mode,
33
- get_server_return_hidden_states_mode,
34
- )
35
- from sglang.srt.runtime_context import (
36
- get_disagg,
37
- get_memory,
38
- get_observability,
39
- mamba_extra_buffer_lazy_enabled,
40
- mamba_track_grid,
41
- max_speculative_num_draft_tokens,
42
- )
43
- from sglang.srt.sampling.sampling_observer import CommittedTokens
44
- from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
45
- from sglang.srt.state_capturer.indexer_topk import get_global_indexer_capturer
46
- from sglang.srt.state_capturer.routed_experts import get_global_experts_capturer
47
-
48
- if TYPE_CHECKING:
49
- from sglang.srt.beam_search.coordinator import BeamCoordinator
50
- from sglang.srt.configs.model_config import ModelConfig
51
- from sglang.srt.disaggregation.decode_kvcache_offload_manager import (
52
- DecodeKVCacheOffloadManager,
53
- )
54
- from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
55
- from sglang.srt.managers.scheduler_components.logprob_result_processor import (
56
- SchedulerLogprobResultProcessor,
57
- )
58
- from sglang.srt.managers.scheduler_components.metrics_reporter import (
59
- SchedulerMetricsReporter,
60
- )
61
- from sglang.srt.managers.scheduler_components.output_streamer import (
62
- SchedulerOutputStreamer,
63
- )
64
- from sglang.srt.managers.tp_worker import BaseTpWorker
65
- from sglang.srt.managers.utils import (
66
- EmbeddingBatchResult,
67
- GenerationBatchResult,
68
- )
69
- from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
70
- from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
71
- from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
72
- from sglang.srt.observability.metrics_collector import SchedulerMetricsCollector
73
- from sglang.srt.sampling.sampling_observer import HostAuxiliaryOutput
74
-
75
- logger = logging.getLogger(__name__)
76
-
77
-
78
- @dataclass(kw_only=True, slots=True, frozen=True)
79
- class SchedulerBatchResultProcessor:
80
- is_generation: bool
81
- disaggregation_mode: DisaggregationMode
82
- enable_overlap: bool
83
- enable_overlap_mlx: bool
84
- model_config: ModelConfig
85
- token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator
86
- tree_cache: BasePrefixCache
87
- hisparse_coordinator: Optional[HiSparseCoordinator]
88
- req_to_token_pool: ReqToTokenPool
89
- decode_offload_manager: Optional[DecodeKVCacheOffloadManager]
90
- metrics_collector: SchedulerMetricsCollector
91
- metrics_reporter: SchedulerMetricsReporter
92
- draft_worker: BaseTpWorker
93
- model_worker: BaseTpWorker
94
- logprob_result_processor: SchedulerLogprobResultProcessor
95
- output_streamer: SchedulerOutputStreamer
96
- beam_coordinator: BeamCoordinator
97
- abort_request: Callable
98
-
99
- def process_batch_result_prebuilt(self, batch: ScheduleBatch):
100
- assert self.disaggregation_mode == DisaggregationMode.DECODE
101
- use_free_group = get_disagg().disaggregation_decode_enable_radix_cache
102
- if use_free_group:
103
- self.token_to_kv_pool_allocator.free_group_begin()
104
- for req in batch.reqs:
105
- req.time_stats.set_decode_prebuilt_finish_time()
106
- req.update_finish_state()
107
- if req.finished():
108
- req.time_stats.set_quick_finish_time()
109
- if get_memory().enable_hisparse:
110
- self.hisparse_coordinator.request_finished(req)
111
- release_kv_cache(req, self.tree_cache)
112
-
113
- # Note: Logprobs should be handled on the prefill engine.
114
- self.output_streamer.stream_output(batch.reqs, batch.return_logprob)
115
- if use_free_group:
116
- self.token_to_kv_pool_allocator.free_group_end()
117
-
118
- def _maybe_collect_routed_experts(self, req: Req):
119
- """Collect routed experts for a finished request.
120
-
121
- Returns immediately if `return_routed_experts` was not set on the
122
- request, so non-opted-in reqs don't pay the host-gather cost.
123
-
124
- Honors the caller's absolute start so the response covers
125
- `[start_len, seqlen - 1)`. The default start_len is 0, which returns
126
- the full sequence.
127
-
128
- Logs a soft warning if the resulting tensor's row count differs from
129
- the expected `seqlen - 1 - start_len`, to catch silent regressions.
130
- """
131
- if not req.return_routed_experts:
132
- return
133
- capturer = get_global_experts_capturer()
134
- if capturer is None:
135
- return
136
- start_len = req.routed_experts_start_len
137
- seqlen = len(req.origin_input_ids) + len(req.output_ids_through_stop)
138
- req.routed_experts = capturer.get_topk(
139
- req_pool_idx=req.kv.req_pool_idx,
140
- seqlen=seqlen,
141
- req_to_token_pool=self.req_to_token_pool,
142
- start_len=start_len,
143
- )
144
-
145
- expected_rows = max(0, seqlen - 1 - start_len)
146
- if (
147
- req.routed_experts is not None
148
- and req.routed_experts.shape[0] != expected_rows
149
- ):
150
- logger.warning(
151
- "routed_experts row-count mismatch for req %s: got %d, expected %d "
152
- "(seqlen=%d, raw_seqlen=%d, cached_tokens=%d, start_len=%s). "
153
- "This indicates a silent bug.",
154
- req.rid,
155
- req.routed_experts.shape[0],
156
- expected_rows,
157
- seqlen,
158
- req.seqlen,
159
- req.cached_tokens,
160
- req.routed_experts_start_len,
161
- )
162
-
163
- def _maybe_collect_indexer_topk(self, req: Req):
164
- capturer = get_global_indexer_capturer()
165
- if capturer is None:
166
- return
167
- seqlen = len(req.origin_input_ids) + len(req.output_ids_through_stop)
168
- req.indexer_topk = capturer.get_topk(
169
- req_pool_idx=req.kv.req_pool_idx,
170
- seqlen=seqlen,
171
- req_to_token_pool=self.req_to_token_pool,
172
- )
173
-
174
- def _maybe_collect_customized_info(
175
- self,
176
- i: int,
177
- req: Req,
178
- logits_output: LogitsProcessorOutput,
179
- ):
180
- if logits_output is not None and logits_output.customized_info is not None:
181
- if req.customized_info is None:
182
- req.customized_info = {}
183
- for k, v in logits_output.customized_info.items():
184
- if k not in req.customized_info:
185
- req.customized_info[k] = []
186
- # Copy the element so it doesn't retain the entire batch
187
- # tensor/array via a view reference.
188
- elem = v[i]
189
- if isinstance(elem, torch.Tensor):
190
- elem = elem.clone()
191
- elif hasattr(elem, "copy") and callable(elem.copy):
192
- elem = elem.copy()
193
- req.customized_info[k].append(elem)
194
-
195
- @staticmethod
196
- def _visible_output_len(req: Req) -> int:
197
- return req.finished_len if req.finished_len is not None else len(req.output_ids)
198
-
199
- @classmethod
200
- def snapshot_auxiliary_output_starts(
201
- cls,
202
- batch: ScheduleBatch,
203
- result: GenerationBatchResult,
204
- ) -> Optional[List[int]]:
205
- if result.auxiliary_host_output is None:
206
- return None
207
- return [cls._visible_output_len(req) for req in batch.reqs]
208
-
209
- @classmethod
210
- def _build_auxiliary_commits(
211
- cls,
212
- batch: ScheduleBatch,
213
- output_starts: List[int],
214
- ) -> List[Optional[CommittedTokens]]:
215
- commits: List[Optional[CommittedTokens]] = []
216
- for req, output_start in zip(batch.reqs, output_starts, strict=True):
217
- output_end = cls._visible_output_len(req)
218
- if output_end < output_start:
219
- raise RuntimeError("committed output length moved backwards")
220
- if output_end == output_start:
221
- commits.append(None)
222
- continue
223
- commits.append(
224
- CommittedTokens(
225
- output_index=output_start,
226
- token_ids=tuple(req.output_ids[output_start:output_end]),
227
- )
228
- )
229
- return commits
230
-
231
- @classmethod
232
- def consume_auxiliary_output(
233
- cls,
234
- batch: ScheduleBatch,
235
- output: HostAuxiliaryOutput,
236
- output_starts: List[int],
237
- ) -> None:
238
- output.consume(batch, cls._build_auxiliary_commits(batch, output_starts))
239
-
240
- def process_batch_result_prefill(
241
- self,
242
- batch: ScheduleBatch,
243
- result: Union[GenerationBatchResult, EmbeddingBatchResult],
244
- ):
245
- skip_stream_req = None
246
- self.token_to_kv_pool_allocator.free_group_begin()
247
-
248
- if self.is_generation:
249
- if result.copy_done is not None:
250
- result.copy_done.synchronize()
251
- auxiliary_output_starts = self.snapshot_auxiliary_output_starts(
252
- batch, result
253
- )
254
- auxiliary_output = result.auxiliary_host_output
255
- if result.routed_experts_output is not None:
256
- result.routed_experts_output.finalize()
257
- result.routed_experts_output = None
258
- if result.indexer_topk_output is not None:
259
- result.indexer_topk_output.finalize()
260
- result.indexer_topk_output = None
261
-
262
- (
263
- logits_output,
264
- next_token_ids,
265
- extend_input_len_per_req,
266
- extend_logprob_start_len_per_req,
267
- ) = (
268
- result.logits_output,
269
- result.next_token_ids,
270
- result.extend_input_len_per_req,
271
- result.extend_logprob_start_len_per_req,
272
- )
273
-
274
- # Move next_token_ids and logprobs to cpu
275
- next_token_ids = next_token_ids.tolist()
276
- self.move_logprobs_to_cpu(batch=batch, logits_output=logits_output)
277
-
278
- self._validate_pp_skip_output_comm(batch, result)
279
-
280
- hidden_state_offset = 0
281
- prefill_hidden_capture_mode = self._get_prefill_hidden_capture_mode(
282
- batch,
283
- )
284
-
285
- # Check finish conditions
286
- logprob_pt = 0
287
-
288
- for i, (req, next_token_id) in enumerate(zip(batch.reqs, next_token_ids)):
289
- if (
290
- batch.return_hidden_states
291
- and logits_output.hidden_states is not None
292
- ):
293
- assert extend_input_len_per_req is not None
294
- hidden_state_offset = self._append_prefill_hidden_states(
295
- req=req,
296
- logits_output=logits_output,
297
- hidden_state_offset=hidden_state_offset,
298
- capture_hidden_mode=prefill_hidden_capture_mode,
299
- extend_input_len=extend_input_len_per_req[i],
300
- store=(
301
- not req.finished()
302
- and not req.is_retracted
303
- and req.inflight_middle_chunks <= 0
304
- ),
305
- )
306
-
307
- if (
308
- req.finished() and req.inflight_middle_chunks <= 0
309
- ) or req.is_retracted:
310
- # Decode req in a mixed batch, or a retracted req. Keep an
311
- # aborted middle chunk in the chunked branch long enough to
312
- # drain its accounting without streaming it.
313
- continue
314
-
315
- if req.inflight_middle_chunks <= 0:
316
- req.time_stats.set_prefill_finished_time()
317
-
318
- if req.beam_group is not None:
319
- # The relay point already replaced the sampled-token
320
- # append; the group owns all finish semantics.
321
- self.beam_coordinator.commit_prefill(
322
- req, up_to_tick=batch.forward_iter
323
- )
324
- else:
325
- # req output_ids are set here
326
- req.output_ids.append(next_token_id)
327
-
328
- self._maybe_update_reasoning_tokens(req, next_token_id)
329
-
330
- req.update_finish_state()
331
- # A mixed spec tail committed its pending bonus token; advance
332
- # so the next spec prepare_for_decode reserves from the right base.
333
- if (
334
- not req.finished()
335
- and batch.decoding_reqs
336
- and req in batch.decoding_reqs
337
- and not batch.spec_algorithm.is_none()
338
- ):
339
- req.kv.kv_committed_len += 1
340
- if req.finished():
341
- self._maybe_collect_routed_experts(req)
342
- self._maybe_collect_indexer_topk(req)
343
- release_kv_cache(req, self.tree_cache)
344
- req.time_stats.set_completion_time()
345
- elif not batch.decoding_reqs or req not in batch.decoding_reqs:
346
- maybe_cache_unfinished_req(req, self.tree_cache)
347
- if get_memory().enable_hisparse:
348
- self.hisparse_coordinator.admit_request_into_staging(req)
349
-
350
- self._maybe_collect_customized_info(i, req, logits_output)
351
-
352
- if batch.return_logprob:
353
- logprob_pt = self._apply_prefill_logprobs(
354
- req=req,
355
- i=i,
356
- logits_output=logits_output,
357
- extend_input_len_per_req=extend_input_len_per_req,
358
- extend_logprob_start_len_per_req=extend_logprob_start_len_per_req,
359
- next_token_ids=next_token_ids,
360
- logprob_pt=logprob_pt,
361
- )
362
-
363
- if req.return_sampling_mask:
364
- self.add_sampling_mask_return_values(i, req, logits_output)
365
-
366
- if req.grammar is not None:
367
- self._apply_prefill_grammar(
368
- req=req,
369
- next_token_id=next_token_id,
370
- already_advanced=result.grammar_advanced,
371
- )
372
-
373
- else:
374
- # being chunked reqs' prefill is not finished
375
- req.inflight_middle_chunks -= 1
376
- # There is only at most one request being currently chunked.
377
- # Because this request does not finish prefill,
378
- # we don't want to stream the request currently being chunked.
379
- skip_stream_req = req
380
-
381
- # Incrementally update input logprobs.
382
- if batch.return_logprob:
383
- logprob_pt = self._apply_chunked_prefill_logprobs(
384
- req=req,
385
- i=i,
386
- logits_output=logits_output,
387
- extend_input_len_per_req=extend_input_len_per_req,
388
- extend_logprob_start_len_per_req=extend_logprob_start_len_per_req,
389
- logprob_pt=logprob_pt,
390
- )
391
-
392
- req.time_stats.set_last_chunked_prefill_finish_time()
393
-
394
- if auxiliary_output is not None:
395
- self.consume_auxiliary_output(
396
- batch,
397
- auxiliary_output,
398
- auxiliary_output_starts,
399
- )
400
-
401
- else: # embedding or reward model
402
- if result.copy_done is not None:
403
- result.copy_done.synchronize()
404
-
405
- embeddings = self._convert_embeddings(result=result)
406
- phs = result.pooled_hidden_states
407
-
408
- if phs is not None:
409
- if isinstance(phs, list):
410
- phs = [t.cpu().detach() for t in phs]
411
- else:
412
- phs = phs.cpu().detach()
413
-
414
- # Check finish conditions
415
- for i, req in enumerate(batch.reqs):
416
- if req.is_retracted:
417
- continue
418
-
419
- req.embedding = embeddings[i]
420
- if req.return_pooled_hidden_states and phs is not None:
421
- req.pooled_hidden_state = phs[i]
422
- if req.inflight_middle_chunks <= 0:
423
- req.time_stats.set_prefill_finished_time()
424
- # Dummy output token for embedding models
425
- req.output_ids.append(0)
426
- req.update_finish_state()
427
-
428
- if req.finished():
429
- release_kv_cache(req, self.tree_cache)
430
- req.time_stats.set_completion_time()
431
- else:
432
- maybe_cache_unfinished_req(req, self.tree_cache)
433
- else:
434
- # being chunked reqs' prefill is not finished
435
- req.inflight_middle_chunks -= 1
436
- req.time_stats.set_last_chunked_prefill_finish_time()
437
-
438
- self.token_to_kv_pool_allocator.free_group_end()
439
- self.output_streamer.stream_output(
440
- batch.reqs, batch.return_logprob, skip_stream_req
441
- )
442
-
443
- can_run_cuda_graph = result.can_run_cuda_graph
444
- # None on decode->extend converted batches; they are decode work and
445
- # have no prefill stats to report.
446
- if batch.prefill_stats is not None:
447
- self.metrics_reporter.report_prefill_stats(
448
- batch=batch,
449
- prefill_stats=batch.prefill_stats,
450
- can_run_cuda_graph=can_run_cuda_graph,
451
- dp_cooperation_info=batch.dp_cooperation_info,
452
- )
453
-
454
- def _convert_embeddings(self, *, result: EmbeddingBatchResult) -> list:
455
- is_sparse = envs.SGLANG_EMBEDDINGS_SPARSE_HEAD.is_set()
456
-
457
- embeddings = result.embeddings
458
-
459
- if is_sparse:
460
- batch_ids, token_ids = embeddings.indices()
461
- values = embeddings.values()
462
-
463
- embeddings = [{} for _ in range(embeddings.size(0))]
464
- for i in range(batch_ids.shape[0]):
465
- embeddings[batch_ids[i].item()][token_ids[i].item()] = values[i].item()
466
- else:
467
- if isinstance(embeddings, torch.Tensor):
468
- embeddings = embeddings.tolist()
469
- else:
470
- embeddings = [tensor.tolist() for tensor in embeddings]
471
- return embeddings
472
-
473
- def move_logprobs_to_cpu(
474
- self,
475
- *,
476
- batch: ScheduleBatch,
477
- logits_output: LogitsProcessorOutput,
478
- ) -> None:
479
- if batch.return_logprob:
480
- if logits_output.next_token_logprobs is not None:
481
- logits_output.next_token_logprobs = (
482
- logits_output.next_token_logprobs.tolist()
483
- )
484
- if logits_output.input_token_logprobs is not None:
485
- logits_output.input_token_logprobs = tuple(
486
- logits_output.input_token_logprobs.tolist()
487
- )
488
- if logits_output.next_token_top_logprobs_val:
489
- logits_output.next_token_top_logprobs_val = [
490
- v.tolist() for v in logits_output.next_token_top_logprobs_val
491
- ]
492
- logits_output.next_token_top_logprobs_idx = [
493
- x.tolist() for x in logits_output.next_token_top_logprobs_idx
494
- ]
495
- if logits_output.next_token_token_ids_logprobs_val:
496
- logits_output.next_token_token_ids_logprobs_val = [
497
- v.tolist() if isinstance(v, torch.Tensor) else v
498
- for v in logits_output.next_token_token_ids_logprobs_val
499
- ]
500
-
501
- def _apply_prefill_logprobs(
502
- self,
503
- *,
504
- req: Req,
505
- i: int,
506
- logits_output: LogitsProcessorOutput,
507
- extend_input_len_per_req: Optional[List[int]],
508
- extend_logprob_start_len_per_req: Optional[List[int]],
509
- next_token_ids: List[int],
510
- logprob_pt: int,
511
- ) -> int:
512
- assert extend_logprob_start_len_per_req is not None
513
- assert extend_input_len_per_req is not None
514
- extend_logprob_start_len = extend_logprob_start_len_per_req[i]
515
- extend_input_len = extend_input_len_per_req[i]
516
-
517
- num_input_logprobs = self.logprob_result_processor.calculate_num_input_logprobs(
518
- req,
519
- extend_input_len,
520
- extend_logprob_start_len,
521
- )
522
-
523
- if req.return_logprob:
524
- self.logprob_result_processor.add_logprob_return_values(
525
- i,
526
- req,
527
- logprob_pt,
528
- next_token_ids,
529
- num_input_logprobs,
530
- logits_output,
531
- )
532
- logprob_pt += num_input_logprobs
533
- return logprob_pt
534
-
535
- @staticmethod
536
- def _validate_pp_skip_output_comm(
537
- batch: ScheduleBatch,
538
- result: Union[GenerationBatchResult, EmbeddingBatchResult],
539
- ):
540
- """Validate PP skip output comm correctness.
541
-
542
- - When skip=True: all reqs must be middle chunks (inflight_middle_chunks > 0)
543
- so placeholder zeros are never consumed via req.output_ids.append().
544
- - When skip=False: at least one req should consume next_token_ids
545
- (inflight_middle_chunks <= 0), otherwise warn.
546
- """
547
- if not envs.SGLANG_PP_SKIP_PURE_CHUNKED_OUTPUT_COMM.get():
548
- return
549
-
550
- if not getattr(result, "skipped_output_comm", False):
551
- if batch.forward_mode.is_extend() and not batch.forward_mode.is_prebuilt():
552
- has_consumed_output = any(
553
- req.inflight_middle_chunks <= 0
554
- for req in batch.reqs
555
- if not req.finished() and not req.is_retracted
556
- )
557
- if not has_consumed_output and len(batch.reqs) > 0:
558
- chunks = list([r.inflight_middle_chunks for r in batch.reqs])
559
- logger.warning(
560
- f"PP non-skip output comm: no req consumed next_token_ids. "
561
- f"contains_last_prefill_chunk={batch.contains_last_prefill_chunk}, "
562
- f"num_reqs={len(batch.reqs)}, all inflight_middle_chunks={chunks}"
563
- )
564
- return
565
-
566
- for req in batch.reqs:
567
- if not req.finished() and not req.is_retracted:
568
- assert req.inflight_middle_chunks > 0, (
569
- f"PP skip output comm invariant violated: req {req.rid} "
570
- f"has inflight_middle_chunks={req.inflight_middle_chunks} "
571
- f"but output was skipped (contains_last_prefill_chunk="
572
- f"{batch.contains_last_prefill_chunk}). "
573
- f"Placeholder zeros would be appended to output_ids."
574
- )
575
-
576
- def _append_prefill_hidden_states(
577
- self,
578
- *,
579
- req: Req,
580
- logits_output: LogitsProcessorOutput,
581
- hidden_state_offset: int,
582
- capture_hidden_mode: CaptureHiddenMode,
583
- extend_input_len: int,
584
- store: bool = True,
585
- ) -> int:
586
- if capture_hidden_mode.is_full():
587
- start = hidden_state_offset
588
- hidden_state_offset += extend_input_len
589
- if not store or not req.return_hidden_states:
590
- return hidden_state_offset
591
-
592
- req_hidden_states = logits_output.hidden_states[start:hidden_state_offset]
593
- if req.return_hidden_states is True:
594
- req.hidden_states.append(req_hidden_states.cpu().clone().tolist())
595
- elif req.return_hidden_states == "last":
596
- req.hidden_states.append(req_hidden_states[-1].cpu().tolist())
597
- elif capture_hidden_mode.is_last():
598
- index = hidden_state_offset
599
- hidden_state_offset += 1
600
- if store and req.return_hidden_states:
601
- req.hidden_states.append(
602
- logits_output.hidden_states[index].cpu().tolist()
603
- )
604
- else:
605
- raise ValueError(
606
- f"Unexpected hidden states capture mode: {capture_hidden_mode}"
607
- )
608
- return hidden_state_offset
609
-
610
- @staticmethod
611
- def _append_decode_hidden_states(
612
- *,
613
- req: Req,
614
- hidden_states: torch.Tensor,
615
- start: int,
616
- accept_len: int,
617
- ) -> None:
618
- if accept_len <= 0:
619
- return
620
-
621
- if req.return_hidden_states == "last":
622
- valid_accept_len = accept_len
623
- if req.finished_len is not None:
624
- step_start = len(req.output_ids) - accept_len
625
- valid_accept_len = max(
626
- 0,
627
- min(accept_len, req.finished_len - step_start),
628
- )
629
- if valid_accept_len > 0:
630
- req.hidden_states[:] = [
631
- hidden_states[start + valid_accept_len - 1].cpu().tolist()
632
- ]
633
- else:
634
- req.hidden_states.extend(
635
- hidden_states[start : start + accept_len].cpu().tolist()
636
- )
637
-
638
- @staticmethod
639
- def _get_prefill_hidden_capture_mode(batch: ScheduleBatch) -> CaptureHiddenMode:
640
- return get_required_capture_hidden_mode(
641
- max(
642
- batch.return_hidden_states_mode,
643
- get_server_return_hidden_states_mode(),
644
- ),
645
- batch.spec_info,
646
- )
647
-
648
- def _apply_prefill_grammar(
649
- self, *, req: Req, next_token_id: int, already_advanced: bool = False
650
- ) -> None:
651
- # The grammar barrier may have already advanced the FSM over this prefilled
652
- # token (spec overlap path); only advance if not, but always sync
653
- # grammar.finished.
654
- if not already_advanced:
655
- # FIXME: this try-except block is for handling unexpected xgrammar issue.
656
- try:
657
- req.grammar.accept_token(next_token_id)
658
- except ValueError as e:
659
- # Grammar accept_token can raise ValueError if the token is not in the
660
- # grammar. This can happen if the grammar is not set correctly or the
661
- # token is invalid.
662
- logger.error(
663
- f"Grammar accept_token failed for req {req.rid} with token {next_token_id}: {e}"
664
- )
665
- req.to_finish = FINISH_ABORT()
666
- req.grammar.finished = req.finished()
667
-
668
- def _apply_chunked_prefill_logprobs(
669
- self,
670
- *,
671
- req: Req,
672
- i: int,
673
- logits_output: LogitsProcessorOutput,
674
- extend_input_len_per_req: Optional[List[int]],
675
- extend_logprob_start_len_per_req: Optional[List[int]],
676
- logprob_pt: int,
677
- ) -> int:
678
- extend_logprob_start_len = extend_logprob_start_len_per_req[i]
679
- extend_input_len = extend_input_len_per_req[i]
680
- if extend_logprob_start_len < extend_input_len:
681
- # Update input logprobs.
682
- num_input_logprobs = (
683
- self.logprob_result_processor.calculate_num_input_logprobs(
684
- req,
685
- extend_input_len,
686
- extend_logprob_start_len,
687
- )
688
- )
689
- if req.return_logprob:
690
- self.logprob_result_processor.add_input_logprob_return_values(
691
- i,
692
- req,
693
- logits_output,
694
- logprob_pt,
695
- num_input_logprobs,
696
- last_prefill_chunk=False,
697
- )
698
- logprob_pt += num_input_logprobs
699
- return logprob_pt
700
-
701
- def _resolve_spec_v2_tokens(
702
- self,
703
- result: GenerationBatchResult,
704
- batch: ScheduleBatch,
705
- ) -> List[List[int]]:
706
- """Resolve the padded next token ids for spec-v2 (overlap and non-overlap)."""
707
- assert result.next_token_ids.is_cpu
708
- assert result.accept_lens.is_cpu
709
-
710
- next_token_ids = result.next_token_ids.tolist()
711
- accept_lens = result.accept_lens.tolist()
712
- result.num_correct_drafts = sum(accept_lens) - len(batch.reqs)
713
- result.num_correct_drafts_per_req_cpu = [x - 1 for x in accept_lens]
714
-
715
- block_accept_lens = (
716
- result.block_accept_lens.tolist()
717
- if result.block_accept_lens is not None
718
- else None
719
- )
720
- result.num_block_accept_tokens = (
721
- sum(block_accept_lens) if block_accept_lens else 0
722
- )
723
- cap_lens = result.cap_lens.tolist() if result.cap_lens is not None else None
724
- result.num_cap_tokens = sum(cap_lens) if cap_lens else 0
725
-
726
- # Feed the adaptive controller now that accept_lens is on CPU,
727
- # instead of doing a synchronous GPU→CPU copy in the worker hot path.
728
- # BaseSpecWorker provides a no-op default for non-adaptive workers.
729
- self.model_worker.on_verify_complete_cpu(
730
- result.num_correct_drafts_per_req_cpu, batch_size=len(batch.reqs)
731
- )
732
-
733
- # Advance the grammar FSM over this batch's committed tokens (idempotent):
734
- # the EAGLE overlap path already did this inside verify() via the grammar
735
- # barrier; otherwise advance now. advance_grammar_fsm self-gates on per-req
736
- # grammar (the queued batch.copy() does not carry has_grammar) and consumes
737
- # result.grammar_retained_tokens below instead of re-advancing.
738
- self.advance_grammar_fsm(result, batch)
739
-
740
- predict_tokens = []
741
- # In adaptive spec-v2, the worker state may already have switched when this
742
- # delayed result is processed. Use the draft token count recorded on result.
743
- stride = result.speculative_num_draft_tokens
744
- assert stride is not None, "spec-v2 result missing speculative_num_draft_tokens"
745
-
746
- for i, req in enumerate(batch.reqs):
747
- accept_tokens = next_token_ids[i * stride : i * stride + accept_lens[i]]
748
-
749
- if req.is_retracted or req.finished():
750
- # Nothing to settle: no worker pre-claims the bonus, so
751
- # kv_committed_len already holds the committed prefix.
752
- pass
753
- else:
754
- if req.grammar is not None:
755
- # FSM already advanced + truncated by advance_grammar_fsm; reuse
756
- # the retained (grammar-legal) run instead of advancing again.
757
- accept_tokens = result.grammar_retained_tokens[i]
758
-
759
- # Commit the full accepted run (drafts + bonus).
760
- num_accept_tokens = len(accept_tokens)
761
- req.kv.kv_committed_len += num_accept_tokens
762
- req.spec_verify_ct += 1
763
-
764
- num_correct_drafts = result.num_correct_drafts_per_req_cpu[i]
765
- req.spec_num_correct_drafts += num_correct_drafts
766
- req.update_spec_correct_drafts_histogram(num_correct_drafts)
767
-
768
- if block_accept_lens is not None:
769
- req.spec_num_block_accept_tokens += block_accept_lens[i]
770
- if cap_lens is not None:
771
- req.spec_num_cap_tokens += cap_lens[i]
772
- req.update_spec_cap_lens_histogram(cap_lens[i])
773
-
774
- predict_tokens.append(accept_tokens)
775
-
776
- return predict_tokens
777
-
778
- def _accept_grammar_tokens(
779
- self, req: Req, tokens: Union[int, List[int]]
780
- ) -> List[int]:
781
- """Advance the grammar over the accepted token(s), stopping at the token
782
- that terminates it.
783
-
784
- ``tokens`` is a single sampled token (normal decode) or the whole
785
- verified run (spec decode). Returns the retained prefix; for spec the
786
- suffix past grammar completion is dropped so it is never committed to KV
787
- nor emitted. Advances the grammar FSM only -- ``grammar.finished`` is
788
- synced by the caller once the finish state is updated.
789
- """
790
- if isinstance(tokens, int):
791
- tokens = [tokens]
792
- retained = []
793
- try:
794
- for token_id in tokens:
795
- req.grammar.accept_token(token_id)
796
- retained.append(token_id)
797
- if req.grammar.is_terminated():
798
- break
799
- except ValueError as e:
800
- # accept_token raises ValueError if the token is not in the grammar
801
- # (misconfigured grammar or invalid token); abort the request.
802
- logger.error(
803
- f"Grammar accept_token failed for req {req.rid} with token "
804
- f"{tokens}: {e}"
805
- )
806
- req.to_finish = FINISH_ABORT()
807
- return retained
808
-
809
- def advance_grammar_fsm(
810
- self, result: GenerationBatchResult, batch: ScheduleBatch
811
- ) -> None:
812
- """Advance each req's grammar FSM over the tokens THIS batch committed, and
813
- (for decode) memoize the grammar-truncated run on ``result``.
814
-
815
- This is the single place the spec-v2 FSM advances. It is idempotent
816
- (``result.grammar_advanced``) so it runs either eagerly — inside ``verify(N)``
817
- via the scheduler's grammar barrier, so the advance overlaps the target-verify
818
- forward — or lazily from the result processors on the non-overlap / non-EAGLE
819
- paths. It handles both decode (the accepted spec run) and extend (the single
820
- prefilled token) results, so the barrier can resolve whatever the previous
821
- batch was — e.g. the extend->decode boundary.
822
- """
823
- if result.grammar_advanced or not batch.has_grammar:
824
- return
825
- is_decode = batch.forward_mode.is_decode()
826
- if not (is_decode or batch.forward_mode.is_extend()):
827
- return
828
- if result.copy_done is not None:
829
- result.copy_done.synchronize()
830
- next_token_ids = result.next_token_ids.tolist()
831
-
832
- if not is_decode:
833
- # Extend: advance over the single token each completed-prefill req emitted
834
- # (mirrors process_batch_result_prefill's per-req token indexing).
835
- for i, req in enumerate(batch.reqs):
836
- if (
837
- req.grammar is None
838
- or req.is_retracted
839
- or req.finished()
840
- or req.inflight_middle_chunks > 0
841
- ):
842
- continue
843
- self._accept_grammar_tokens(req, next_token_ids[i])
844
- result.grammar_advanced = True
845
- return
846
-
847
- # Decode: only the spec-v2 path reaches here (the grammar barrier for
848
- # spec-overlap workers and _resolve_spec_v2_tokens). Non-spec grammar decode
849
- # advances its FSM in process_batch_result_decode and has no accept_lens, so
850
- # bail out defensively.
851
- if result.accept_lens is None:
852
- return
853
- accept_lens = result.accept_lens.tolist()
854
- stride = result.speculative_num_draft_tokens
855
- assert stride is not None, "spec-v2 result missing speculative_num_draft_tokens"
856
- retained = [None] * len(batch.reqs)
857
- for i, req in enumerate(batch.reqs):
858
- if req.grammar is None or req.is_retracted or req.finished():
859
- continue
860
- accept_tokens = next_token_ids[i * stride : i * stride + accept_lens[i]]
861
- # Stop accepting once the grammar terminates so the over-drafted suffix
862
- # is never committed to KV nor emitted; this advances the FSM.
863
- retained[i] = self._accept_grammar_tokens(req, accept_tokens)
864
- result.grammar_retained_tokens = retained
865
- result.grammar_advanced = True
866
-
867
- def process_batch_result_idle(
868
- self,
869
- batch: ScheduleBatch,
870
- result: GenerationBatchResult,
871
- ):
872
- if result.copy_done is not None:
873
- result.copy_done.synchronize()
874
-
875
- self.output_streamer._stream_output_generation(
876
- batch.reqs, batch.return_logprob, is_idle_batch=True
877
- )
878
-
879
- def process_batch_result_decode(
880
- self,
881
- batch: ScheduleBatch,
882
- result: GenerationBatchResult,
883
- ):
884
- if result.copy_done is not None:
885
- result.copy_done.synchronize()
886
- auxiliary_output_starts = self.snapshot_auxiliary_output_starts(batch, result)
887
- auxiliary_output = result.auxiliary_host_output
888
- if result.routed_experts_output is not None:
889
- result.routed_experts_output.finalize()
890
- result.routed_experts_output = None
891
- if result.indexer_topk_output is not None:
892
- result.indexer_topk_output.finalize()
893
- result.indexer_topk_output = None
894
-
895
- logits_output, next_token_ids, can_run_cuda_graph = (
896
- result.logits_output,
897
- result.next_token_ids,
898
- result.can_run_cuda_graph,
899
- )
900
-
901
- next_token_ids, next_token_logprobs = self._normalize_decode_outputs(
902
- batch=batch,
903
- result=result,
904
- logits_output=logits_output,
905
- next_token_ids=next_token_ids,
906
- )
907
-
908
- self.metrics_reporter.num_generated_tokens += len(batch.reqs)
909
- if not batch.spec_algorithm.is_none():
910
- self.metrics_reporter.update_spec_metrics(
911
- batch.batch_size(),
912
- result.num_correct_drafts,
913
- num_block_accept_tokens=result.num_block_accept_tokens,
914
- num_cap_tokens=result.num_cap_tokens,
915
- )
916
- if get_observability().enable_metrics:
917
- self.metrics_collector.increment_decode_cuda_graph_pass(
918
- value=can_run_cuda_graph
919
- )
920
-
921
- self.token_to_kv_pool_allocator.free_group_begin()
922
-
923
- # Folds the relay point's selection into the DAG and sets the finish
924
- # states the loop below observes. Beam + spec is rejected at admission.
925
- newly_finished_beam_groups = set()
926
- if batch.spec_algorithm.is_none() and logits_output is not None:
927
- newly_finished_beam_groups = self.beam_coordinator.commit_decode(batch)
928
-
929
- for i, req in enumerate(batch.reqs):
930
- req: Req
931
-
932
- if req.beam_group is not None:
933
- # Under overlap a finished row reappears for one overshoot tick;
934
- # gate on the committing tick so this runs exactly once.
935
- if req.finished() and (
936
- id(req.beam_group) not in newly_finished_beam_groups
937
- ):
938
- continue
939
- req.time_stats.set_last_decode_finish_time()
940
- self._handle_finish_state_updated_req(
941
- req, batch, result, i, logits_output
942
- )
943
- continue
944
-
945
- if (self.enable_overlap or self.enable_overlap_mlx) and (
946
- req.finished() or req.is_retracted
947
- ):
948
- # NOTE: This (req.finished() or req.is_retracted) should only happen when overlap scheduling is enabled.
949
- # And all the over-allocated tokens will be freed in `release_kv_cache`.
950
- continue
951
-
952
- # next_token_id is a per-req list: 1 token for non-spec, the verified
953
- # run for spec (already grammar-truncated in _resolve_spec_v2_tokens).
954
- next_token_id = next_token_ids[i]
955
- is_spec = not batch.spec_algorithm.is_none()
956
-
957
- req.output_ids.extend(next_token_id)
958
- new_accept_len = len(next_token_id)
959
-
960
- self._maybe_update_reasoning_tokens(req, next_token_id)
961
- req.time_stats.set_last_decode_finish_time()
962
- req.update_finish_state(new_accept_len)
963
-
964
- self._handle_finish_state_updated_req(req, batch, result, i, logits_output)
965
-
966
- if req.return_logprob:
967
- self._apply_decode_logprobs(
968
- req=req,
969
- i=i,
970
- batch=batch,
971
- next_token_id=next_token_id,
972
- next_token_logprobs=next_token_logprobs,
973
- logits_output=logits_output,
974
- )
975
-
976
- if req.return_sampling_mask:
977
- # return_sampling_mask + speculative decoding is rejected at
978
- # request entry, so this remains one support mask per token.
979
- self.add_sampling_mask_return_values(i, req, logits_output)
980
-
981
- if req.return_hidden_states and logits_output.hidden_states is not None:
982
- # hidden_states is [bs * stride, hidden_dim], one row per emitted
983
- # token; stride = speculative_num_draft_tokens for spec, 1 for non-spec.
984
- stride = result.speculative_num_draft_tokens or 1
985
- accept_len = len(next_token_id)
986
- start = i * stride
987
- self._append_decode_hidden_states(
988
- req=req,
989
- hidden_states=logits_output.hidden_states,
990
- start=start,
991
- accept_len=accept_len,
992
- )
993
-
994
- if req.grammar is not None:
995
- if not is_spec:
996
- # Normal decode advances the grammar for its single token
997
- # here; spec already advanced it in _resolve_spec_v2_tokens.
998
- self._accept_grammar_tokens(req, next_token_id)
999
- req.grammar.finished = req.finished()
1000
-
1001
- if auxiliary_output is not None:
1002
- self.consume_auxiliary_output(
1003
- batch,
1004
- auxiliary_output,
1005
- auxiliary_output_starts,
1006
- )
1007
-
1008
- self.output_streamer.stream_output(batch.reqs, batch.return_logprob)
1009
- self.token_to_kv_pool_allocator.free_group_end()
1010
-
1011
- self.metrics_reporter.forward_ct_decode = (
1012
- self.metrics_reporter.forward_ct_decode + 1
1013
- ) % (1 << 30)
1014
- self.metrics_reporter.report_decode_stats(
1015
- can_run_cuda_graph,
1016
- running_batch=batch,
1017
- num_correct_drafts=result.num_correct_drafts,
1018
- )
1019
-
1020
- def _normalize_decode_outputs(
1021
- self,
1022
- *,
1023
- batch: ScheduleBatch,
1024
- result: GenerationBatchResult,
1025
- logits_output: LogitsProcessorOutput,
1026
- next_token_ids: Union[torch.Tensor, List[int]],
1027
- ) -> Tuple[Union[List[int], List[List[int]]], Optional[List[float]]]:
1028
- next_token_logprobs = None
1029
- # Normalize to a uniform per-req list of accepted tokens (List[List[int]]):
1030
- # spec unpacks the padded verify output; non-spec wraps its single token.
1031
- if not batch.spec_algorithm.is_none():
1032
- next_token_ids = self._resolve_spec_v2_tokens(result, batch)
1033
- else:
1034
- # CUDA workers return a device tensor, MLX a host list[int]; both -> list.
1035
- ids = (
1036
- next_token_ids.tolist()
1037
- if torch.is_tensor(next_token_ids)
1038
- else next_token_ids
1039
- )
1040
- next_token_ids = [[t] for t in ids]
1041
-
1042
- if batch.return_logprob:
1043
- next_token_logprobs = logits_output.next_token_logprobs.tolist()
1044
- if logits_output.next_token_top_logprobs_val:
1045
- logits_output.next_token_top_logprobs_val = [
1046
- v.tolist() for v in logits_output.next_token_top_logprobs_val
1047
- ]
1048
- logits_output.next_token_top_logprobs_idx = [
1049
- x.tolist() for x in logits_output.next_token_top_logprobs_idx
1050
- ]
1051
-
1052
- if logits_output.next_token_token_ids_logprobs_val:
1053
- logits_output.next_token_token_ids_logprobs_val = [
1054
- v.tolist() if isinstance(v, torch.Tensor) else v
1055
- for v in logits_output.next_token_token_ids_logprobs_val
1056
- ]
1057
- return next_token_ids, next_token_logprobs
1058
-
1059
- def _apply_decode_logprobs(
1060
- self,
1061
- *,
1062
- req: Req,
1063
- i: int,
1064
- batch: ScheduleBatch,
1065
- next_token_id: Union[int, List[int]],
1066
- next_token_logprobs: list,
1067
- logits_output: LogitsProcessorOutput,
1068
- ) -> None:
1069
- # accepted_ids is already a per-req list; non-spec logprobs are flat, so
1070
- # the scalar logprob still needs wrapping.
1071
- if not batch.spec_algorithm.is_none():
1072
- accepted_logprobs = next_token_logprobs[i]
1073
- accepted_ids = next_token_id
1074
- max_accept = len(accepted_logprobs)
1075
- else:
1076
- accepted_logprobs = [next_token_logprobs[i]]
1077
- accepted_ids = next_token_id
1078
- max_accept = 1
1079
-
1080
- for j, tok_id in enumerate(accepted_ids):
1081
- req.logprob.output_token_logprobs_val.append(accepted_logprobs[j])
1082
- req.logprob.output_token_logprobs_idx.append(tok_id)
1083
- if req.logprob.top_logprobs_num > 0:
1084
- flat_idx = i * max_accept + j
1085
- req.logprob.output_top_logprobs_val.append(
1086
- logits_output.next_token_top_logprobs_val[flat_idx]
1087
- )
1088
- req.logprob.output_top_logprobs_idx.append(
1089
- logits_output.next_token_top_logprobs_idx[flat_idx]
1090
- )
1091
- if req.logprob.token_ids_logprob is not None:
1092
- flat_idx = i * max_accept + j
1093
- req.logprob.output_token_ids_logprobs_val.append(
1094
- logits_output.next_token_token_ids_logprobs_val[flat_idx]
1095
- )
1096
- req.logprob.output_token_ids_logprobs_idx.append(
1097
- logits_output.next_token_token_ids_logprobs_idx[flat_idx]
1098
- )
1099
-
1100
- def add_sampling_mask_return_values(
1101
- self,
1102
- i: int,
1103
- req: Req,
1104
- output: LogitsProcessorOutput,
1105
- ) -> None:
1106
- """Attach sparse sampling support metadata to the return values."""
1107
- mask = output.next_token_sampling_mask_idx
1108
- logprobs = output.next_token_sampling_logprobs
1109
- req.output_token_sampling_mask.append(None if mask is None else mask[i])
1110
- req.output_token_sampling_logprobs.append(
1111
- None if logprobs is None else logprobs[i]
1112
- )
1113
-
1114
- def _handle_finish_state_updated_req(
1115
- self,
1116
- req: Req,
1117
- batch: ScheduleBatch,
1118
- result: GenerationBatchResult,
1119
- i: int,
1120
- logits_output: LogitsProcessorOutput,
1121
- ):
1122
- lazy = mamba_extra_buffer_lazy_enabled()
1123
- known_mamba_boundary = None
1124
- completed_mamba_boundary = None
1125
- lookahead = 0
1126
- if batch.mamba_track_mask_cpu is not None:
1127
- completed_mamba_boundary = bool(batch.mamba_track_mask_cpu[i])
1128
- lookahead = req.decode_batch_idx - batch.mamba_decode_batch_idx_cpu[i]
1129
- assert lookahead in (0, 1), (
1130
- f"mamba result lookahead={lookahead} for req {req.rid}; "
1131
- "overlap advanced more than one decode batch"
1132
- )
1133
- if lookahead == 0:
1134
- known_mamba_boundary = bool(batch.mamba_track_mask_cpu[i])
1135
- else:
1136
- known_mamba_boundary = bool(batch.mamba_track_mask_next_cpu[i])
1137
-
1138
- if completed_mamba_boundary and not lazy:
1139
- req.kv.mamba_last_track_idx = batch.mamba_track_buffer_indices[i]
1140
- req.kv.mamba_last_track_seqlen = req.kv.kv_committed_len - lookahead
1141
- elif (
1142
- req.finished()
1143
- and lazy
1144
- and lookahead == 1
1145
- and known_mamba_boundary
1146
- and req.kv.mamba_next_track_idx == req.kv.mamba_last_track_idx
1147
- ):
1148
- req.mamba_lazy_is_insert = False
1149
-
1150
- # Called here (after update_finish_state) so req.finished() is valid
1151
- # for mamba_lazy_post_decode_at_boundary inside.
1152
- should_update = completed_mamba_boundary if lazy else known_mamba_boundary
1153
- if should_update is None or should_update:
1154
- self._mamba_prefix_cache_update(
1155
- req,
1156
- batch,
1157
- result,
1158
- i,
1159
- known_boundary=not lazy and known_mamba_boundary is True,
1160
- )
1161
-
1162
- if (
1163
- get_disagg().disaggregation_decode_enable_offload_kvcache
1164
- and not req.finished()
1165
- ):
1166
- self.decode_offload_manager.offload_kv_cache(req)
1167
-
1168
- if req.finished():
1169
- # isinstance narrowing: create_worker may also return plain
1170
- # TpModelWorker-based drafts, which carry no spec-worker hooks.
1171
- if isinstance(self.draft_worker, BaseSpecWorker):
1172
- self.draft_worker.note_request_finished(
1173
- rid=req.rid,
1174
- natural_stop=isinstance(req.finished_reason, FINISH_MATCHED_TOKEN),
1175
- )
1176
-
1177
- # delete feature to save memory
1178
- if req.multimodal_inputs is not None and req.session is None:
1179
- req.multimodal_inputs.release_features()
1180
- self._maybe_collect_routed_experts(req)
1181
- self._maybe_collect_indexer_topk(req)
1182
-
1183
- if get_disagg().disaggregation_decode_enable_offload_kvcache:
1184
- # Asynchronously offload KV cache; release_kv_cache will be called after Device->Host transfer completes
1185
- if not self.decode_offload_manager.offload_kv_cache(req):
1186
- self.decode_offload_manager.finalize_release_on_finish(req)
1187
- else:
1188
- if get_memory().enable_hisparse:
1189
- self.hisparse_coordinator.request_finished(req)
1190
- prepare_release = getattr(
1191
- self.model_worker, "prepare_for_kv_cache_release", None
1192
- )
1193
- if callable(prepare_release):
1194
- prepare_release(req)
1195
- is_insert = (
1196
- req.mamba_lazy_is_insert
1197
- if mamba_extra_buffer_lazy_enabled()
1198
- else True
1199
- )
1200
- release_kv_cache(req, self.tree_cache, is_insert=is_insert)
1201
-
1202
- req.time_stats.set_completion_time()
1203
-
1204
- self._maybe_collect_customized_info(i, req, logits_output)
1205
-
1206
- def _maybe_update_reasoning_tokens(
1207
- self,
1208
- req: Req,
1209
- next_token_id: Union[int, List[int]],
1210
- ):
1211
- think_end_ids = self.model_config.think_end_ids
1212
- if req.require_reasoning and think_end_ids:
1213
- req.update_reasoning_tokens(next_token_id, think_end_ids)
1214
-
1215
- def _mamba_prefix_cache_update(
1216
- self,
1217
- req: Req,
1218
- batch: ScheduleBatch,
1219
- result: GenerationBatchResult,
1220
- i: int,
1221
- known_boundary: bool = False,
1222
- ) -> None:
1223
- """Update mamba track state at ping-pong boundaries.
1224
-
1225
- Non-lazy: swap the ping-pong index so the next forward writes to
1226
- the alternate slot.
1227
- Lazy: keep the same index (prealloc handles the swap) and run
1228
- post-decode cleanup to free the temporary second slot.
1229
- """
1230
- if req.kv.mamba_ping_pong_track_buffer is None:
1231
- return
1232
-
1233
- lazy = mamba_extra_buffer_lazy_enabled()
1234
- if known_boundary:
1235
- self._mamba_assert_committed_len_lookahead(req)
1236
- track_seqlen = req.kv.kv_committed_len
1237
- assert track_seqlen % mamba_track_grid(self.tree_cache.page_size) == 0
1238
- at_boundary = True
1239
- else:
1240
- at_boundary, track_seqlen = self._mamba_check_track_boundary(
1241
- req, batch, result, i
1242
- )
1243
-
1244
- if lazy and not batch.spec_algorithm.is_none():
1245
- # For spec, at_boundary means this step actually crossed an interval.
1246
- self._mamba_lazy_spec_update(req, batch, i, at_boundary, track_seqlen)
1247
- return
1248
-
1249
- if not at_boundary:
1250
- return
1251
-
1252
- track_idx = req.kv.mamba_next_track_idx
1253
- if not known_boundary and batch.mamba_track_buffer_indices is not None:
1254
- track_idx = batch.mamba_track_buffer_indices[i]
1255
- if not known_boundary:
1256
- req.kv.mamba_last_track_seqlen = track_seqlen
1257
- if lazy:
1258
- self.mamba_lazy_post_decode_at_boundary(req, batch, track_idx)
1259
- else:
1260
- if not known_boundary:
1261
- req.kv.mamba_last_track_idx = track_idx
1262
- req.kv.mamba_next_track_idx = (
1263
- batch.req_to_token_pool.get_mamba_ping_pong_other_idx(track_idx)
1264
- )
1265
-
1266
- def _mamba_lazy_spec_update(
1267
- self, req: Req, batch: ScheduleBatch, i: int, crossed: bool, track_seqlen: int
1268
- ) -> None:
1269
- """Lazy + spec post-processing.
1270
-
1271
- Running req with a confirmed crossing: the commit scattered the
1272
- crossing state into the planned position; promote it to keep unless
1273
- the plan already pointed at the keep slot (in-place fallback, or
1274
- promoted by an earlier overlapped confirmation).
1275
- Finishing req: donate the keep slot only if no scatter wrote it
1276
- (this step) or may still write it (the in-flight next step).
1277
- """
1278
- positions = batch.mamba_lazy_spec_track_positions_cpu
1279
- planned_pos = (
1280
- positions[i]
1281
- if positions is not None and i < len(positions)
1282
- else None # filtered/merged snapshot without a plan: be conservative
1283
- )
1284
-
1285
- if req.finished():
1286
- # Skip the donation if a scatter wrote or may still write the keep slot.
1287
- keep_written_by_this_step = (
1288
- crossed and planned_pos == req.kv.mamba_next_track_idx
1289
- )
1290
- other_idx = 1 - req.kv.mamba_next_track_idx
1291
- # Recompute the in-flight verify's plan (kv_committed_len is
1292
- # frozen since its prepare, so the recompute is exact).
1293
- keep_may_be_written_in_flight = req.kv.mamba_ping_pong_track_buffer[
1294
- other_idx
1295
- ].item() == -1 and mamba_lazy_spec_in_window(
1296
- req,
1297
- mamba_track_grid(self.tree_cache.page_size),
1298
- max_speculative_num_draft_tokens(),
1299
- )
1300
- if (
1301
- planned_pos is None
1302
- or keep_written_by_this_step
1303
- or keep_may_be_written_in_flight
1304
- ):
1305
- req.mamba_lazy_is_insert = False
1306
- return
1307
-
1308
- if not crossed or planned_pos is None:
1309
- return
1310
- if planned_pos != req.kv.mamba_next_track_idx:
1311
- # Promote pending -> keep: free the old checkpoint, repoint.
1312
- pool = batch.req_to_token_pool
1313
- keep_idx = req.kv.mamba_next_track_idx
1314
- keep_val = req.kv.mamba_ping_pong_track_buffer[keep_idx]
1315
- pool.mamba_allocator.free(keep_val.unsqueeze(0))
1316
- pool.set_mamba_ping_pong_slot(req, keep_idx, -1)
1317
- req.kv.mamba_next_track_idx = planned_pos
1318
- # else: in-place fallback, or promoted by an earlier confirmation —
1319
- # keep holds the track_seqlen state either way.
1320
- req.kv.mamba_last_track_idx = planned_pos
1321
- req.kv.mamba_last_track_seqlen = track_seqlen
1322
-
1323
- @staticmethod
1324
- def _mamba_assert_committed_len_lookahead(req: Req) -> None:
1325
- """Alarm if overlap advances beyond the scheduler's modeled window."""
1326
- assert req.output_ids, (
1327
- "mamba track boundary reached before a decode token was appended "
1328
- f"(req {req.rid}); output_ids is empty"
1329
- )
1330
- token_seq_len = len(req.origin_input_ids) + len(req.output_ids) - 1
1331
- assert (req.kv.kv_committed_len - token_seq_len) in (0, 1), (
1332
- f"mamba track boundary: kv_committed_len={req.kv.kv_committed_len} "
1333
- f"leads seq_len={token_seq_len} by more than one (req {req.rid}); "
1334
- "overlap lookahead wider than assumed"
1335
- )
1336
-
1337
- def _mamba_check_track_boundary(self, req, batch, result, i):
1338
- """Check if this decode step crosses a mamba track interval boundary.
1339
-
1340
- Returns (at_boundary, track_seqlen). The boundary condition
1341
- matches what the forward's tracking mask used:
1342
- ``prepare_for_decode`` increments both ``seq_lens_cpu`` and
1343
- ``kv_committed_len`` by 1, then checks
1344
- ``seq_lens_cpu % interval == 0``. Subtracting the overlap
1345
- lookahead from ``kv_committed_len`` reproduces that check.
1346
-
1347
- For spec decode, the boundary is detected by comparing the
1348
- accepted seq_len range against interval boundaries.
1349
- """
1350
- interval = mamba_track_grid(self.tree_cache.page_size)
1351
-
1352
- if batch.spec_algorithm.is_none():
1353
- lookahead = req.decode_batch_idx - batch.mamba_decode_batch_idx_cpu[i]
1354
- committed_len = req.kv.kv_committed_len - lookahead
1355
- if committed_len % interval == 0:
1356
- return True, committed_len
1357
- elif result.num_correct_drafts_per_req_cpu is not None:
1358
- cur = req.seqlen - 1
1359
- prev = cur - result.num_correct_drafts_per_req_cpu[i] - 1
1360
- if cur // interval != prev // interval:
1361
- return True, cur // interval * interval
1362
-
1363
- return False, 0
1364
-
1365
- def mamba_lazy_post_decode_at_boundary(
1366
- self, req: Req, batch: ScheduleBatch, track_idx: int
1367
- ):
1368
- """Commit a completed lazy-mode boundary and free its old slot."""
1369
- req.kv.mamba_last_track_idx = track_idx
1370
- req.kv.mamba_next_track_idx = track_idx
1371
- other_idx = 1 - track_idx
1372
- other_val = req.kv.mamba_ping_pong_track_buffer[other_idx].item()
1373
- if other_val != -1:
1374
- pool = batch.req_to_token_pool
1375
- pool.mamba_allocator.free(
1376
- req.kv.mamba_ping_pong_track_buffer[other_idx].unsqueeze(0)
1377
- )
1378
- pool.set_mamba_ping_pong_slot(req, other_idx, -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runtime/patches/sglang-logits-processor.py DELETED
@@ -1,1272 +0,0 @@
1
- # Copyright 2023-2024 SGLang Team
2
- # Licensed under the Apache License, Version 2.0 (the "License");
3
- # you may not use this file except in compliance with the License.
4
- # You may obtain a copy of the License at
5
- #
6
- # http://www.apache.org/licenses/LICENSE-2.0
7
- #
8
- # Unless required by applicable law or agreed to in writing, software
9
- # distributed under the License is distributed on an "AS IS" BASIS,
10
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
- # See the License for the specific language governing permissions and
12
- # limitations under the License.
13
- # ==============================================================================
14
- """Logits processing."""
15
-
16
- import dataclasses
17
- import logging
18
- from contextlib import contextmanager
19
- from typing import Any, Dict, List, Optional, Tuple, Union
20
-
21
- import torch
22
- from torch import nn
23
-
24
- from sglang.kernels.ops.activation.softcap import (
25
- softcap_inplace_logits as fused_softcap,
26
- )
27
- from sglang.srt.beam_search.logits_capture import BeamLogitsCapture
28
- from sglang.srt.distributed import get_tp_group
29
- from sglang.srt.distributed.device_communicators import triton_symm_mem_ag
30
- from sglang.srt.environ import envs
31
- from sglang.srt.layers import layernorm_sp
32
- from sglang.srt.layers.aux_hidden_states import (
33
- AuxHiddenStates,
34
- pack_aux_hidden_states,
35
- )
36
- from sglang.srt.layers.dp_attention import (
37
- DpPaddingMode,
38
- attn_tp_all_gather,
39
- attn_tp_all_gather_into_tensor,
40
- dp_gather_replicate,
41
- dp_scatter,
42
- get_dp_device,
43
- get_dp_dtype,
44
- get_dp_hidden_size,
45
- )
46
- from sglang.srt.layers.logprob_processor import (
47
- InputLogprobProcessor,
48
- LogprobStage,
49
- get_token_ids_logprobs_raw,
50
- get_top_logprobs_raw,
51
- )
52
- from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
53
- from sglang.srt.model_executor.forward_batch_info import (
54
- CaptureHiddenMode,
55
- ForwardBatch,
56
- ForwardMode,
57
- )
58
- from sglang.srt.runtime_context import get_exec, get_parallel
59
- from sglang.srt.sampling.sampling_observer import DeviceAuxiliaryOutput
60
- from sglang.srt.utils.common import (
61
- is_cpu,
62
- is_npu,
63
- is_pin_memory_available,
64
- use_intel_amx_backend,
65
- )
66
-
67
- logger = logging.getLogger(__name__)
68
-
69
- _is_npu = is_npu()
70
- _is_cpu = is_cpu()
71
-
72
- _UNQUANTIZED_LM_HEAD_METHODS = {
73
- "UnquantizedEmbeddingMethod",
74
- "UnquantizedLinearMethod",
75
- "PackWeightMethod",
76
- }
77
-
78
- # None outside a FlashInfer autotune pass; inside one, whether that pass runs the
79
- # LM head. Not-None means the forward's output is discarded -- attention backends
80
- # read that via get_in_autotune_dummy_run() to skip a cross-node exchange.
81
- # Skipping the LM head skips its [batch * dp_size, vocab] all-gather, which OOMs
82
- # under DP attention with a tight mem_fraction_static.
83
- _autotune_run_lm_head: Optional[bool] = None
84
-
85
-
86
- def _trace_e2e_logits(stage: str, **fields) -> None:
87
- if not envs.SGLANG_TRACE_LOGITS_E2E.get():
88
- return
89
- try:
90
- parallel = get_parallel()
91
- rank = f"dp={parallel.attn_dp_rank} " f"tp={parallel.tp_rank}"
92
- except Exception:
93
- rank = "rank=unknown"
94
- details = " ".join(f"{key}={value}" for key, value in fields.items())
95
- print(f"SGLANG_TRACE_LOGITS_E2E {rank} stage={stage} {details}", flush=True)
96
-
97
-
98
- def _has_lm_head_runtime_attrs(lm_head, attr_names: Tuple[str, ...]) -> bool:
99
- return all(hasattr(lm_head, attr_name) for attr_name in attr_names)
100
-
101
-
102
- def should_apply_lm_head_quant_method(lm_head, quant_method) -> bool:
103
- if (
104
- quant_method is None
105
- or not hasattr(lm_head, "weight")
106
- or not callable(getattr(quant_method, "apply", None))
107
- ):
108
- return False
109
-
110
- method_name = type(quant_method).__name__
111
- if method_name in _UNQUANTIZED_LM_HEAD_METHODS:
112
- return False
113
-
114
- # A shared target lm_head can retain the draft's stale ModelOpt method; use it
115
- # only when the runtime tensor layout matches that method.
116
- if method_name == "ModelOptFp4LinearMethod":
117
- if lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
118
- lm_head,
119
- (
120
- "weight_scale",
121
- "weight_global_scale",
122
- "workspace",
123
- "input_size_per_partition",
124
- "output_size_per_partition",
125
- ),
126
- ):
127
- return True
128
- return lm_head.weight.dtype == torch.uint8 and _has_lm_head_runtime_attrs(
129
- lm_head,
130
- (
131
- "weight_scale_interleaved",
132
- "alpha",
133
- "input_scale_inv",
134
- "input_size_per_partition",
135
- "output_size_per_partition",
136
- ),
137
- )
138
- if method_name == "ModelOptNvFp4A16LinearMethod":
139
- return lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
140
- lm_head,
141
- (
142
- "weight_scale",
143
- "weight_global_scale",
144
- "workspace",
145
- "input_size_per_partition",
146
- "output_size_per_partition",
147
- ),
148
- )
149
- if method_name == "ModelOptFp8LinearMethod":
150
- return (
151
- lm_head.weight.dtype == torch.float8_e4m3fn
152
- and _has_lm_head_runtime_attrs(lm_head, ("weight_scale", "input_scale"))
153
- )
154
-
155
- return True
156
-
157
-
158
- # FlashInfer autotune skips the unprofiled LM-head all-gather; its
159
- # [batch * dp_size, vocab] output can OOM under tight DP-attention memory.
160
- _in_autotune_dummy_run = False
161
-
162
-
163
- def get_in_autotune_dummy_run() -> bool:
164
- return _autotune_run_lm_head is not None
165
-
166
-
167
- @contextmanager
168
- def autotune_dummy_run_mode(*, run_lm_head: bool):
169
- global _autotune_run_lm_head
170
- _autotune_run_lm_head = run_lm_head
171
- try:
172
- yield
173
- finally:
174
- _autotune_run_lm_head = None
175
-
176
-
177
- @dataclasses.dataclass
178
- class LogitsProcessorOutput:
179
- ## Part 1: This part will be assigned in python/sglang/srt/layers/logits_processor.py::LogitsProcessor
180
- # The logits of the next tokens. shape: [#seq, vocab_size]
181
- # Can be None for certain prefill-only requests (e.g., multi-item scoring) that don't need next token generation
182
- next_token_logits: Optional[torch.Tensor]
183
- # Used by speculative decoding (EAGLE)
184
- # The last hidden layers
185
- hidden_states: Optional[torch.Tensor] = None
186
-
187
- ## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler
188
- # he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature.
189
- next_token_logprobs: Optional[torch.Tensor] = None
190
- # The logprobs and ids of the top-k tokens in output positions. shape: [#seq, k]
191
- next_token_top_logprobs_val: Optional[List] = None
192
- next_token_top_logprobs_idx: Optional[List] = None
193
- # The logprobs and ids of the requested token ids in output positions. shape: [#seq, n] (n is the number of requested token ids)
194
- # Can contain either lists or GPU tensors (for delayed copy optimization in prefill-only requests)
195
- next_token_token_ids_logprobs_val: Optional[
196
- List[Union[List[float], torch.Tensor]]
197
- ] = None
198
- next_token_token_ids_logprobs_idx: Optional[List] = None
199
- # Sparse top-k/top-p/min-p support ids and selected-token logprob after
200
- # truncation/renormalization. Only populated when requested.
201
- next_token_sampling_mask_idx: Optional[List[Optional[List[int]]]] = None
202
- next_token_sampling_logprobs: Optional[List[Optional[float]]] = None
203
-
204
- ## Part 3: Prefill-only. This part will be assigned in python/sglang/srt/layers/logits_processor.py::LogitsProcessor
205
- # The logprobs of input tokens. shape: [#token]
206
- input_token_logprobs: Optional[torch.Tensor] = None
207
- # The logprobs and ids of the top-k tokens in input positions. shape: [#seq, #token, k]
208
- input_top_logprobs_val: Optional[List] = None
209
- input_top_logprobs_idx: Optional[List] = None
210
- # The logprobs and ids of the requested token ids in input positions. shape: [#seq, n] (n is the number of requested token ids)
211
- # Can contain either lists or GPU tensors (for delayed GPU-to-CPU transfer optimization)
212
- input_token_ids_logprobs_val: Optional[List[Union[List[float], torch.Tensor]]] = (
213
- None
214
- )
215
- input_token_ids_logprobs_idx: Optional[List] = None
216
-
217
- ## Part 4: Diffusion LLM only.
218
- full_logits: Optional[torch.Tensor] = None
219
-
220
- # Beam search only: raw pre-sample logits for the scheduler-side joint
221
- # selection; see beam_search.logits_capture.
222
- beam: Optional[BeamLogitsCapture] = None
223
-
224
- ## Part 5: Customized Info
225
- customized_info: Optional[Dict[str, List[Any]]] = None
226
-
227
- ## Part 6: Temporary variables
228
- # FIXME: These fields are not logits-related but are passed through here as a
229
- # workaround since ForwardBatch is local to forward_batch_generation().
230
- # They should be moved to GenerationBatchResult to keep this class clean.
231
- mm_input_embeds: Optional[torch.Tensor] = None
232
-
233
- # Scheduler-local output copied alongside the ordinary generation result.
234
- auxiliary_device_output: Optional[DeviceAuxiliaryOutput] = None
235
-
236
-
237
- @dataclasses.dataclass
238
- class LogitsMetadata:
239
- forward_mode: ForwardMode
240
- capture_hidden_mode: CaptureHiddenMode = CaptureHiddenMode.NULL
241
- next_token_logits_buffer: Optional[torch.Tensor] = None
242
-
243
- extend_return_logprob: bool = False
244
- extend_return_top_logprob: bool = False
245
- extend_token_ids_logprob: bool = False
246
- extend_seq_lens: Optional[torch.Tensor] = None
247
- extend_seq_lens_cpu: Optional[List[int]] = None
248
- extend_logprob_start_lens_cpu: Optional[List[int]] = None
249
- extend_logprob_pruned_lens_cpu: Optional[List[int]] = None
250
- top_logprobs_nums: Optional[List[int]] = None
251
- extend_input_logprob_token_ids_gpu: Optional[torch.Tensor] = None
252
- token_ids_logprobs: Optional[List[List[int]]] = None
253
-
254
- # logits and logprobs post processing
255
- temperature: torch.Tensor = None
256
- top_p: torch.Tensor = None
257
-
258
- # DP attention metadata. Not needed when DP attention is not used.
259
- # Number of tokens in the request.
260
- global_num_tokens_gpu: Optional[torch.Tensor] = None
261
- # The start position of local hidden states.
262
- dp_local_start_pos: Optional[torch.Tensor] = None
263
- dp_local_num_tokens: Optional[torch.Tensor] = None
264
- global_dp_buffer_len: Optional[int] = None
265
- # Number of tokens to sample per DP rank
266
- global_num_tokens_for_logprob_cpu: Optional[torch.Tensor] = None
267
- global_num_tokens_for_logprob_gpu: Optional[torch.Tensor] = None
268
- # The gather mode for DP attention
269
- dp_padding_mode: Optional[DpPaddingMode] = None
270
-
271
- # Whether this batch is prefill-only (no token generation needed)
272
- is_prefill_only: bool = False
273
-
274
- mm_input_embeds: Optional[torch.Tensor] = None
275
-
276
- # DRAFT_EXTEND_V2: when set, lm_head runs only on these rows (see
277
- # EagleDraftExtendInput.select_index).
278
- draft_extend_select_index: Optional[torch.Tensor] = None
279
-
280
- @classmethod
281
- def from_forward_batch(cls, forward_batch: ForwardBatch):
282
- if (
283
- forward_batch.forward_mode.is_extend()
284
- and forward_batch.return_logprob
285
- and not forward_batch.forward_mode.is_target_verify()
286
- ):
287
- extend_return_top_logprob = any(
288
- x > 0 for x in forward_batch.top_logprobs_nums
289
- )
290
- extend_token_ids_logprob = any(
291
- x is not None for x in forward_batch.token_ids_logprobs
292
- )
293
- extend_return_logprob = False
294
- extend_logprob_pruned_lens_cpu = []
295
- for extend_len, start_len in zip(
296
- forward_batch.extend_seq_lens_cpu,
297
- forward_batch.extend_logprob_start_lens_cpu,
298
- ):
299
- if extend_len - start_len > 0:
300
- extend_return_logprob = True
301
- extend_logprob_pruned_lens_cpu.append(extend_len - start_len)
302
- else:
303
- extend_return_logprob = extend_return_top_logprob = (
304
- extend_token_ids_logprob
305
- ) = extend_logprob_pruned_lens_cpu = False
306
-
307
- if forward_batch.forward_mode.is_draft_extend_v2():
308
- draft_extend_select_index = forward_batch.spec_info.select_index
309
- else:
310
- draft_extend_select_index = None
311
-
312
- return cls(
313
- forward_mode=forward_batch.forward_mode,
314
- capture_hidden_mode=forward_batch.capture_hidden_mode,
315
- next_token_logits_buffer=forward_batch.next_token_logits_buffer,
316
- extend_return_logprob=extend_return_logprob,
317
- extend_return_top_logprob=extend_return_top_logprob,
318
- extend_token_ids_logprob=extend_token_ids_logprob,
319
- extend_seq_lens=forward_batch.extend_seq_lens,
320
- extend_seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
321
- extend_logprob_start_lens_cpu=forward_batch.extend_logprob_start_lens_cpu,
322
- extend_logprob_pruned_lens_cpu=extend_logprob_pruned_lens_cpu,
323
- top_logprobs_nums=forward_batch.top_logprobs_nums,
324
- token_ids_logprobs=forward_batch.token_ids_logprobs,
325
- extend_input_logprob_token_ids_gpu=forward_batch.extend_input_logprob_token_ids_gpu,
326
- is_prefill_only=forward_batch.is_prefill_only,
327
- global_num_tokens_gpu=forward_batch.global_num_tokens_gpu,
328
- dp_local_start_pos=forward_batch.dp_local_start_pos,
329
- dp_local_num_tokens=forward_batch.dp_local_num_tokens,
330
- global_dp_buffer_len=forward_batch.global_dp_buffer_len,
331
- global_num_tokens_for_logprob_cpu=forward_batch.global_num_tokens_for_logprob_cpu,
332
- global_num_tokens_for_logprob_gpu=forward_batch.global_num_tokens_for_logprob_gpu,
333
- dp_padding_mode=DpPaddingMode.SUM_LEN,
334
- mm_input_embeds=forward_batch.mm_input_embeds,
335
- draft_extend_select_index=draft_extend_select_index,
336
- )
337
-
338
- def compute_dp_attention_metadata(self):
339
- cumtokens = torch.cumsum(self.global_num_tokens_for_logprob_gpu, dim=0)
340
- dp_rank = get_parallel().attn_dp_rank
341
- if dp_rank == 0:
342
- dp_local_start_pos = torch.zeros_like(
343
- self.global_num_tokens_for_logprob_gpu[0]
344
- )
345
- else:
346
- dp_local_start_pos = cumtokens[dp_rank - 1]
347
-
348
- self.dp_local_start_pos = dp_local_start_pos
349
- self.dp_local_num_tokens = self.global_num_tokens_for_logprob_gpu[dp_rank]
350
-
351
- hidden_size = get_dp_hidden_size()
352
- dtype = get_dp_dtype()
353
- device = get_dp_device()
354
-
355
- if self.global_num_tokens_for_logprob_cpu is not None:
356
- # create a smaller buffer to reduce peak memory usage
357
- self.global_dp_buffer_len = sum(self.global_num_tokens_for_logprob_cpu)
358
- else:
359
- self.global_dp_buffer_len = self.global_dp_buffer_len
360
-
361
- self.gathered_buffer = torch.empty(
362
- (
363
- self.global_dp_buffer_len,
364
- hidden_size,
365
- ),
366
- dtype=dtype,
367
- device=device,
368
- )
369
-
370
-
371
- class LogitsProcessor(nn.Module):
372
- def __init__(
373
- self,
374
- config,
375
- skip_all_gather: bool = False,
376
- logit_scale: Optional[float] = None,
377
- return_full_logits: bool = False,
378
- ):
379
- super().__init__()
380
- self.config = config
381
- # Decision-only optional projection. Public API renormalizes selected labels.
382
- import os, json
383
- self.jev_selected_ids = None
384
- if os.getenv("JEV_SELECTED_HEAD", "0") == "1":
385
- if get_parallel().tp_size != 1:
386
- raise ValueError("Selected decision head requires TP1")
387
- with open("/jev-label-token-ids.json") as label_file:
388
- self.jev_selected_ids = json.load(label_file)
389
- self.jev_head_cache = None
390
- self.vocab_size = config.vocab_size
391
- self.logit_scale = logit_scale
392
- self.use_attn_tp_group = get_parallel().enable_dp_lm_head
393
- self.use_tp_lm_head_all_to_all = get_parallel().enable_tp_lm_head_all_to_all
394
- self.use_fp32_lm_head = get_exec().features.enable_fp32_lm_head
395
- if self.use_attn_tp_group:
396
- self.attn_tp_size = get_parallel().attn_tp_size
397
- self.do_tensor_parallel_all_gather = (
398
- not skip_all_gather and self.attn_tp_size > 1
399
- )
400
- self.do_tensor_parallel_all_gather_dp_attn = False
401
- else:
402
- self.do_tensor_parallel_all_gather = (
403
- not skip_all_gather and get_parallel().tp_size > 1
404
- )
405
- self.do_tensor_parallel_all_gather_dp_attn = (
406
- self.do_tensor_parallel_all_gather and get_parallel().attn_dp_size != 1
407
- )
408
- self.final_logit_softcapping = getattr(
409
- self.config, "final_logit_softcapping", None
410
- )
411
- if (
412
- self.final_logit_softcapping is not None
413
- and self.final_logit_softcapping < 0
414
- ):
415
- self.final_logit_softcapping = None
416
-
417
- self.return_full_logits = return_full_logits
418
- self.enable_mis = get_exec().features.enable_mis
419
- self.rl_on_policy_target = get_exec().deterministic.rl_on_policy_target
420
-
421
- self._logits_gatherer = triton_symm_mem_ag.MultimemAllGatherer(
422
- max_tokens=triton_symm_mem_ag.recommended_max_tokens(
423
- include_prefill=False, floor=128
424
- ),
425
- enabled=self.do_tensor_parallel_all_gather and not self.use_attn_tp_group,
426
- skip_entry_sync=True,
427
- )
428
-
429
- self.input_logprob_processor = InputLogprobProcessor()
430
-
431
- def forward(
432
- self,
433
- input_ids,
434
- hidden_states,
435
- lm_head: VocabParallelEmbedding,
436
- logits_metadata: Union[LogitsMetadata, ForwardBatch],
437
- aux_hidden_states: Optional[AuxHiddenStates] = None,
438
- hidden_states_before_norm: Optional[torch.Tensor] = None,
439
- ) -> LogitsProcessorOutput:
440
- # Extract MIS indices before ForwardBatch → LogitsMetadata conversion
441
- multi_item_delimiter_indices = None
442
- if isinstance(logits_metadata, ForwardBatch):
443
- multi_item_delimiter_indices = logits_metadata.multi_item_delimiter_indices
444
- logits_metadata = LogitsMetadata.from_forward_batch(logits_metadata)
445
-
446
- # Autotune dummy run discards this output. `is False` not `not`: None
447
- # means no autotune pass, which must not skip. Placed before the MIS /
448
- # DLLM / common dispatch so all three LM-head paths are skipped.
449
- if _autotune_run_lm_head is False:
450
- return LogitsProcessorOutput(next_token_logits=None)
451
-
452
- # Under LayerNorm SP the decoder loop leaves these sequence-sharded; undo
453
- # that before the LM head, which must not participate.
454
- hidden_states, hidden_states_before_norm = layernorm_sp.maybe_exit_gather(
455
- hidden_states=hidden_states,
456
- hidden_states_before_norm=hidden_states_before_norm,
457
- input_ids=input_ids,
458
- forward_mode=logits_metadata.forward_mode,
459
- )
460
-
461
- # Multi-item scoring only for prefill-only requests with pre-computed indices.
462
- if multi_item_delimiter_indices is not None and logits_metadata.is_prefill_only:
463
- return self.compute_logprobs_for_multi_item_scoring(
464
- input_ids,
465
- hidden_states,
466
- lm_head,
467
- logits_metadata,
468
- multi_item_delimiter_indices,
469
- )
470
-
471
- # Diffusion LLM only.
472
- if logits_metadata.forward_mode.is_dllm_extend():
473
- return self._get_dllm_logits(hidden_states, lm_head, logits_metadata)
474
-
475
- # Get the last hidden states and last logits for the next token prediction
476
- (
477
- pruned_states,
478
- pruned_states_before_norm,
479
- aux_pruned_states,
480
- sample_indices,
481
- input_logprob_indices,
482
- token_to_seq_idx,
483
- ) = self._get_pruned_states(
484
- hidden_states,
485
- hidden_states_before_norm,
486
- aux_hidden_states,
487
- logits_metadata,
488
- )
489
-
490
- hidden_states_to_store = self._get_hidden_states_to_store(
491
- hidden_states,
492
- hidden_states_before_norm,
493
- aux_hidden_states,
494
- pruned_states,
495
- pruned_states_before_norm,
496
- aux_pruned_states,
497
- sample_indices,
498
- logits_metadata,
499
- )
500
- del hidden_states
501
-
502
- if not logits_metadata.extend_return_logprob:
503
- # Compute logits for both input and sampled tokens.
504
- logits = self._get_logits(pruned_states, lm_head, logits_metadata)
505
- sampled_logits = (
506
- logits[sample_indices] if sample_indices is not None else logits
507
- )
508
-
509
- # Decode mode or extend mode without return_logprob.
510
- return LogitsProcessorOutput(
511
- next_token_logits=sampled_logits,
512
- hidden_states=hidden_states_to_store,
513
- mm_input_embeds=logits_metadata.mm_input_embeds,
514
- )
515
-
516
- logprobs_result, sampled_logits = self.input_logprob_processor.forward(
517
- pruned_states=pruned_states,
518
- sample_indices=sample_indices,
519
- input_logprob_indices=input_logprob_indices,
520
- token_to_seq_idx=token_to_seq_idx,
521
- lm_head=lm_head,
522
- get_logits_fn=self._get_logits,
523
- logits_metadata=logits_metadata,
524
- skip_chunking_for_dp_attn=self.do_tensor_parallel_all_gather_dp_attn,
525
- )
526
-
527
- logits_output = LogitsProcessorOutput(
528
- next_token_logits=sampled_logits,
529
- hidden_states=hidden_states_to_store,
530
- mm_input_embeds=logits_metadata.mm_input_embeds,
531
- )
532
- logprobs_result.write_input_to(logits_output)
533
- return logits_output
534
-
535
- def _get_pruned_states(
536
- self,
537
- hidden_states: torch.Tensor,
538
- hidden_states_before_norm: Optional[torch.Tensor],
539
- aux_hidden_states: Optional[AuxHiddenStates],
540
- logits_metadata: LogitsMetadata,
541
- ):
542
- pruned_states_before_norm: Optional[torch.Tensor] = None
543
- aux_pruned_states = None
544
- token_to_seq_idx = []
545
-
546
- if (
547
- logits_metadata.forward_mode.is_decode_or_idle()
548
- or logits_metadata.forward_mode.is_target_verify()
549
- or logits_metadata.forward_mode.is_draft_extend_v2()
550
- ):
551
- if logits_metadata.draft_extend_select_index is not None:
552
- # Only next_token_logits narrows to [bs, vocab]; the
553
- # FULL-capture hidden stays unpruned.
554
- pruned_states = hidden_states[logits_metadata.draft_extend_select_index]
555
- else:
556
- pruned_states = hidden_states
557
- pruned_states_before_norm = hidden_states_before_norm
558
- if aux_hidden_states is not None:
559
- aux_pruned_states = (
560
- aux_hidden_states
561
- if isinstance(aux_hidden_states, torch.Tensor)
562
- else [hidden for hidden in aux_hidden_states]
563
- )
564
- sample_indices = None
565
- input_logprob_indices = None
566
-
567
- elif (
568
- logits_metadata.forward_mode.is_extend()
569
- and not logits_metadata.extend_return_logprob
570
- ):
571
- # Prefill without input logprobs.
572
- last_index = torch.cumsum(logits_metadata.extend_seq_lens, dim=0) - 1
573
- pruned_states = hidden_states[last_index]
574
- if hidden_states_before_norm is not None:
575
- pruned_states_before_norm = hidden_states_before_norm[last_index]
576
- if aux_hidden_states is not None:
577
- aux_pruned_states = (
578
- aux_hidden_states[last_index]
579
- if isinstance(aux_hidden_states, torch.Tensor)
580
- else [hidden[last_index] for hidden in aux_hidden_states]
581
- )
582
- sample_indices = None
583
- input_logprob_indices = None
584
- else:
585
- # Prefill with input logprobs.
586
- # Find 4 different indices.
587
- # 1. pruned_states: hidden states that we want logprobs from.
588
- # 2. sample_indices: Indices that have sampled tokens.
589
- # 3. input_logprob_indices: Indices that have input logprob tokens.
590
- # 4. token_to_seq_idx: map each token to its sequence index
591
- #
592
- # Example
593
- # -------
594
- # Suppose a batch (flattened by sequence):
595
- # [t00, t01, t02, t03, t10, t11, t12, t13, t14, t20, t21, t22, t23, t24, t25]
596
- # extend_seq_lens_cpu = [4, 5, 6]
597
- # extend_logprob_start_lens_cpu = [0, 5, 3]
598
- #
599
- # Then, the indices are:
600
- # pruned_states -> [t00, t01, t02, t03, t14, t23, t24, t25]
601
- # sample_indices -> [3, 4, 7]
602
- # input_logprob_indices -> [0, 1, 2, 3, 5, 6, 7]
603
- # token_to_seq_idx -> [0, 0, 0, 0, 1, 2, 2, 2]
604
- #
605
- # If chunk is enabled and chunk_size = 3, the chunks will be computed in a chunked manner:
606
- # [t00, t01, t02], [t03, t14, t23], [t24, t25]
607
-
608
- sample_index_pt = -1
609
- sample_indices = []
610
- input_logprob_indices_pt = 0
611
- input_logprob_indices = []
612
- pt, pruned_states_list, pruned_states_before_norm_list = 0, [], []
613
- is_packed_aux_hidden_states = isinstance(aux_hidden_states, torch.Tensor)
614
- aux_pruned_states_lists = None
615
- if aux_hidden_states is not None:
616
- aux_pruned_states_lists = (
617
- []
618
- if is_packed_aux_hidden_states
619
- else [[] for _ in aux_hidden_states]
620
- )
621
-
622
- for idx, (extend_logprob_start_len, extend_len) in enumerate(
623
- zip(
624
- logits_metadata.extend_logprob_start_lens_cpu,
625
- logits_metadata.extend_seq_lens_cpu,
626
- )
627
- ):
628
- # It can happen in chunked prefill. We still need to sample 1 token,
629
- # But we don't want to include it in input logprob.
630
- if extend_len == extend_logprob_start_len:
631
- start_len = extend_logprob_start_len - 1
632
- else:
633
- start_len = extend_logprob_start_len
634
-
635
- # We always need at least 1 token to sample because that's required
636
- # by a caller.
637
- assert extend_len > start_len
638
- pruned_states_list.append(
639
- hidden_states[pt + start_len : pt + extend_len]
640
- )
641
- if hidden_states_before_norm is not None:
642
- pruned_states_before_norm_list.append(
643
- hidden_states_before_norm[pt + start_len : pt + extend_len]
644
- )
645
- if aux_pruned_states_lists is not None:
646
- if is_packed_aux_hidden_states:
647
- aux_pruned_states_lists.append(
648
- aux_hidden_states[pt + start_len : pt + extend_len]
649
- )
650
- else:
651
- for j, hidden in enumerate(aux_hidden_states):
652
- aux_pruned_states_lists[j].append(
653
- hidden[pt + start_len : pt + extend_len]
654
- )
655
- # Map each token to its sequence index, for chunked computation
656
- # of input logprobs
657
- token_to_seq_idx.extend([idx] * (extend_len - start_len))
658
- pt += extend_len
659
- sample_index_pt += extend_len - start_len
660
- sample_indices.append(sample_index_pt)
661
- input_logprob_indices.extend(
662
- [
663
- input_logprob_indices_pt + i
664
- for i in range(extend_len - extend_logprob_start_len)
665
- ]
666
- )
667
- input_logprob_indices_pt += extend_len - start_len
668
-
669
- pruned_states = torch.cat(pruned_states_list)
670
- if hidden_states_before_norm is not None:
671
- pruned_states_before_norm = torch.cat(pruned_states_before_norm_list)
672
- if aux_pruned_states_lists is not None:
673
- aux_pruned_states = (
674
- torch.cat(aux_pruned_states_lists)
675
- if is_packed_aux_hidden_states
676
- else [torch.cat(lst) for lst in aux_pruned_states_lists]
677
- )
678
-
679
- # Build the index tensors via pinned host memory + non-blocking H2D
680
- # so the small copy doesn't drain the stream.
681
- sample_indices = torch.tensor(
682
- sample_indices,
683
- dtype=torch.int64,
684
- pin_memory=is_pin_memory_available(),
685
- ).to(pruned_states.device, non_blocking=True)
686
- input_logprob_indices = torch.tensor(
687
- input_logprob_indices,
688
- dtype=torch.int64,
689
- pin_memory=is_pin_memory_available(),
690
- ).to(pruned_states.device, non_blocking=True)
691
-
692
- return (
693
- pruned_states,
694
- pruned_states_before_norm,
695
- aux_pruned_states,
696
- sample_indices,
697
- input_logprob_indices,
698
- token_to_seq_idx,
699
- )
700
-
701
- def _get_hidden_states_to_store(
702
- self,
703
- hidden_states: torch.Tensor,
704
- hidden_states_before_norm: Optional[torch.Tensor],
705
- aux_hidden_states: Optional[AuxHiddenStates],
706
- pruned_states: torch.Tensor,
707
- pruned_states_before_norm: Optional[torch.Tensor],
708
- aux_pruned_states: Optional[AuxHiddenStates],
709
- sample_indices: Optional[torch.Tensor],
710
- logits_metadata: LogitsMetadata,
711
- ) -> Optional[torch.Tensor]:
712
- hidden_states_to_store: Optional[torch.Tensor] = None
713
- hidden_states_to_store_before_norm: Optional[torch.Tensor] = None
714
- if logits_metadata.capture_hidden_mode.need_capture():
715
- if logits_metadata.capture_hidden_mode.is_full():
716
- if aux_hidden_states is not None:
717
- hidden_states_to_store = pack_aux_hidden_states(aux_hidden_states)
718
- else:
719
- hidden_states_to_store = hidden_states
720
- hidden_states_to_store_before_norm = hidden_states_before_norm
721
- elif logits_metadata.capture_hidden_mode.is_last():
722
- # Get the last token hidden states. If sample_indices is None,
723
- # pruned states only contain the last tokens already.
724
- if aux_hidden_states is not None:
725
- assert aux_pruned_states is not None
726
- aux_pruned_states = pack_aux_hidden_states(aux_pruned_states)
727
- hidden_states_to_store = (
728
- aux_pruned_states[sample_indices]
729
- if sample_indices is not None
730
- else aux_pruned_states
731
- )
732
- else:
733
- hidden_states_to_store = (
734
- pruned_states[sample_indices]
735
- if sample_indices is not None
736
- else pruned_states
737
- )
738
- if hidden_states_before_norm is not None:
739
- hidden_states_to_store_before_norm = (
740
- pruned_states_before_norm[sample_indices]
741
- if sample_indices is not None
742
- else pruned_states_before_norm
743
- )
744
- else:
745
- assert False, "Should never reach"
746
-
747
- if hidden_states_to_store_before_norm is not None:
748
- # NOTE: when hidden_states_before_norm is provided, we always
749
- # prefer to return it.
750
- hidden_states_to_store = hidden_states_to_store_before_norm
751
-
752
- return hidden_states_to_store
753
-
754
- def _get_logits(
755
- self,
756
- hidden_states: torch.Tensor,
757
- lm_head: VocabParallelEmbedding,
758
- logits_metadata: LogitsMetadata,
759
- embedding_bias: Optional[torch.Tensor] = None,
760
- use_logits_buffer: bool = True,
761
- ) -> torch.Tensor:
762
- """Get logits from hidden_states.
763
-
764
- If sampled_logits_only is True, it means hidden_states only contain the
765
- last position (e.g., extend without input logprobs). The caller should
766
- guarantee the given hidden_states follow this constraint.
767
- """
768
- _trace_e2e_logits(
769
- "get_logits_enter",
770
- hidden_shape=tuple(hidden_states.shape),
771
- dp_gather=self.do_tensor_parallel_all_gather_dp_attn,
772
- tp_gather=self.do_tensor_parallel_all_gather,
773
- )
774
- hidden_states, local_hidden_states = self._gather_dp_attn_hidden_states(
775
- hidden_states, logits_metadata
776
- )
777
- _trace_e2e_logits(
778
- "dp_hidden_gather_returned",
779
- global_shape=tuple(hidden_states.shape),
780
- local_shape=tuple(local_hidden_states.shape),
781
- )
782
-
783
- if envs.SGLANG_TRACE_LOGITS_E2E_SYNC.get():
784
- _trace_e2e_logits("pre_lm_head_sync_enter")
785
- torch.cuda.synchronize()
786
- _trace_e2e_logits("pre_lm_head_sync_returned")
787
-
788
- _trace_e2e_logits("lm_head_enter", hidden_shape=tuple(hidden_states.shape))
789
- logits = self._compute_lm_head(hidden_states, lm_head, embedding_bias)
790
- _trace_e2e_logits("lm_head_returned", logits_shape=tuple(logits.shape))
791
- if envs.SGLANG_TRACE_LOGITS_E2E_SYNC.get():
792
- _trace_e2e_logits("post_lm_head_sync_enter")
793
- torch.cuda.synchronize()
794
- _trace_e2e_logits("post_lm_head_sync_returned")
795
-
796
- if self.logit_scale is not None:
797
- logits.mul_(self.logit_scale)
798
-
799
- used_tp_lm_head_all_to_all = False
800
- if self.do_tensor_parallel_all_gather:
801
- _trace_e2e_logits(
802
- "tp_logits_gather_enter", logits_shape=tuple(logits.shape)
803
- )
804
- if self.use_attn_tp_group:
805
- logits = self._gather_attn_tp_logits(logits)
806
- elif self._can_use_tp_lm_head_all_to_all(
807
- logits, local_hidden_states, lm_head, logits_metadata
808
- ):
809
- logits = self._tp_lm_head_all_to_all(logits)
810
- used_tp_lm_head_all_to_all = True
811
- else:
812
- logits = self._logits_gatherer(logits)
813
- _trace_e2e_logits(
814
- "tp_logits_gather_returned", logits_shape=tuple(logits.shape)
815
- )
816
-
817
- if not used_tp_lm_head_all_to_all:
818
- _trace_e2e_logits(
819
- "dp_logits_scatter_enter", logits_shape=tuple(logits.shape)
820
- )
821
- logits = self._scatter_dp_attn_logits(
822
- logits, local_hidden_states, logits_metadata
823
- )
824
- _trace_e2e_logits(
825
- "dp_logits_scatter_returned", logits_shape=tuple(logits.shape)
826
- )
827
-
828
- logits = self._copy_logits_to_buffer(
829
- logits, logits_metadata, use_buffer=use_logits_buffer
830
- )
831
-
832
- if self.final_logit_softcapping:
833
- if not (_is_npu or _is_cpu):
834
- fused_softcap(logits, self.final_logit_softcapping)
835
- else:
836
- logits = self.final_logit_softcapping * torch.tanh(
837
- logits / self.final_logit_softcapping
838
- )
839
-
840
- return logits
841
-
842
- def _compute_lm_head(
843
- self,
844
- hidden_states: torch.Tensor,
845
- lm_head: VocabParallelEmbedding,
846
- embedding_bias: Optional[torch.Tensor] = None,
847
- ) -> torch.Tensor:
848
- if self.jev_selected_ids is not None:
849
- if not hasattr(lm_head, "weight") or lm_head.weight.dtype not in (torch.bfloat16, torch.float16, torch.float32) or hasattr(lm_head, "set_lora"):
850
- raise ValueError("Selected head requires a frozen unquantized output projection")
851
- if embedding_bias is not None or self.final_logit_softcapping:
852
- raise ValueError("Selected head does not support output bias or softcapping")
853
- if self.jev_head_cache is None:
854
- ids = torch.tensor(self.jev_selected_ids, device=lm_head.weight.device, dtype=torch.long)
855
- weight = lm_head.weight.index_select(0, ids).contiguous()
856
- self.jev_head_cache = (ids, weight)
857
- ids, weight = self.jev_head_cache
858
- if self.use_fp32_lm_head:
859
- selected = torch.matmul(hidden_states.float(), weight.float().T)
860
- else:
861
- selected = torch.matmul(hidden_states.to(weight.dtype), weight.T)
862
- logits = torch.full((hidden_states.shape[0], self.vocab_size), -float("inf"), device=hidden_states.device, dtype=selected.dtype)
863
- return logits.index_copy_(1, ids, selected)
864
- quant_method = getattr(lm_head, "quant_method", None)
865
- if hasattr(lm_head, "set_lora") and hasattr(lm_head, "apply_lora"):
866
- # This is a LoRA-wrapped module, use its forward method
867
- logits = lm_head(hidden_states)
868
- elif should_apply_lm_head_quant_method(lm_head, quant_method):
869
- logits = quant_method.apply(lm_head, hidden_states, embedding_bias)
870
- elif hasattr(lm_head, "weight"):
871
- # Normal linear layer
872
- if self.use_fp32_lm_head:
873
- # Avoid materializing FP32 copies for same-dtype CUDA FP16/BF16
874
- # inputs. Retain explicit FP32 casts for unsupported devices or
875
- # dtype combinations.
876
- use_mm_out_dtype = (
877
- hidden_states.is_cuda
878
- and hidden_states.dtype == lm_head.weight.dtype
879
- and hidden_states.dtype in (torch.float16, torch.bfloat16)
880
- )
881
- if use_mm_out_dtype:
882
- logits = torch.mm(
883
- hidden_states,
884
- lm_head.weight.T,
885
- out_dtype=torch.float32,
886
- )
887
- else:
888
- logits = torch.matmul(
889
- hidden_states.to(torch.float32),
890
- lm_head.weight.to(torch.float32).T,
891
- )
892
- elif use_intel_amx_backend(lm_head):
893
- logits = torch.ops.sgl_kernel.weight_packed_linear(
894
- hidden_states.to(lm_head.weight.dtype),
895
- lm_head.weight,
896
- None, # bias
897
- True, # is_vnni
898
- )
899
- elif self.rl_on_policy_target is not None:
900
- # Due to tie-weight, we may not be able to change lm_head's weight dtype
901
- logits = torch.matmul(
902
- hidden_states.bfloat16(), lm_head.weight.T.bfloat16()
903
- )
904
- else:
905
- logits = torch.matmul(
906
- hidden_states.to(lm_head.weight.dtype), lm_head.weight.T
907
- )
908
- else:
909
- # GGUF models
910
- # TODO: use weight_packed_linear for GGUF models
911
- if self.use_fp32_lm_head:
912
- with torch.cuda.amp.autocast(enabled=False):
913
- logits = lm_head.quant_method.apply(
914
- lm_head, hidden_states.to(torch.float32), embedding_bias
915
- )
916
- else:
917
- logits = lm_head.quant_method.apply(
918
- lm_head, hidden_states, embedding_bias
919
- )
920
- return logits
921
-
922
- def _gather_dp_attn_hidden_states(
923
- self, hidden_states: torch.Tensor, logits_metadata: LogitsMetadata
924
- ) -> Tuple[torch.Tensor, torch.Tensor]:
925
- if self.do_tensor_parallel_all_gather_dp_attn:
926
- _trace_e2e_logits(
927
- "dp_metadata_enter",
928
- local_shape=tuple(hidden_states.shape),
929
- global_counts_cpu=logits_metadata.global_num_tokens_for_logprob_cpu,
930
- )
931
- logits_metadata.compute_dp_attention_metadata()
932
- _trace_e2e_logits(
933
- "dp_metadata_returned",
934
- buffer_shape=tuple(logits_metadata.gathered_buffer.shape),
935
- local_start=logits_metadata.dp_local_start_pos,
936
- local_tokens=logits_metadata.dp_local_num_tokens,
937
- )
938
- local_hidden_states = hidden_states
939
- hidden_states = logits_metadata.gathered_buffer
940
- _trace_e2e_logits(
941
- "dp_hidden_gather_enter",
942
- global_shape=tuple(hidden_states.shape),
943
- local_shape=tuple(local_hidden_states.shape),
944
- )
945
- dp_gather_replicate(hidden_states, local_hidden_states, logits_metadata)
946
- _trace_e2e_logits("dp_hidden_gather_collective_returned")
947
- return hidden_states, local_hidden_states
948
- return hidden_states, hidden_states
949
-
950
- def _gather_attn_tp_logits(self, logits: torch.Tensor) -> torch.Tensor:
951
- if self.vocab_size % self.attn_tp_size == 0:
952
- global_logits = torch.empty(
953
- (
954
- self.attn_tp_size,
955
- logits.shape[0],
956
- self.vocab_size // self.attn_tp_size,
957
- ),
958
- device=logits.device,
959
- dtype=logits.dtype,
960
- )
961
- attn_tp_all_gather_into_tensor(global_logits, logits)
962
- global_logits = global_logits.permute(1, 0, 2).reshape(
963
- logits.shape[0], self.vocab_size
964
- )
965
- else:
966
- global_logits = torch.empty(
967
- (self.vocab_size, logits.shape[0]),
968
- device=logits.device,
969
- dtype=logits.dtype,
970
- )
971
- global_logits = global_logits.T
972
- attn_tp_all_gather(
973
- list(global_logits.tensor_split(self.attn_tp_size, dim=-1)),
974
- logits,
975
- )
976
- return global_logits
977
-
978
- def _can_use_tp_lm_head_all_to_all(
979
- self,
980
- logits: torch.Tensor,
981
- local_hidden_states: torch.Tensor,
982
- lm_head: VocabParallelEmbedding,
983
- logits_metadata: LogitsMetadata,
984
- ) -> bool:
985
- if not self.use_tp_lm_head_all_to_all:
986
- return False
987
-
988
- tp_size = get_parallel().tp_size
989
- base_lm_head = getattr(lm_head, "base_layer", lm_head)
990
- if getattr(base_lm_head, "tp_size", None) != tp_size:
991
- # Tied embeddings may be replicated across DP ranks (tp_size=1),
992
- # even though the logits processor runs in a larger global TP
993
- # group. Such logits are full-vocabulary rather than TP shards and
994
- # therefore do not satisfy the all-to-all layout contract.
995
- return False
996
-
997
- # Every participant must make the same collective choice. Decode CUDA
998
- # graphs omit CPU counts and fill every GPU count with the same padded
999
- # bucket size. Eager batches carry the same global CPU count list on
1000
- # every rank, so they are also safe when all entries are equal.
1001
- global_counts_cpu = logits_metadata.global_num_tokens_for_logprob_cpu
1002
- is_equal_padded_graph_layout = global_counts_cpu is None and (
1003
- logits_metadata.global_num_tokens_for_logprob_gpu is not None
1004
- )
1005
- is_equal_eager_layout = (
1006
- global_counts_cpu is not None
1007
- and len(global_counts_cpu) == tp_size
1008
- and len(global_counts_cpu) > 0
1009
- and all(count == global_counts_cpu[0] for count in global_counts_cpu)
1010
- )
1011
- if not (is_equal_padded_graph_layout or is_equal_eager_layout):
1012
- return False
1013
-
1014
- local_rows = local_hidden_states.shape[0]
1015
- return local_rows > 0 and logits.shape[0] == local_rows * tp_size
1016
-
1017
- def _tp_lm_head_all_to_all(self, logits: torch.Tensor) -> torch.Tensor:
1018
- """Exchange only the row block owned by each destination DP rank."""
1019
- logits = logits.contiguous()
1020
- all_to_all_output = torch.empty_like(logits)
1021
- get_tp_group().all_to_all_single(all_to_all_output.view(-1), logits.view(-1))
1022
- return _reassemble_tp_lm_head_all_to_all_output(
1023
- all_to_all_output, get_parallel().tp_size
1024
- )
1025
-
1026
- def _scatter_dp_attn_logits(
1027
- self,
1028
- logits: torch.Tensor,
1029
- local_hidden_states: torch.Tensor,
1030
- logits_metadata: LogitsMetadata,
1031
- ) -> torch.Tensor:
1032
- if self.do_tensor_parallel_all_gather_dp_attn:
1033
- global_logits = logits
1034
- logits = torch.empty(
1035
- (local_hidden_states.shape[0], global_logits.shape[1]),
1036
- device=global_logits.device,
1037
- dtype=global_logits.dtype,
1038
- )
1039
- dp_scatter(logits, global_logits, logits_metadata)
1040
- return logits
1041
-
1042
- def _copy_logits_to_buffer(
1043
- self,
1044
- logits: torch.Tensor,
1045
- logits_metadata: LogitsMetadata,
1046
- use_buffer: bool = True,
1047
- ) -> torch.Tensor:
1048
- logits_buffer = logits_metadata.next_token_logits_buffer if use_buffer else None
1049
- if logits.shape[-1] > self.vocab_size:
1050
- logits = logits[:, : self.vocab_size]
1051
- logits_width = logits.shape[-1]
1052
- # The shared logits buffer is keyed by vocab width and rows; skip it
1053
- # when this batch has a different logits shape than the graph buffer.
1054
- if logits_buffer is not None and tuple(logits_buffer.shape) == tuple(
1055
- logits.shape
1056
- ):
1057
- assert logits_buffer.dtype == torch.float
1058
- logits_buffer.copy_(logits)
1059
- logits = logits_buffer
1060
- else:
1061
- logits = logits.float()
1062
- return logits
1063
-
1064
- def _get_dllm_logits(
1065
- self,
1066
- hidden_states: torch.Tensor,
1067
- lm_head: VocabParallelEmbedding,
1068
- logits_metadata: LogitsMetadata,
1069
- ) -> LogitsProcessorOutput:
1070
- assert self.return_full_logits
1071
- full_logits = self._get_logits(hidden_states, lm_head, logits_metadata)
1072
- return LogitsProcessorOutput(
1073
- full_logits=full_logits,
1074
- next_token_logits=None,
1075
- )
1076
-
1077
- def compute_logprobs_for_multi_item_scoring(
1078
- self,
1079
- input_ids,
1080
- hidden_states,
1081
- lm_head: VocabParallelEmbedding,
1082
- logits_metadata: Union[LogitsMetadata, ForwardBatch],
1083
- multi_item_delimiter_indices: List[torch.Tensor],
1084
- ):
1085
- """
1086
- Compute logprobs for multi-item scoring using pre-computed delimiter indices.
1087
-
1088
- Sequence format: Query<delimiter>Item1<delimiter>Item2<delimiter>...
1089
- Scoring positions: Extracts logprobs at positions before each <delimiter>
1090
-
1091
- Args:
1092
- input_ids: Input token IDs. Shape: [total_sequence_length].
1093
- hidden_states: Hidden states from the model. Shape: [sequence_length, hidden_dim].
1094
- lm_head: Language model head for computing logits.
1095
- logits_metadata: Metadata containing batch info and logprob specs.
1096
- multi_item_delimiter_indices: Pre-computed delimiter positions per request (CPU tensors).
1097
- """
1098
- # Compute positions just before each delimiter.
1099
- # Build offset-adjusted indices on CPU, then do a single CPU→GPU transfer.
1100
- device = input_ids.device
1101
- all_tensors = []
1102
- if logits_metadata.extend_seq_lens_cpu is not None:
1103
- offset = 0
1104
- for req_seq_len, indices_tensor in zip(
1105
- logits_metadata.extend_seq_lens_cpu, multi_item_delimiter_indices
1106
- ):
1107
- if len(indices_tensor) > 0:
1108
- # Note: if the first delimiter is at position 0 (empty query),
1109
- # indices - 1 wraps to -1. This is harmless — the first
1110
- # delimiter entry is always discarded by
1111
- # _process_multi_item_scoring_results.
1112
- all_tensors.append(indices_tensor + (offset - 1))
1113
- offset += req_seq_len
1114
- else:
1115
- all_tensors.append(multi_item_delimiter_indices[0] - 1)
1116
- multi_item_indices = torch.cat(all_tensors).to(device, non_blocking=True)
1117
-
1118
- # Extract hidden states at delimiter positions for multi-item scoring
1119
- sliced_hidden = hidden_states[multi_item_indices]
1120
-
1121
- sliced_logits = self._get_logits(sliced_hidden, lm_head, logits_metadata)
1122
- sliced_logprobs = torch.nn.functional.log_softmax(sliced_logits, dim=-1)
1123
-
1124
- # Initialize return values
1125
- input_token_ids_logprobs_val = []
1126
- input_token_ids_logprobs_idx = []
1127
- input_top_logprobs_val = None
1128
- input_top_logprobs_idx = None
1129
-
1130
- # Recalculate extend_logprob_pruned_lens_cpu to match delimiter counts per request
1131
- if (
1132
- logits_metadata.token_ids_logprobs
1133
- or logits_metadata.extend_return_top_logprob
1134
- ):
1135
- logits_metadata.extend_logprob_pruned_lens_cpu = [
1136
- len(t) for t in multi_item_delimiter_indices
1137
- ]
1138
-
1139
- # Get the logprobs of specified token ids
1140
- if logits_metadata.extend_token_ids_logprob:
1141
- (
1142
- input_token_ids_logprobs_val,
1143
- input_token_ids_logprobs_idx,
1144
- ) = get_token_ids_logprobs_raw(
1145
- sliced_logprobs,
1146
- logits_metadata.token_ids_logprobs,
1147
- stage=LogprobStage.PREFILL,
1148
- extend_logprob_pruned_lens_cpu=logits_metadata.extend_logprob_pruned_lens_cpu,
1149
- no_copy_to_cpu=True,
1150
- )
1151
-
1152
- # Get the logprob of top-k tokens
1153
- if logits_metadata.extend_return_top_logprob:
1154
- (
1155
- input_top_logprobs_val,
1156
- input_top_logprobs_idx,
1157
- ) = get_top_logprobs_raw(
1158
- sliced_logprobs,
1159
- logits_metadata.top_logprobs_nums,
1160
- stage=LogprobStage.PREFILL,
1161
- extend_logprob_pruned_lens_cpu=logits_metadata.extend_logprob_pruned_lens_cpu,
1162
- )
1163
-
1164
- # MIS scores come from input_token_ids_logprobs_val (label-token logprobs),
1165
- # not from per-position input_token_logprobs. However, the shared logprob
1166
- # pipeline (add_input_logprob_return_values) asserts input_token_logprobs is
1167
- # non-None, converts it to a tuple, slices it, and validates its length —
1168
- # all before score_request() ever sees the result. We can't set it to None
1169
- # without changing those shared asserts, so we fill with zeros to satisfy
1170
- # the pipeline. score_request() ignores this field entirely.
1171
- input_token_logprobs = torch.zeros(multi_item_indices.shape[0], device=device)
1172
-
1173
- return LogitsProcessorOutput(
1174
- next_token_logits=None,
1175
- input_token_logprobs=input_token_logprobs,
1176
- input_top_logprobs_val=input_top_logprobs_val,
1177
- input_top_logprobs_idx=input_top_logprobs_idx,
1178
- input_token_ids_logprobs_val=input_token_ids_logprobs_val,
1179
- input_token_ids_logprobs_idx=input_token_ids_logprobs_idx,
1180
- mm_input_embeds=logits_metadata.mm_input_embeds,
1181
- )
1182
-
1183
-
1184
- def _reassemble_tp_lm_head_all_to_all_output(
1185
- all_to_all_output: torch.Tensor, tp_size: int
1186
- ) -> torch.Tensor:
1187
- """Convert source-major all-to-all output to row-major full-vocab logits.
1188
-
1189
- Each source TP rank contributes ``[local_rows, vocab_shard]`` for this
1190
- destination DP rank. ``all_to_all_single`` concatenates those contributions
1191
- along dim 0, while the sampler expects the vocab shards concatenated along
1192
- dim 1.
1193
- """
1194
- assert all_to_all_output.shape[0] % tp_size == 0
1195
- local_rows = all_to_all_output.shape[0] // tp_size
1196
- vocab_shard = all_to_all_output.shape[1]
1197
- return (
1198
- all_to_all_output.view(tp_size, local_rows, vocab_shard)
1199
- .permute(1, 0, 2)
1200
- .reshape(local_rows, tp_size * vocab_shard)
1201
- )
1202
-
1203
-
1204
- def _has_lm_head_runtime_attrs(lm_head, attr_names: Tuple[str, ...]) -> bool:
1205
- return all(hasattr(lm_head, attr_name) for attr_name in attr_names)
1206
-
1207
-
1208
- def should_apply_lm_head_quant_method(lm_head, quant_method) -> bool:
1209
- if (
1210
- quant_method is None
1211
- or not hasattr(lm_head, "weight")
1212
- or not callable(getattr(quant_method, "apply", None))
1213
- ):
1214
- return False
1215
-
1216
- method_name = type(quant_method).__name__
1217
- if method_name in _UNQUANTIZED_LM_HEAD_METHODS:
1218
- return False
1219
-
1220
- # Some draft models share an unquantized target lm_head tensor while still
1221
- # carrying the draft model's stale ModelOpt quant_method. Only use the
1222
- # ModelOpt lm_head kernel when the runtime quantization state matches it.
1223
- if method_name == "ModelOptFp4LinearMethod":
1224
- if quant_method.quant_mode == "w4a16":
1225
- return lm_head.weight.dtype == torch.uint8 and _has_lm_head_runtime_attrs(
1226
- lm_head,
1227
- (
1228
- "weight_scale_interleaved",
1229
- "alpha",
1230
- "input_size_per_partition",
1231
- "output_size_per_partition",
1232
- ),
1233
- )
1234
- if lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
1235
- lm_head,
1236
- (
1237
- "weight_scale",
1238
- "weight_global_scale",
1239
- "workspace",
1240
- "input_size_per_partition",
1241
- "output_size_per_partition",
1242
- ),
1243
- ):
1244
- return True
1245
- return lm_head.weight.dtype == torch.uint8 and _has_lm_head_runtime_attrs(
1246
- lm_head,
1247
- (
1248
- "weight_scale_interleaved",
1249
- "alpha",
1250
- "input_scale_inv",
1251
- "input_size_per_partition",
1252
- "output_size_per_partition",
1253
- ),
1254
- )
1255
- if method_name == "ModelOptNvFp4A16LinearMethod":
1256
- return lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
1257
- lm_head,
1258
- (
1259
- "weight_scale",
1260
- "weight_global_scale",
1261
- "workspace",
1262
- "input_size_per_partition",
1263
- "output_size_per_partition",
1264
- ),
1265
- )
1266
- if method_name == "ModelOptFp8LinearMethod":
1267
- return (
1268
- lm_head.weight.dtype == torch.float8_e4m3fn
1269
- and _has_lm_head_runtime_attrs(lm_head, ("weight_scale", "input_scale"))
1270
- )
1271
-
1272
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runtime/patches/sglang-qwen35-text.py DELETED
@@ -1,298 +0,0 @@
1
- # Copyright 2025 Qwen Team
2
- # Copyright 2025 SGLang Team
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
14
- # ==============================================================================
15
-
16
- import logging
17
- from typing import Iterable, Optional, Set, Tuple, Union
18
-
19
- import torch
20
- from torch import nn
21
-
22
- from sglang.srt.distributed import get_pp_group
23
- from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
24
- from sglang.srt.layers.logits_processor import LogitsProcessor
25
- from sglang.srt.layers.quantization.base_config import QuantizationConfig
26
- from sglang.srt.layers.utils import PPMissingLayer
27
- from sglang.srt.layers.vocab_parallel_embedding import ParallelLMHead
28
- from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
29
- from sglang.srt.model_loader.weight_utils import default_weight_loader
30
- from sglang.srt.models import qwen3_5
31
- from sglang.srt.models.qwen2_moe import Qwen2MoeSparseMoeBlock
32
- from sglang.srt.runtime_context import get_parallel
33
- from sglang.srt.utils import LazyValue, add_prefix
34
-
35
- logger = logging.getLogger(__name__)
36
-
37
- _MODEL_PREFIX = "model."
38
-
39
-
40
- class Qwen3_5ForCausalLM(nn.Module):
41
- body_cls = qwen3_5.Qwen3_5ForCausalLM
42
-
43
- packed_modules_mapping = qwen3_5.Qwen3_5ForCausalLM.packed_modules_mapping
44
- supported_lora_modules = qwen3_5.Qwen3_5ForCausalLM.supported_lora_modules
45
-
46
- @classmethod
47
- def shared_experts_fusion_disable_reason(cls, hf_config, quant_config):
48
- # The body decides; it is handed this config and quantization verbatim.
49
- return cls.body_cls.shared_experts_fusion_disable_reason(
50
- hf_config, quant_config
51
- )
52
-
53
- def __init__(
54
- self,
55
- config,
56
- quant_config: Optional[QuantizationConfig] = None,
57
- prefix: str = "",
58
- ) -> None:
59
- super().__init__()
60
- self.config = config
61
- self.quant_config = quant_config
62
- self.pp_group = get_pp_group()
63
-
64
- if quant_config is not None and hasattr(quant_config, "packed_modules_mapping"):
65
- quant_config.packed_modules_mapping = self.packed_modules_mapping
66
-
67
- self.model = self.body_cls(
68
- config=config,
69
- quant_config=quant_config,
70
- prefix=add_prefix("model", prefix),
71
- )
72
-
73
- if self.pp_group.is_last_rank:
74
- if self.pp_group.world_size == 1 and config.tie_word_embeddings:
75
- self.lm_head = self.model.embed_tokens
76
- else:
77
- self.lm_head = ParallelLMHead(
78
- config.vocab_size,
79
- config.hidden_size,
80
- quant_config=quant_config,
81
- org_num_embeddings=config.vocab_size,
82
- prefix=add_prefix("lm_head", prefix),
83
- use_attn_tp_group=get_parallel().enable_dp_lm_head,
84
- )
85
- else:
86
- self.lm_head = PPMissingLayer()
87
-
88
- self.logits_processor = LogitsProcessor(config)
89
-
90
- # Text-only checkpoints retain mrope_section, but identical position rows
91
- # make its rotary embedding equivalent to 1-D RoPE.
92
- rope_config = getattr(config, "rope_parameters", None) or getattr(
93
- config, "rope_scaling", None
94
- )
95
- self.is_mrope_enabled = bool(rope_config) and "mrope_section" in rope_config
96
-
97
- self.capture_aux_hidden_states = False
98
-
99
- @property
100
- def start_layer(self) -> int:
101
- return self.model.start_layer
102
-
103
- @property
104
- def end_layer(self) -> int:
105
- return self.model.end_layer
106
-
107
- def get_input_embeddings(self) -> nn.Embedding:
108
- return self.model.embed_tokens
109
-
110
- def prepare_before_cuda_graph_capture(self, model_runner) -> None:
111
- """Forward model-owned warmup to the text backbone."""
112
- self.model.prepare_before_cuda_graph_capture(model_runner)
113
-
114
- def set_dflash_layers_to_capture(self, layer_ids: list[int]) -> None:
115
- if self.pp_group.world_size > 1:
116
- raise NotImplementedError("DFLASH/DSPARK aux hidden capture requires PP=1.")
117
- num_layers = len(self.model.layers)
118
- if sorted(set(layer_ids)) != list(layer_ids) or not all(
119
- 0 <= layer_id < num_layers - 1 for layer_id in layer_ids
120
- ):
121
- raise ValueError(
122
- "target_layer_ids must be unique, strictly increasing, and in "
123
- f"[0, {num_layers - 1}); got {layer_ids}"
124
- )
125
- self.capture_aux_hidden_states = True
126
- self.model.set_dflash_layers_to_capture(
127
- [layer_id + 1 for layer_id in layer_ids]
128
- )
129
-
130
- def get_embed_and_head(self):
131
- # PP splits embedding and lm_head across first/last stages; the draft keeps
132
- # its own copy of whichever half its stage cannot receive.
133
- embed = (
134
- None
135
- if isinstance(self.model.embed_tokens, PPMissingLayer)
136
- else self.model.embed_tokens.weight
137
- )
138
- head = None if isinstance(self.lm_head, PPMissingLayer) else self.lm_head.weight
139
- return embed, head
140
-
141
- def set_embed_and_head(self, embed, head):
142
- del self.model.embed_tokens.weight
143
- del self.lm_head.weight
144
- self.model.embed_tokens.weight = embed
145
- self.lm_head.weight = head
146
- torch.cuda.empty_cache()
147
- torch.cuda.synchronize()
148
-
149
- def set_dflash_layers_to_capture(self, layers_to_capture: list[int]):
150
- if not self.pp_group.is_last_rank:
151
- return
152
- if layers_to_capture is None:
153
- raise ValueError(
154
- "DFLASH requires explicit layer ids for aux hidden capture."
155
- )
156
- self.capture_aux_hidden_states = True
157
- self.model.set_dflash_layers_to_capture(layers_to_capture)
158
-
159
- @torch.no_grad()
160
- def forward(
161
- self,
162
- input_ids: torch.Tensor,
163
- positions: torch.Tensor,
164
- forward_batch: ForwardBatch,
165
- input_embeds: Optional[torch.Tensor] = None,
166
- pp_proxy_tensors: Optional[PPProxyTensors] = None,
167
- **kwargs,
168
- ) -> Union[torch.Tensor, PPProxyTensors]:
169
- if self.is_mrope_enabled:
170
- positions = forward_batch.mrope_positions
171
-
172
- hidden_states = self.model(
173
- input_ids,
174
- positions,
175
- forward_batch,
176
- input_embeds,
177
- pp_proxy_tensors=pp_proxy_tensors,
178
- )
179
-
180
- if not self.pp_group.is_last_rank:
181
- return hidden_states
182
-
183
- aux_hidden_states = None
184
- if isinstance(hidden_states, tuple):
185
- hidden_states, aux_hidden_states = hidden_states
186
-
187
- return self.logits_processor(
188
- input_ids, hidden_states, self.lm_head, forward_batch, aux_hidden_states
189
- )
190
-
191
- def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]) -> Set[str]:
192
- params_dict = dict(self.named_parameters())
193
- loaded_params: Set[str] = set()
194
-
195
- # Keep prefix stripping lazy: materializing mmap-backed checkpoint tensors
196
- # across model processes can exceed host memory before GPU copies finish.
197
- def portable_weights():
198
- # HF native export packs experts and shared gate/up matrices. Expand
199
- # lazily into the loader's per-expert format; no tensor values change.
200
- for name, tensor in weights:
201
- name = name.replace("model.language_model.", "model.")
202
- if ".mlp.experts.gate_up_proj" in name:
203
- stem = name.split(".mlp.experts.gate_up_proj")[0]
204
- for expert in range(tensor.shape[0]):
205
- gate, up = tensor[expert].chunk(2, dim=0)
206
- yield f"{stem}.mlp.experts.{expert}.gate_proj.weight", gate
207
- yield f"{stem}.mlp.experts.{expert}.up_proj.weight", up
208
- elif ".mlp.experts.down_proj" in name:
209
- stem = name.split(".mlp.experts.down_proj")[0]
210
- for expert in range(tensor.shape[0]):
211
- yield f"{stem}.mlp.experts.{expert}.down_proj.weight", tensor[expert]
212
- elif ".mlp.shared_expert.gate_up_proj.weight" in name:
213
- gate, up = tensor.chunk(2, dim=0)
214
- yield name.replace("gate_up_proj", "gate_proj"), gate
215
- yield name.replace("gate_up_proj", "up_proj"), up
216
- else:
217
- yield name, tensor
218
-
219
- def body_weights():
220
- fused_shared = max((getattr(layer.mlp, "num_fused_shared_experts", 0) for layer in self.model.layers), default=0)
221
- for name, loaded_weight in portable_weights():
222
- if fused_shared and ".mlp.shared_expert." in name:
223
- name = name.replace(".mlp.shared_expert.", f".mlp.experts.{self.config.num_experts}.")
224
- if name.startswith(_MODEL_PREFIX):
225
- yield name[len(_MODEL_PREFIX) :], loaded_weight
226
- elif name == "lm_head.weight":
227
- if self.config.tie_word_embeddings:
228
- continue
229
- if "lm_head.weight" not in params_dict:
230
- continue
231
- param = params_dict["lm_head.weight"]
232
- weight_loader = getattr(
233
- param, "weight_loader", default_weight_loader
234
- )
235
- weight_loader(param, loaded_weight)
236
- loaded_params.add("lm_head.weight")
237
-
238
- body_loaded = self.model.load_weights(body_weights())
239
- loaded_params.update(f"{_MODEL_PREFIX}{n}" for n in body_loaded)
240
-
241
- if self.config.tie_word_embeddings and self.pp_group.is_last_rank:
242
- loaded_params.add("lm_head.weight")
243
-
244
- missing = set(params_dict) - loaded_params
245
- if (self.quant_config is not None
246
- and self.quant_config.get_name() == "fp8"
247
- and not self.quant_config.is_checkpoint_fp8_serialized):
248
- # Online BF16->FP8 creates these scales itself. They are deliberately
249
- # absent from the source BF16 checkpoint; all model weights still
250
- # have to load. Do not apply this exception to serialized FP8.
251
- generated_suffixes = (
252
- ".mlp.experts.w13_weight_scale", ".mlp.experts.w2_weight_scale",
253
- ".mlp.experts.w13_weight_scale1", ".mlp.experts.w2_weight_scale1",
254
- ".attn.k_scale", ".attn.v_scale",
255
- )
256
- generated = {name for name in missing if name.endswith(generated_suffixes)}
257
- missing -= generated
258
- if missing:
259
- raise RuntimeError(f"Incomplete text checkpoint load: {sorted(missing)[:20]}")
260
- return loaded_params
261
-
262
-
263
- class Qwen3_5MoeForCausalLM(Qwen3_5ForCausalLM):
264
- body_cls = qwen3_5.Qwen3_5MoeForCausalLM
265
-
266
- packed_modules_mapping = qwen3_5.Qwen3_5MoeForCausalLM.packed_modules_mapping
267
- supported_lora_modules = qwen3_5.Qwen3_5MoeForCausalLM.supported_lora_modules
268
-
269
- def __init__(
270
- self,
271
- config,
272
- quant_config: Optional[QuantizationConfig] = None,
273
- prefix: str = "",
274
- ) -> None:
275
- super().__init__(config=config, quant_config=quant_config, prefix=prefix)
276
-
277
- self._routed_experts_weights_of_layer = LazyValue(
278
- lambda: {
279
- layer_id: layer.mlp.get_moe_weights()
280
- for layer_id, layer in enumerate(self.model.layers)
281
- if isinstance(layer.mlp, Qwen2MoeSparseMoeBlock)
282
- }
283
- )
284
-
285
- @property
286
- def routed_experts_weights_of_layer(self):
287
- return self._routed_experts_weights_of_layer.value
288
-
289
- @classmethod
290
- def get_model_config_for_expert_location(cls, config):
291
- return ModelConfigForExpertLocation(
292
- num_layers=config.num_hidden_layers,
293
- num_logical_experts=config.num_experts,
294
- num_groups=None,
295
- )
296
-
297
-
298
- EntryClass = [Qwen3_5MoeForCausalLM, Qwen3_5ForCausalLM]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runtime/patches/sglang-qwen35.py DELETED
The diff for this file is too large to render. See raw diff
 
serve.sh DELETED
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- model_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
- image='lmsysorg/sglang@sha256:a0ffcdd013af6d79c9b7c4348c42b14a79a48f6c43dd3ca0235ec3802da45f75'
6
- container_name=${NYX_CONTAINER_NAME:-nyx}
7
- port=${NYX_PORT:-30000}
8
- cache_dir=${NYX_CACHE_DIR:-/tmp/nyx-sglang-cache}
9
- memory_fraction=${NYX_MEM_FRACTION:-0.75}
10
-
11
- mkdir -p "$cache_dir/aiter-jit" "$cache_dir/sglang"
12
-
13
- exec docker run --rm --name "$container_name" \
14
- --device /dev/kfd --device /dev/dri --group-add video \
15
- --ipc=host -p "127.0.0.1:${port}:30000" \
16
- -e JEV_SELECTED_HEAD=0 \
17
- -e SGLANG_USE_AITER_UNIFIED_ATTN=0 \
18
- -e SGLANG_MAMBA_SSM_DTYPE=float32 \
19
- -e AITER_JIT_DIR=/aiter-jit \
20
- -v "$model_dir:/model:ro" \
21
- -v "$cache_dir/aiter-jit:/aiter-jit" \
22
- -v "$cache_dir/sglang:/root/.cache" \
23
- -v "$model_dir/runtime/label-token-ids.json:/jev-label-token-ids.json:ro" \
24
- -v "$model_dir/runtime/patches/sglang-logits-processor.py:/sgl-workspace/sglang/python/sglang/srt/layers/logits_processor.py:ro" \
25
- -v "$model_dir/runtime/patches/sglang-qwen35.py:/sgl-workspace/sglang/python/sglang/srt/models/qwen3_5.py:ro" \
26
- -v "$model_dir/runtime/patches/sglang-batch-result-processor.py:/sgl-workspace/sglang/python/sglang/srt/managers/scheduler_components/batch_result_processor.py:ro" \
27
- --entrypoint /model/runtime/entrypoint.sh "$image" \
28
- -m sglang.launch_server \
29
- --model-path /model \
30
- --load-format presharded \
31
- --model-loader-extra-config '{"presharded_path":"/model/weights","max_file_bytes":5368709120,"verify_on_load":true}' \
32
- --host 0.0.0.0 --port 30000 \
33
- --dtype bfloat16 \
34
- --quantization quark_int4fp8_moe \
35
- --fp8-gemm-backend aiter \
36
- --moe-runner-backend aiter \
37
- --tp-size 1 --mem-fraction-static "$memory_fraction" \
38
- --context-length 262144 --max-running-requests 64 \
39
- --chunked-prefill-size 4096 \
40
- --mamba-radix-cache-strategy extra_buffer \
41
- --page-size 64 --attention-backend aiter \
42
- --cuda-graph-backend-prefill breakable \
43
- --cuda-graph-max-bs-decode 64 \
44
- --cuda-graph-bs-prefill 128 256 512 1024 2048 4096
45
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tokenizer.json DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
- size 19989325
 
 
 
 
tokenizer_config.json DELETED
@@ -1,32 +0,0 @@
1
- {
2
- "add_prefix_space": false,
3
- "audio_bos_token": "<|audio_start|>",
4
- "audio_eos_token": "<|audio_end|>",
5
- "audio_token": "<|audio_pad|>",
6
- "backend": "tokenizers",
7
- "bos_token": null,
8
- "clean_up_tokenization_spaces": false,
9
- "eos_token": "<|im_end|>",
10
- "errors": "replace",
11
- "image_token": "<|image_pad|>",
12
- "is_local": true,
13
- "local_files_only": true,
14
- "model_max_length": 262144,
15
- "model_specific_special_tokens": {
16
- "audio_bos_token": "<|audio_start|>",
17
- "audio_eos_token": "<|audio_end|>",
18
- "audio_token": "<|audio_pad|>",
19
- "image_token": "<|image_pad|>",
20
- "video_token": "<|video_pad|>",
21
- "vision_bos_token": "<|vision_start|>",
22
- "vision_eos_token": "<|vision_end|>"
23
- },
24
- "pad_token": "<|endoftext|>",
25
- "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
- "split_special_tokens": false,
27
- "tokenizer_class": "Qwen2Tokenizer",
28
- "unk_token": null,
29
- "video_token": "<|video_pad|>",
30
- "vision_bos_token": "<|vision_start|>",
31
- "vision_eos_token": "<|vision_end|>"
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
weights/TP-1-sig-e793a9432f30cd4e/READY DELETED
@@ -1 +0,0 @@
1
- {"plan_version": 1, "world_size": 1, "created_at": 1789971566.5357497}
 
 
weights/TP-1-sig-e793a9432f30cd4e/checksum.json DELETED
The diff for this file is too large to render. See raw diff
 
weights/TP-1-sig-e793a9432f30cd4e/model-00000-common.safetensor DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:16e3906e9a161a6337a4a0f6d06e352d09360b37a9d4c4ea341ea978192ee289
3
- size 5268047400
 
 
 
 
weights/TP-1-sig-e793a9432f30cd4e/model-00001-common.safetensor DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7ad7affbcc8912528ff2859568b51ee9427cc17119312ea8750cf118544ea5d3
3
- size 5120198752
 
 
 
 
weights/TP-1-sig-e793a9432f30cd4e/model-00002-common.safetensor DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:fa4a08a1e0e0de30543d343a629c6710e59575cd6f98f5beae789f34a59b79b7
3
- size 5254941984
 
 
 
 
weights/TP-1-sig-e793a9432f30cd4e/model-00003-common.safetensor DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:dfb557a70a97b91208837dac145569c844971062970f41466738e61e77d53eb3
3
- size 4014197624