Devansh Batham commited on
Commit
f80a01b
·
1 Parent(s): 70155f1

Publish Nyx INT4+FP8 production checkpoint

Browse files
.gitattributes CHANGED
@@ -1,35 +1,11 @@
1
  *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
  *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
  *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
  *.7z filter=lfs diff=lfs merge=lfs -text
 
2
  *.bin filter=lfs diff=lfs merge=lfs -text
 
3
  *.ckpt filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
4
  *.model filter=lfs diff=lfs merge=lfs -text
5
+ *.png filter=lfs diff=lfs merge=lfs -text
6
+ *.safetensor filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
7
  *.safetensors filter=lfs diff=lfs merge=lfs -text
8
+ 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
 
 
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Alibaba Cloud
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
NOTICE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 this release.
13
+
README.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 decision model published by
20
+ [`devanshbatham`](https://huggingface.co/devanshbatham). This repository contains the exact
21
+ accepted production checkpoint in its native SGLang presharded format.
22
+
23
+ ![Nyx and Jev benchmark comparison](assets/nyx-vs-jev-benchmarks.png)
24
+
25
+ ## Model
26
+
27
+ | Property | Value |
28
+ |---|---|
29
+ | Base model | [`Qwen/Qwen3.5-35B-A3B`](https://huggingface.co/Qwen/Qwen3.5-35B-A3B) |
30
+ | Base revision | `59d61f3ce65a6d9863b86d2e96597125219dc754` |
31
+ | Architecture | Qwen3.5-35B-A3B text MoE |
32
+ | Quantization | Per-channel INT4 routed-expert storage; FP8 expert compute; dynamic FP8 dense layers |
33
+ | Runtime precision | BF16 activations and KV cache; FP32 SSM state |
34
+ | Measured model memory | 19.43 GB |
35
+ | Qualified hardware | AMD MI325X (`gfx942`) |
36
+ | Publisher | `devanshbatham` |
37
+
38
+ The original architecture, tokenizer, and base weights are from Qwen. Nyx contains subsequent
39
+ task-specific decision adaptation and quantization. See [NOTICE](NOTICE) and [provenance.json](provenance.json).
40
+
41
+ ## Compatibility
42
+
43
+ This is **not** a standard Transformers `safetensors` checkpoint and cannot be loaded with
44
+ `AutoModel.from_pretrained`. The weight files use SGLang's pinned presharded loader plus the
45
+ included runtime patches. Only the pinned SGLang container on AMD MI325X has been qualified.
46
+ Other GPUs, SGLang versions, quantization backends, and conversions are unverified.
47
+
48
+ ## Run
49
+
50
+ Requirements: Linux, Docker, ROCm-visible `/dev/kfd` and `/dev/dri`, and an AMD MI325X.
51
+
52
+ ```bash
53
+ hf download devanshbatham/nyx --local-dir nyx
54
+ cd nyx
55
+ python3 verify.py
56
+ bash serve.sh
57
+ ```
58
+
59
+ The server binds to `127.0.0.1:30000` by default. Override the port with `NYX_PORT`. Keep the
60
+ model backend private and place an authenticated API gateway and TLS termination in front of it
61
+ for networked production use.
62
+
63
+ ```bash
64
+ curl http://127.0.0.1:30000/v1/chat/completions \
65
+ -H 'Content-Type: application/json' \
66
+ -d '{
67
+ "model": "/model",
68
+ "messages": [{"role": "user", "content": "Classify as positive or negative: I loved it."}],
69
+ "temperature": 0,
70
+ "max_tokens": 8
71
+ }'
72
+ ```
73
+
74
+ The production System One gateway and typed clients are maintained separately at
75
+ [`devanshbatham/nyx`](https://github.com/devanshbatham/nyx).
76
+
77
+ ## Evaluation
78
+
79
+ The frozen public comparison used 2,277 identical cases, concurrency 8, and counted every failure
80
+ as incorrect. All three providers completed every case with zero errors and zero retries.
81
+
82
+ | Benchmark | Nyx INT4+FP8 | Qwen base BF16 | Jev 1.13 |
83
+ |---|---:|---:|---:|
84
+ | AG News | 84.75% | 85.75% | 86.50% |
85
+ | TREC coarse | 92.00% | 94.00% | 93.00% |
86
+ | CoLA | 79.00% | 79.75% | 77.75% |
87
+ | RTE | 93.14% | 93.86% | 91.34% |
88
+ | Emotion | 43.33% | 44.67% | 47.00% |
89
+ | IMDb | 96.50% | 96.25% | 97.00% |
90
+ | **Dataset-macro accuracy** | **81.45%** | **82.38%** | **82.10%** |
91
+ | **Pooled accuracy** | **82.96%** | **83.93%** | **83.62%** |
92
+
93
+ Nyx versus Jev exact paired McNemar p=`0.23154`. Base versus Nyx p=`0.03896` before correction
94
+ for three comparisons and does not cross the Bonferroni-adjusted `0.0167` threshold. Public
95
+ pretraining contamination is unknown for every model. These six benchmarks do not establish
96
+ universal superiority. See [the evaluation record](benchmarks/random-six-report.md).
97
+
98
+ On the separately authored 2,000-case H1 development workload, Nyx INT4+FP8 scored 85.65% versus
99
+ 86.65% for Nyx FP8. The compact model used 19.43 GB versus 34.58 GB model memory and was accepted
100
+ with the measured 1.00 percentage-point tradeoff. H1 is development evidence, not an independent
101
+ external test.
102
+
103
+ ## Training disclosure
104
+
105
+ Nyx was post-trained for structured decision/classification tasks using task-specific examples,
106
+ including teacher probability targets. The publisher has affirmed the right to distribute the
107
+ resulting weights. Raw training records and private evaluation data are not included.
108
+
109
+ ## Limitations
110
+
111
+ - Optimized for structured classification and decision workloads; general assistant quality was not established.
112
+ - Qualified only with the included SGLang/ROCm runtime on one AMD MI325X.
113
+ - The public evaluation is finite and may overlap model pretraining data.
114
+ - Outputs can be incorrect or biased. Use independent validation and human review for consequential decisions.
115
+ - No claim of bit-exact parity with BF16, Jev, or the original Qwen model is made.
116
+ - The included launcher exposes only a loopback model backend; production authentication is the operator's responsibility.
117
+
118
+ ## License and attribution
119
+
120
+ Apache License 2.0. Nyx is derived from
121
+ [`Qwen/Qwen3.5-35B-A3B`](https://huggingface.co/Qwen/Qwen3.5-35B-A3B), also distributed under
122
+ Apache-2.0. Qwen and Alibaba Cloud are credited as the original model authors. This project is not
123
+ endorsed by or affiliated with the Qwen authors.
124
+
assets/nyx-vs-jev-benchmarks.png ADDED

Git LFS Details

  • SHA256: 679a775f1f891713884549dcf6c2eca849f5b4c40064bc1ffb70a507e1add9c8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.76 MB
benchmarks/random-six-report.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Six public classification benchmarks: Nyx vs base vs Jev
2
+
3
+ Frozen paired cohort: **2,277 cases**; SHA-256 `046a38c24747fd724003aca7e5f1d1b6e148d599092222361c0717ecff84ecad`; concurrency 8; every failure counts wrong.
4
+
5
+ | Dataset | Cases | Nyx INT4+FP8 | Qwen base BF16 | Jev 1.13 |
6
+ |---|---:|---:|---:|---:|
7
+ | ag_news | 400 | 84.75% (339/400) | 85.75% (343/400) | 86.50% (346/400) |
8
+ | trec_coarse | 500 | 92.00% (460/500) | 94.00% (470/500) | 93.00% (465/500) |
9
+ | cola | 400 | 79.00% (316/400) | 79.75% (319/400) | 77.75% (311/400) |
10
+ | rte | 277 | 93.14% (258/277) | 93.86% (260/277) | 91.34% (253/277) |
11
+ | emotion | 300 | 43.33% (130/300) | 44.67% (134/300) | 47.00% (141/300) |
12
+ | imdb | 400 | 96.50% (386/400) | 96.25% (385/400) | 97.00% (388/400) |
13
+
14
+ | Aggregate | Nyx INT4+FP8 | Qwen base BF16 | Jev 1.13 |
15
+ |---|---:|---:|---:|
16
+ | Dataset-macro accuracy | 81.45% | 82.38% | 82.10% |
17
+ | Dataset-macro F1 | 81.06% | 82.02% | 81.85% |
18
+ | Pooled micro accuracy | 82.96% | 83.93% | 83.62% |
19
+
20
+ ## Paired comparison
21
+
22
+ Exact two-sided McNemar tests use the identical case-level predictions. Base beat Nyx on 63 discordant cases versus 41 in the other direction (p=0.03896, nominal and not corrected for three comparisons). Jev beat Nyx 76 to 61 (p=0.23154), while base beat Jev 66 to 59 (p=0.59168). Thus only base-versus-Nyx crosses an uncorrected 0.05 threshold; none crosses a Bonferroni-adjusted 0.0167 threshold.
23
+
24
+ | Operational timing | Nyx INT4+FP8 | Qwen base BF16 | Jev 1.13 |
25
+ |---|---:|---:|---:|
26
+ | Wall time | 20.21 s | 23.82 s | 53.67 s |
27
+ | Throughput | 112.67 req/s | 95.58 req/s | 42.43 req/s |
28
+ | Median request | 66.25 ms | 78.23 ms | 182.33 ms |
29
+ | p95 request | 89.22 ms | 101.82 ms | 245.12 ms |
30
+
31
+ ## Method and caveats
32
+
33
+ - Dataset choices and examples were frozen before provider scoring; request bodies and order are byte-equivalent across providers.
34
+ - AG News, CoLA, Emotion, and IMDb are fixed class-stratified samples; TREC test and RTE validation are complete.
35
+ - Accuracy is reported per dataset. Dataset-macro accuracy weights each benchmark equally; pooled accuracy weights individual cases.
36
+ - Public-benchmark pretraining contamination is unknown for all three models. This finite suite does not prove universal classification quality.
37
+ - Timing is operational only: Nyx and base are local on one MI325X, while Jev is remote HTTPS on unmatched infrastructure.
38
+
39
+ ## Artifacts
40
+
41
+ - Manifest: `artifacts/random-six-headtohead-v2/manifest.json`
42
+ - Comparison JSON: `artifacts/random-six-headtohead-v2/comparison.json`
43
+ - Raw provider results: `artifacts/random-six-headtohead-v2/result-nyx-int4fp8.json`, `artifacts/random-six-headtohead-v2/result-qwen-base-bf16.json`, `artifacts/random-six-headtohead-v2/result-jev-1.13.json`
benchmarks/summary.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format_version": 1,
3
+ "cases_sha256": "046a38c24747fd724003aca7e5f1d1b6e148d599092222361c0717ecff84ecad",
4
+ "cases": 2277,
5
+ "concurrency": 8,
6
+ "errors": {
7
+ "nyx-int4fp8": 0,
8
+ "qwen-base-bf16": 0,
9
+ "jev-1.13": 0
10
+ },
11
+ "dataset_macro_accuracy": {
12
+ "nyx-int4fp8": 0.8145402125952668,
13
+ "qwen-base-bf16": 0.8237991375852386,
14
+ "jev-1.13": 0.8209762334536702
15
+ },
16
+ "pooled_accuracy": {
17
+ "nyx-int4fp8": 0.8296003513394817,
18
+ "qwen-base-bf16": 0.839262187088274,
19
+ "jev-1.13": 0.8361879666227492
20
+ },
21
+ "paired_mcnemar_two_sided_p": {
22
+ "nyx-int4fp8_vs_qwen-base-bf16": 0.03895793340131187,
23
+ "nyx-int4fp8_vs_jev-1.13": 0.23153716588439505,
24
+ "qwen-base-bf16_vs_jev-1.13": 0.591683927094163
25
+ },
26
+ "limitations": [
27
+ "Public benchmark contamination is unknown.",
28
+ "Fixed samples are not complete official-suite scores except TREC test and RTE validation.",
29
+ "Jev timing used unmatched remote infrastructure."
30
+ ]
31
+ }
32
+
build_manifest.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build the deterministic release manifest for this repository."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+ EXCLUDE = {"release-manifest.json", "build_manifest.py", "verify.py"}
13
+
14
+
15
+ def digest(path: Path) -> str:
16
+ checksum = hashlib.sha256()
17
+ with path.open("rb") as stream:
18
+ for block in iter(lambda: stream.read(8 * 1024 * 1024), b""):
19
+ checksum.update(block)
20
+ return checksum.hexdigest()
21
+
22
+
23
+ files = {}
24
+ for path in sorted(ROOT.rglob("*")):
25
+ if not path.is_file() or ".git" in path.parts:
26
+ continue
27
+ relative = path.relative_to(ROOT).as_posix()
28
+ if relative in EXCLUDE:
29
+ continue
30
+ files[relative] = {"bytes": path.stat().st_size, "sha256": digest(path)}
31
+
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
+
chat_template.jinja ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
provenance.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format_version": 1,
3
+ "model": "devanshbatham/nyx",
4
+ "publisher": "devanshbatham",
5
+ "base_model": "Qwen/Qwen3.5-35B-A3B",
6
+ "base_revision": "59d61f3ce65a6d9863b86d2e96597125219dc754",
7
+ "license": "Apache-2.0",
8
+ "quantization": {
9
+ "routed_expert_storage": "per-channel INT4",
10
+ "expert_compute": "FP8",
11
+ "dense_layers": "dynamic FP8",
12
+ "activations": "BF16",
13
+ "kv_cache": "BF16",
14
+ "ssm_state": "FP32"
15
+ },
16
+ "qualified_runtime": {
17
+ "hardware": "AMD MI325X gfx942",
18
+ "sglang_image": "lmsysorg/sglang@sha256:a0ffcdd013af6d79c9b7c4348c42b14a79a48f6c43dd3ca0235ec3802da45f75",
19
+ "tensor_parallel_size": 1
20
+ },
21
+ "accepted_evidence": {
22
+ "model_memory_gb": 19.43,
23
+ "package_bytes": 19657745446,
24
+ "h1_correct": 1713,
25
+ "h1_total": 2000,
26
+ "h1_result_sha256": "b10697d6a03fb19c56e7193eb85b932c92aee2a27d4b6a088f684051dff1c01e",
27
+ "source_checksum_manifest_sha256": "829aa54dd4f440c10d85c6253a41891a69ba1717ace2d193cbfab3c1a7546c6e"
28
+ }
29
+ }
30
+
release-manifest.json ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "artifact": "nyx-35b-a3b-int4fp8",
3
+ "files": {
4
+ ".gitattributes": {
5
+ "bytes": 448,
6
+ "sha256": "f3b77d221a265fd892b0993f88aa613596c2f58adf13dcee9b1d3a14156a14f8"
7
+ },
8
+ "LICENSE": {
9
+ "bytes": 11544,
10
+ "sha256": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a"
11
+ },
12
+ "NOTICE": {
13
+ "bytes": 459,
14
+ "sha256": "08640c173e604546300dd76b47c842858ffcc236942b45c6cfdd894c851d8a77"
15
+ },
16
+ "README.md": {
17
+ "bytes": 4989,
18
+ "sha256": "f8965349f25854cf6818f780b0a28151150f0b78eab15064a435e695defa0316"
19
+ },
20
+ "assets/nyx-vs-jev-benchmarks.png": {
21
+ "bytes": 1757593,
22
+ "sha256": "679a775f1f891713884549dcf6c2eca849f5b4c40064bc1ffb70a507e1add9c8"
23
+ },
24
+ "benchmarks/random-six-report.md": {
25
+ "bytes": 2731,
26
+ "sha256": "1dd4c4a7cad8a4158a4e5d9f2ddd0fa01d1f8c3df629585a1591229c8b4d9a98"
27
+ },
28
+ "benchmarks/summary.json": {
29
+ "bytes": 955,
30
+ "sha256": "e3a56d1bc57b8d1837ef9abbd5852d2d45a83cbc345b2a638f1e8e5018599a3f"
31
+ },
32
+ "chat_template.jinja": {
33
+ "bytes": 7756,
34
+ "sha256": "a4aee8afcf2e0711942cf848899be66016f8d14a889ff9ede07bca099c28f715"
35
+ },
36
+ "config.json": {
37
+ "bytes": 2336,
38
+ "sha256": "27e3575d049c5a0187b783aecff914135be455439134368aba8cdcc6d678ebb7"
39
+ },
40
+ "generation_config.json": {
41
+ "bytes": 214,
42
+ "sha256": "df6f86c3fdce573ecdb55cec35f502ddda70e70abe78288a78164aef120232e7"
43
+ },
44
+ "provenance.json": {
45
+ "bytes": 980,
46
+ "sha256": "85773821fad372dd98cf10b02c4103e78406f0b45d13f2875772470a9791f1d6"
47
+ },
48
+ "runtime/entrypoint.sh": {
49
+ "bytes": 276,
50
+ "sha256": "742824765cb2f5361a476674e16a5d347abb9ae905e2798a2183121ea1ee4b07"
51
+ },
52
+ "runtime/label-token-ids.json": {
53
+ "bytes": 761,
54
+ "sha256": "8cc5610b02d5ba14358ebef59cd16f567767632fb6ea03de1565b18f1c003a40"
55
+ },
56
+ "runtime/patches/qwen35-text-quark-generated-scales.patch": {
57
+ "bytes": 1680,
58
+ "sha256": "920a8cabd8d2f6565e4f0c3296b1b0e154e48c227e4fd12f019033792cbe567f"
59
+ },
60
+ "runtime/patches/sglang-batch-result-processor.py": {
61
+ "bytes": 57803,
62
+ "sha256": "f7f58ef7f377e3df111c87d1803b6e0008bdf76726d80cbf1012275839e77da7"
63
+ },
64
+ "runtime/patches/sglang-logits-processor.py": {
65
+ "bytes": 54217,
66
+ "sha256": "aed498368a4402a84a4ef71a5962f1ff78cad265d4becbe5b3b677f1cb8ad9e2"
67
+ },
68
+ "runtime/patches/sglang-qwen35-text.py": {
69
+ "bytes": 12308,
70
+ "sha256": "52cdbda541ff612353adf5d5b04a6668954040b48cfa5209f59abbd662a023e1"
71
+ },
72
+ "runtime/patches/sglang-qwen35.py": {
73
+ "bytes": 106934,
74
+ "sha256": "f6975a9b3303002c25418b0d5b32c6bd40502ba6568ab7d5c847c6983705c01e"
75
+ },
76
+ "serve.sh": {
77
+ "bytes": 2065,
78
+ "sha256": "5f308855054cd6dd86fde051cc9771e02e1e160dc0ddbad1f5eed8e7b81a4b65"
79
+ },
80
+ "tokenizer.json": {
81
+ "bytes": 19989325,
82
+ "sha256": "06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523"
83
+ },
84
+ "tokenizer_config.json": {
85
+ "bytes": 1123,
86
+ "sha256": "bee8eba30f0eb4af73c0fe2cd06d0f89b657d7819941c438157ec42f7c80ea87"
87
+ },
88
+ "weights/TP-1-sig-e793a9432f30cd4e/READY": {
89
+ "bytes": 70,
90
+ "sha256": "b20cd5cb19a0b44c0189bb79b85ee0300f0453a31bfef7b39ac825b409cbb578"
91
+ },
92
+ "weights/TP-1-sig-e793a9432f30cd4e/checksum.json": {
93
+ "bytes": 359616,
94
+ "sha256": "829aa54dd4f440c10d85c6253a41891a69ba1717ace2d193cbfab3c1a7546c6e"
95
+ },
96
+ "weights/TP-1-sig-e793a9432f30cd4e/model-00000-common.safetensor": {
97
+ "bytes": 5268047400,
98
+ "sha256": "16e3906e9a161a6337a4a0f6d06e352d09360b37a9d4c4ea341ea978192ee289"
99
+ },
100
+ "weights/TP-1-sig-e793a9432f30cd4e/model-00001-common.safetensor": {
101
+ "bytes": 5120198752,
102
+ "sha256": "7ad7affbcc8912528ff2859568b51ee9427cc17119312ea8750cf118544ea5d3"
103
+ },
104
+ "weights/TP-1-sig-e793a9432f30cd4e/model-00002-common.safetensor": {
105
+ "bytes": 5254941984,
106
+ "sha256": "fa4a08a1e0e0de30543d343a629c6710e59575cd6f98f5beae789f34a59b79b7"
107
+ },
108
+ "weights/TP-1-sig-e793a9432f30cd4e/model-00003-common.safetensor": {
109
+ "bytes": 4014197624,
110
+ "sha256": "dfb557a70a97b91208837dac145569c844971062970f41466738e61e77d53eb3"
111
+ }
112
+ },
113
+ "format_version": 1,
114
+ "repository": "devanshbatham/nyx",
115
+ "total_bytes": 19679761943
116
+ }
runtime/entrypoint.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1 @@
 
 
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 ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,1378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,1272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
The diff for this file is too large to render. See raw diff
 
serve.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
tokenizer_config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
verify.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fail-closed integrity check for the published Nyx package."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parent
12
+
13
+
14
+ def digest(path: Path) -> str:
15
+ checksum = hashlib.sha256()
16
+ with path.open("rb") as stream:
17
+ for block in iter(lambda: stream.read(8 * 1024 * 1024), b""):
18
+ checksum.update(block)
19
+ return checksum.hexdigest()
20
+
21
+
22
+ manifest = json.loads((ROOT / "release-manifest.json").read_text())
23
+ if manifest.get("repository") != "devanshbatham/nyx":
24
+ raise SystemExit("Unexpected repository identity")
25
+
26
+ for name, metadata in manifest["files"].items():
27
+ path = ROOT / name
28
+ if not path.is_file() or path.is_symlink():
29
+ raise SystemExit(f"Missing or unsafe release file: {name}")
30
+ if path.stat().st_size != metadata["bytes"]:
31
+ raise SystemExit(f"Size mismatch: {name}")
32
+ if digest(path) != metadata["sha256"]:
33
+ raise SystemExit(f"SHA-256 mismatch: {name}")
34
+
35
+ print(json.dumps({
36
+ "verified": True,
37
+ "files": len(manifest["files"]),
38
+ "total_bytes": manifest["total_bytes"],
39
+ "repository": manifest["repository"],
40
+ }))
41
+
weights/TP-1-sig-e793a9432f30cd4e/READY ADDED
@@ -0,0 +1 @@
 
 
1
+ {"plan_version": 1, "world_size": 1, "created_at": 1789971566.5357497}
weights/TP-1-sig-e793a9432f30cd4e/checksum.json ADDED
The diff for this file is too large to render. See raw diff
 
weights/TP-1-sig-e793a9432f30cd4e/model-00000-common.safetensor ADDED
@@ -0,0 +1,3 @@
 
 
 
 
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 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
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 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
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 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfb557a70a97b91208837dac145569c844971062970f41466738e61e77d53eb3
3
+ size 4014197624