yzhwang commited on
Commit
bc55caa
·
0 Parent(s):

Escha Runtime qwen3dense — escha 1.1.0+qwen3dense SGLang wheel, serve.sh, cookbook

Browse files
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
36
+ sglang/escha-1.1.0+qwen3dense-cp312-cp312-manylinux_2_28_x86_64.whl filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 The og Authors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - quantization
5
+ - sglang
6
+ - cuda
7
+ - inference
8
+ - escha
9
+ - qwen3
10
+ - dense
11
+ library_name: sglang
12
+ ---
13
+
14
+ # Escha Runtime — `qwen3dense`
15
+
16
+ By **[Escha Labs Inc.](https://eschalabs.com/)**
17
+
18
+ The serving runtime for **Escha** 2-/3-bit (`escha`) quantized models of the **`qwen3_5` dense
19
+ architecture** (Qwen3.8-27B and siblings). One repo per model architecture, one directory per
20
+ engine — this architecture currently has **one** engine, [`sglang/`](sglang/).
21
+
22
+ | | **SGLang** — [`sglang/`](sglang/) |
23
+ |---|---|
24
+ | Best for | everything: single user, teams, agents |
25
+ | Concurrency | continuous batching, paged KV, optional radix prefix cache |
26
+ | Tool calls / JSON schema / thinking parser | yes |
27
+ | Interface | OpenAI-compatible (`/v1/chat/completions`, `/v1/completions`, `/v1/models`) |
28
+ | Install | Python 3.12 venv + CUDA-12 PyTorch, then one wheel |
29
+
30
+ The engine is a fork of [SGLang](https://github.com/sgl-project/sglang) bundled inside the wheel,
31
+ running the Escha CUDA kernels. No separate `sglang` install is needed, and none should be
32
+ present — the wheel ships its own.
33
+
34
+ ### Compatible models
35
+
36
+ | Model repo | Bits |
37
+ |---|---|
38
+ | [EschaLabs/Qwen3.8-27B-Escha-W2](https://huggingface.co/EschaLabs/Qwen3.8-27B-Escha-W2) | 2-bit, mixed-rate (`escha`) |
39
+ <!-- add each qwen3_5 dense Escha model you publish here -->
40
+
41
+ > This runtime targets the **`qwen3_5` dense** architecture. Its wheel also happens to register the
42
+ > `eschamoe` mixture-of-experts method, so a `qwen3_5_moe` model will load too — but the tuning,
43
+ > the defaults in `sglang/serve.sh` and the documentation here are all written for the dense
44
+ > architecture. For a mixture-of-experts model use
45
+ > [`escha-runtime-qwen3moe`](https://huggingface.co/EschaLabs/escha-runtime-qwen3moe), whose
46
+ > defaults are measured on it. A model of a genuinely different architecture will not load — use
47
+ > the matching `escha-runtime-<arch>` repo.
48
+
49
+ ## Quickstart
50
+
51
+ Full detail, including the per-GPU cookbook and troubleshooting:
52
+ [`sglang/INSTALL.md`](sglang/INSTALL.md).
53
+
54
+ ```bash
55
+ python3.12 -m venv .venv && source .venv/bin/activate
56
+ pip install -U pip wheel
57
+ pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu128 # cu12 torch FIRST
58
+ pip install ./sglang/escha-*.whl # pulls the bundled sglang fork + its full dep closure
59
+
60
+ hf download EschaLabs/Qwen3.8-27B-Escha-W2 --local-dir ./Qwen3.8-27B-Escha-W2
61
+ MODEL=./Qwen3.8-27B-Escha-W2 bash sglang/serve.sh
62
+ ```
63
+
64
+ Then check the stack and the endpoint:
65
+
66
+ ```bash
67
+ python -c "import torch, escha, sglang; print(torch.cuda.is_available(), hasattr(torch.ops.escha, 'escham_decode_gemv'), escha.__version__)"
68
+ curl -s http://127.0.0.1:30000/v1/models | python3 -m json.tool
69
+ ```
70
+
71
+ > `pip install "torch==2.9.*"` is a hard pin, not a suggestion. A bare `torch>=2.9` resolves to a
72
+ > newer minor and `import escha` then fails with `undefined symbol: _ZN3c10...` — the compiled
73
+ > extension is ABI-linked to libtorch, and that ABI is not stable across PyTorch minors.
74
+
75
+ ## Thinking, and why you probably want a budget
76
+
77
+ This is a reasoning model. With thinking on, the reasoning arrives in `reasoning_content` and the
78
+ answer in `content` — **read both**, or you will see half the response.
79
+
80
+ Two per-request levers, both inside `chat_template_kwargs` (a *top-level* `enable_thinking` field
81
+ is silently ignored):
82
+
83
+ ```jsonc
84
+ { "chat_template_kwargs": {"enable_thinking": true, "reasoning_effort": "xhigh"} }
85
+ ```
86
+
87
+ `reasoning_effort` is `"xhigh"` (**the default**), `"medium"` or `"low"`; anything else makes the
88
+ template raise, which surfaces as an HTTP 400 rather than a silent fallback. It works by injecting
89
+ one sentence of system instruction — `xhigh` asks the model to validate assumptions and weigh
90
+ alternatives, `low` asks it to keep thinking brief, and **`medium` injects nothing at all**, so
91
+ `medium` is the neutral, unsteered model rather than a midpoint. It therefore *asks* for shorter
92
+ reasoning; it does not bound it. If you are running a benchmark
93
+ or an agent, set a **thinking budget** instead, which forces `</think>` after N reasoning tokens so
94
+ an answer is always produced: see
95
+ [`sglang/INSTALL.md` → Bounded thinking](sglang/INSTALL.md#bounded-thinking-thinking_budget) and
96
+ [`sglang/thinking_budget.py`](sglang/thinking_budget.py). Without one, the usual failure is
97
+ `finish_reason: "length"` with `content: null`, which a harness scores as *wrong* rather than as
98
+ *slow*.
99
+
100
+ ## Requirements
101
+
102
+ - **NVIDIA GPU, compute capability 8.0–12.0** (Ampere → Blackwell), Linux x86-64 with
103
+ **glibc ≥ 2.28**. The kernel launch route auto-selects per GPU; you never set it. Per-architecture
104
+ and per-VRAM launch recipes:
105
+ [`sglang/INSTALL.md` → Running on your GPU](sglang/INSTALL.md#running-on-your-gpu).
106
+ - **Python 3.12** (the wheel is `cp312`-only) + **CUDA-12 PyTorch 2.9.x**. The wheel handles every
107
+ other dependency.
108
+ - **A working host C compiler and Python dev headers.** Triton JIT-compiles a small shim at
109
+ CUDA-graph capture time — this is separate from `ptxas` and from a CUDA toolkit, so "driver only"
110
+ does not cover it. On slim container images a stripped `libisl` breaks `cc1` while
111
+ `gcc --version` still succeeds, and the failure surfaces ~40 s in as a `gcc`
112
+ `CalledProcessError` inside `cuda_graph_runner.py` — which reads like a runtime bug and is not.
113
+ Preflight in [`sglang/INSTALL.md`](sglang/INSTALL.md#requirements).
114
+ - **24 GB VRAM** for the shipped defaults (65,536-token context, ~8–9 concurrent streams) with a
115
+ ~10.15 GB model. Note the stream ceiling: `MAMBA_RATIO=0.3` sizes the recurrent-state pool, which
116
+ clamps `max_running_requests` to 8–9 on a 24 GB card, so the `12`/`16` entries in the default
117
+ `CUDA_GRAPH_BS` are **dropped and never captured**. To serve more streams raise
118
+ `MAXREQ`/`MAXMAMBA` with `MEM` — the throughput recipe is in the
119
+ [model card](https://huggingface.co/EschaLabs/Qwen3.8-27B-Escha-W2#verified-configurations).
120
+ 16 GB should fit at a reduced context — the cookbook has a recipe, but we have not run it.
121
+
122
+ ## Known limitations
123
+
124
+ - **Serving throughput has been measured on three cards** — RTX 5090 (32 GB, sm_120), RTX 4090
125
+ (24 GB, sm_89) and RTX 3090 (24 GB, sm_86); the 5090 and 3090 by independent evaluators working
126
+ only from these docs. The 4090 is also where the model's thinking-on benchmarks (GPQA-Diamond,
127
+ LiveCodeBench) were produced; its thinking-off commonsense suite ran on an L40. The **16 GB and
128
+ 40 GB+** tiers in the cookbook remain configuration guidance derived from the model size and the
129
+ wheel's architecture coverage, **not measurements**. Per-GPU numbers live on the
130
+ [model card](https://huggingface.co/EschaLabs/Qwen3.8-27B-Escha-W2#performance-across-gpus).
131
+ - **On Ampere the auto-selected kernel route is the slower one at batch 1.** `ESCHA_ROUTE`
132
+ resolves to `lovelace` on sm_80/sm_86, but forcing `ESCHA_ROUTE=blackwell` measured **1.72×
133
+ faster single-stream on an RTX 3090** (23.6 → 40.7 tok/s, TPOT 42.4 → 24.6 ms) with identical
134
+ output. The two routes are bit-identical launch geometries, so this is safe to set; the gain is
135
+ batch-1-only (parity at 2–16). Serving one user on Ampere? Set it.
136
+ - **`DETERMINISTIC=1` fails on consumer Blackwell (sm_120).** The deterministic attention kernel
137
+ requests 104 KB of shared memory per block, above the sm_120 limit, and the server exits during
138
+ startup. It works on Ampere, Ada and Hopper.
139
+ - **Greedy output is not bit-reproducible across requests.** Batch composition changes fp16
140
+ accumulation order, so a near-tie can flip and a long reasoning chain diverges from there. Two
141
+ identical greedy requests may return different text. Use `DETERMINISTIC=1` when you need
142
+ reproducibility, and never A/B two configurations by diffing one generation.
143
+ - **CUDA-graph batch sizes are capped at 32**, the decode kernel's maximum M
144
+ (`torch.ops.escha.escham_decode_gemv_max_m()`). The shipped default list stops at 16 because
145
+ that is where aggregate throughput peaks on a 4090; capture at `24`/`32` works and is worth it if
146
+ you serve that many streams. Past 32 a batch falls through to a large-M path meant for prefill,
147
+ so the runtime refuses to capture it rather than bake in the wrong kernel.
148
+ - **`ATTN_BACKEND=triton` is required on consumer Blackwell (RTX 50-series).** The default
149
+ flashinfer backend asserts on this hybrid architecture at sm_120. The assertion names three
150
+ acceptable backends — `triton`, `trtllm_mha`, `fa4` — of which only `triton` has been run on
151
+ this model. Note that sm_120 shows steeper long-prompt decode decay than sm_89 (88.5% vs 96.3%
152
+ of short-prompt rate at a 5,000-token prompt); the attention path is the obvious suspect and
153
+ nobody has run the A/B that would confirm it.
154
+ - **The model's stock chat template raises** on a few malformed conversations (a missing user
155
+ query, a system message in the wrong position). That surfaces as an HTTP 400 from the template,
156
+ not as a server fault.
157
+
158
+ ## License
159
+
160
+ Everything here is released under the **Apache License, Version 2.0** — see [`LICENSE`](LICENSE).
161
+ All bundled third-party code is permissive (Apache-2.0 / MIT / BSD-3-Clause) — **no copyleft**.
162
+ Full texts and the component inventory:
163
+ [`THIRD_PARTY_LICENSES/`](THIRD_PARTY_LICENSES/). Model weights are **not** in this repo and carry
164
+ their own license in the model repository.
THIRD_PARTY_LICENSES/AQLM-NOTICE.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AQLM — https://github.com/Vahe1994/AQLM
2
+
3
+ Licensed under the Apache License, Version 2.0.
4
+ Copyright the AQLM authors.
5
+
6
+ The `escha` runtime's additive-codebook GEMV/HMMA kernels (`escha_aqlm_*`, compiled
7
+ into `escha/_C`) are an independent implementation that follows the AQLM additive
8
+ codebook *numeric format*; they are adapted from the escha runtime's own
9
+ kernels rather than copied from AQLM source. This notice is provided out of an
10
+ abundance of caution.
11
+
12
+ The full text of the Apache License, Version 2.0 is reproduced in
13
+ `SGLang-LICENSE.txt` in this directory (the license body is identical for every
14
+ Apache-2.0 component in this release; only the per-component copyright line
15
+ differs).
THIRD_PARTY_LICENSES/BSD-3-Clause.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 3-Clause License
2
+
3
+ Applies to code within the bundled SGLang fork that is adapted from
4
+ FlashAttention and Mamba:
5
+ Copyright (c) 2023, Tri Dao.
6
+ Copyright (c) 2024, Tri Dao, Albert Gu.
7
+ (and other BSD-3-Clause-licensed upstreams whose per-file copyright notices are
8
+ retained verbatim in the corresponding source headers within `sglang/`.)
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its contributors
21
+ may be used to endorse or promote products derived from this software
22
+ without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THIRD_PARTY_LICENSES/MIT.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Applies to code within the bundled SGLang fork that carries an
4
+ `SPDX-License-Identifier: MIT` header and/or is adapted from MIT-licensed
5
+ upstreams (e.g. flash-linear-attention):
6
+ Copyright (c) 2023-2025, Songlin Yang, Yu Zhang
7
+ (and other MIT-licensed upstreams whose per-file copyright notices are retained
8
+ verbatim in the corresponding source headers within `sglang/`.)
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
THIRD_PARTY_LICENSES/SGLang-LICENSE.txt ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2023-2024 SGLang Team
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
THIRD_PARTY_LICENSES/THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-Party Notices — Escha Runtime (`escha` 1.1.0)
2
+
3
+ This repository distributes the **runtime** only (the `escha` wheel: quantization/serving
4
+ kernels + a modified SGLang fork). SGLang is the only engine in this repository. The quantized **model weights** are distributed
5
+ separately and carry their own license — see the model repository
6
+ (**`EschaLabs/Qwen3.8-27B-Escha-W2`**).
7
+
8
+ All bundled third-party code is under **permissive licenses (Apache-2.0, MIT, BSD-3-Clause)** —
9
+ an audit found **no copyleft (GPL / LGPL / AGPL / MPL / SSPL) code**. The obligations below are
10
+ satisfied by the files in this `THIRD_PARTY_LICENSES/` directory together with the copyright
11
+ notices retained verbatim in the redistributed source (inside the wheel). The same texts are also
12
+ embedded in the wheel metadata (`escha-1.1.0+qwen3dense.dist-info/licenses/LICENSE`).
13
+
14
+ The `escha` runtime itself is released under the Apache License, Version 2.0 — see the top-level
15
+ `LICENSE` file of this repository.
16
+
17
+ ---
18
+
19
+ ## 1. Acknowledgements — formats referenced, no code bundled
20
+
21
+ The compiled CUDA runtime (`escha._C`) is **original work**. Its kernels implement, as
22
+ independent (clean-room) reimplementations, numeric *formats* referenced from the two upstream
23
+ projects below. **No upstream source is copied into the binary** and neither project's licence
24
+ obliges us to reproduce it here — the attribution is a courtesy, and we would rather over-credit
25
+ than under-credit work we learned from.
26
+
27
+ | Upstream | Relationship | License | Text |
28
+ |---|---|---|---|
29
+ | **exllamav3** (Turboderp) | quantization codec *format* referenced by the escha decode kernels | MIT, © 2025 Turboderp | `exllamav3-LICENSE.txt` |
30
+ | **AQLM** (Vahe1994/AQLM) | additive-codebook *format* referenced by `escha_aqlm_*` kernels | Apache-2.0 | `AQLM-NOTICE.txt` (text in `SGLang-LICENSE.txt`) |
31
+
32
+ `bitblas` (Microsoft, MIT) is an **optional runtime import** only — not bundled or redistributed here.
33
+
34
+ **Also gratefully acknowledged — the serving framework this engine is built on.**
35
+ Unlike the formats above, its code is genuinely **redistributed** here, so it is covered by real
36
+ licence obligations rather than courtesy: **[SGLang](https://github.com/sgl-project/sglang)**,
37
+ bundled as a modified fork — see §2. This runtime would not exist in this form without it.
38
+
39
+ ## 2. Bundled serving engine — `sglang/` (modified fork)
40
+
41
+ The wheel bundles a **modified fork of SGLang** as source.
42
+
43
+ - **License:** Apache-2.0 — © 2023–2026 SGLang Team. Full text: `SGLang-LICENSE.txt`.
44
+ - **Statement of changes (Apache-2.0 §4(b)):** the fork adds quantization-format support
45
+ (the `escha` dense and `eschamoe` mixture-of-experts methods, plus the int8 dense-layer
46
+ path) and kernel dispatch.
47
+ - **NOTICE (Apache-2.0 §4(d)):** upstream sgl-project/sglang publishes **no NOTICE file**
48
+ (verified against the fork's pristine upstream-mirror at the merge base and across the full
49
+ tree); therefore none is reproduced.
50
+
51
+ SGLang itself vendors code from other permissive projects; those files are redistributed
52
+ **unmodified with their copyright headers intact**, which preserves their notices:
53
+
54
+ | Upstream (as vendored in SGLang) | License | Reference text |
55
+ |---|---|---|
56
+ | vLLM project | Apache-2.0 | `SGLang-LICENSE.txt` |
57
+ | IST-DASLab / Marlin (Elias Frantar) | Apache-2.0 | `SGLang-LICENSE.txt` |
58
+ | EleutherAI + HuggingFace (transformers) | Apache-2.0 | `SGLang-LICENSE.txt` |
59
+ | NVIDIA (CUTLASS / kernels) | BSD-3-Clause / Apache-2.0 | `BSD-3-Clause.txt` |
60
+ | FlashAttention, Mamba (Tri Dao, Albert Gu) | BSD-3-Clause | `BSD-3-Clause.txt` |
61
+ | flash-linear-attention (Songlin Yang, Yu Zhang) | MIT | `MIT.txt` |
62
+ | Model configs (Qwen, DeepSeek, Moonshot, LG AI, Black Forest Labs, PaddlePaddle, Antgroup, …) | Apache-2.0 | `SGLang-LICENSE.txt` |
THIRD_PARTY_LICENSES/exllamav3-LICENSE.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exllamav3 — https://github.com/turboderp-org/exllamav3
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2025 Turboderp
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
sglang/INSTALL.md ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Escha Runtime — install & serve
2
+
3
+ This repository is the **runtime**: one wheel (`escha-*.whl`) holding the compiled `escha` package +
4
+ a scrubbed `sglang` fork, plus the `serve.sh` launcher. It is **model-agnostic** — point it at any
5
+ Escha model directory of a supported architecture. The quantized weights are a **separate**
6
+ repository: **`EschaLabs/Qwen3.8-27B-Escha-W2`**
7
+ (<https://huggingface.co/EschaLabs/Qwen3.8-27B-Escha-W2>).
8
+
9
+ ## Requirements
10
+
11
+ **All are hard, and three of them fail quietly:**
12
+
13
+ - **Linux x86-64 with glibc ≥ 2.28.** That covers RHEL / Rocky / Alma **8** and newer, Ubuntu 20.04+,
14
+ and Debian 10+. The wheel is tagged `manylinux_2_28_x86_64`, so pip refuses it up front on an
15
+ older distro instead of failing later at `import escha`.
16
+ - **Python 3.12.** The wheel is built `cp312`; any other interpreter rejects it at install time
17
+ with "not a supported wheel on this platform". (A compiled-extension wheel is tied to one CPython
18
+ minor — there is intentionally no 3.11/3.13 build.)
19
+ - **NVIDIA GPU, compute capability 8.0–12.0.** The build is a fat binary carrying SASS for
20
+ Ampere (`sm_80`/`sm_86`), Ada (`sm_89`, e.g. RTX 4090 / L40S), Hopper (`sm_90`), and Blackwell
21
+ (`sm_100`/`sm_120`), plus PTX for forward-compat JIT onto newer GPUs. Older cards (`< sm_80`,
22
+ e.g. Turing) are unsupported. Verify the installed wheel actually covers your card with
23
+ `cuobjdump --list-elf $(python -c "import escha,os;print(os.path.dirname(escha.__file__))")/_C*.so | grep -oE 'sm_[0-9]+' | sort -u`
24
+ — you should see all six. If it lists only one, it is a slim build and will fail on other GPUs
25
+ with "no kernel image is available for execution on the device" at the first launch (install and
26
+ the step-4 op-registration check still pass — the failure is at runtime).
27
+ - **A working host C compiler and Python dev headers.** Triton JIT-compiles a `cuda_utils.c` shim
28
+ at CUDA-graph capture time and shells out to `cc`; this is separate from `ptxas` and from a CUDA
29
+ toolkit, so "driver only, no toolkit" does not cover it. Preflight:
30
+ ```bash
31
+ echo '#include <Python.h>' | gcc -xc -I"$(python -c 'import sysconfig;print(sysconfig.get_paths()["include"])')" -c - -o /dev/null
32
+ ```
33
+ - **CUDA 12.x + matching PyTorch 2.9.x-for-CUDA-12.** `escha._C` is ABI-linked to libtorch and
34
+ `libcudart.so.12`, so the installed torch must be the CUDA-12 build of 2.9.x (a different torch
35
+ minor can fail to load `import escha`). Needs a driver new enough for CUDA 12.x.
36
+ - **`transformers >= 5.8`.** Older releases lack the config class for this architecture. The server
37
+ does **not** abort — it logs a single `WARNING ... config normalize FAILED`, serves with
38
+ un-normalized architecture parameters, and then produces fluent-looking nonsense. The wheel
39
+ **pins `transformers>=5.8`**, so a plain `pip install ./escha-*.whl` upgrades an old transformers
40
+ for you; you only hit this if you *downgrade* transformers afterwards. If generation is garbage,
41
+ check your transformers version *first*. (Conversely, transformers 5.x logs a scary-looking
42
+ `WARNING ... you can try downgrading to transformers==4.57.1` about RoPE — that is a generic
43
+ upstream sglang warning and is **safe to ignore**; do **not** downgrade.)
44
+
45
+ 1. `python3.12 -m venv .venv && source .venv/bin/activate`
46
+ 2. `pip install -U pip wheel`
47
+ 3. **Install PyTorch from the CUDA-12 index first** (it must be the cu12 build — `escha._C` is
48
+ ABI-linked to it, and it is the one dependency that cannot come from PyPI). **Pin to 2.9.x**:
49
+ `pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu128`
50
+ A bare `torch>=2.9.0` resolves to a newer minor, and **nothing corrects it** — the wheel
51
+ declares 61 dependencies but *no* `torch` entry, and none of its transitive deps pin one. You
52
+ stay on the wrong minor and `import escha` fails later with `undefined symbol: _ZN3c10...`,
53
+ because that is the C++ ABI surface PyTorch does not keep stable across minor versions.
54
+ 4. Install the runtime wheel (it ships in this repo):
55
+ `pip install ./escha-*.whl`
56
+ — installs the `escha` runtime, the bundled `sglang` fork, **and the fork's full runtime
57
+ dependency closure** (transformers ≥ 5.8, flashinfer, xgrammar, msgspec, uvloop, … — all pinned
58
+ in the wheel). No separate `sglang[srt]` install is needed. Sanity-check the whole stack (all
59
+ three should print `True`):
60
+ ```bash
61
+ python -c "import torch, escha, sglang; print(torch.cuda.is_available(), hasattr(torch.ops.escha, 'escham_decode_gemv'), bool(sglang.__version__))"
62
+ ```
63
+ (Note `import sglang` in the check — a `torch`+`escha`-only check passes on a box that cannot
64
+ actually serve.)
65
+ 5. Download a model (the weights are a **separate** repo; it is a flat folder — safetensors, config
66
+ and tokenizer at the root, no nested subdir):
67
+ ```bash
68
+ pip install -U "huggingface_hub[cli]"
69
+ hf download EschaLabs/Qwen3.8-27B-Escha-W2 --local-dir ./Qwen3.8-27B-Escha-W2
70
+ ```
71
+ 6. Serve it — point `MODEL` at the downloaded model directory:
72
+ ```bash
73
+ MODEL=./Qwen3.8-27B-Escha-W2 bash serve.sh
74
+ ```
75
+ - **Kernel route is auto-selected at runtime by GPU** — no env var needed. The kernels ship a
76
+ single fatbin with two launch-geometry routes; `escha` queries the device compute capability
77
+ and picks the measured-optimal one: `blackwell` (barrier-light, SM-adaptive split-K) on
78
+ **sm_89 Ada, sm_90 Hopper, and sm_100/sm_120 Blackwell**; `lovelace` (original geometry) on
79
+ **sm_80/sm_86 Ampere**. Both are bit-identical; force one with
80
+ `ESCHA_ROUTE=lovelace|blackwell` to A/B on your card (Hopper's default is a best-guess —
81
+ worth A/B'ing). Check the active route with
82
+ `python -c "import escha,torch; torch.zeros(1,device='cuda'); print(torch.ops.escha.escham_route())"`
83
+ (`2`=blackwell, `1`=lovelace).
84
+ - See the top of `serve.sh` for the full knob list (`MEM`, `CTXLEN`, `GRAPHS`, `THINK`, `RADIX`,
85
+ `INT8`, `DETERMINISTIC`, …). The defaults are the profile every published number for this
86
+ model was measured on, on a 24 GB card.
87
+ 7. OpenAI-compatible endpoint on `:30000` (`http://127.0.0.1:30000/v1`). The served model id is
88
+ `SERVED_NAME` from `serve.sh` (override with `SERVED_NAME=...`); list it with:
89
+ ```bash
90
+ curl -s http://127.0.0.1:30000/v1/models | python3 -m json.tool
91
+ ```
92
+
93
+ ## Behaviour worth knowing
94
+
95
+ - **Thinking is per-request.** `chat_template_kwargs: {"enable_thinking": false}` is honoured
96
+ against the default `THINK=1` server, with or without `tools`; a **top-level `enable_thinking`
97
+ field is ignored** — it must go inside `chat_template_kwargs`. Set `THINK=0` to serve the whole
98
+ server with thinking off.
99
+ - **`reasoning_effort` is a second lever, and the default is the expensive one.**
100
+ `chat_template_kwargs: {"reasoning_effort": "xhigh"|"medium"|"low"}`, default **`"xhigh"`**. Each
101
+ level is one sentence of injected system instruction: `xhigh` asks the model to validate
102
+ assumptions and weigh alternatives, `low` asks it to keep thinking brief, and **`medium` injects
103
+ nothing** — it is the neutral, unsteered model, not a midpoint. If `xhigh` is over-thinking your
104
+ traffic, `medium` is usually the right first move, `low` for latency-sensitive volume. Any other
105
+ value makes the template raise, which surfaces as an HTTP 400 — it is a typo, not a server fault.
106
+ All three only apply while thinking is on, and none of them *bounds* anything: a
107
+ `thinking_budget` (below) is the only hard guarantee.
108
+ - **Read `reasoning_content` AND `content`.** With thinking on, the reasoning arrives in
109
+ `reasoning_content` and the answer in `content`. A client that reads only one of them will see
110
+ half the response — this is the single most common integration mistake.
111
+ - **Structured output is stable.** `serve.sh` defaults `JSON_WS=1`
112
+ (`--constrained-json-disable-any-whitespace`), which keeps `json_schema` values consistent
113
+ run-to-run. `JSON_WS=0` opts out.
114
+ - **Over-long prompts are truncated, not rejected.** `serve.sh` defaults `TRUNCATE=1`
115
+ (`--allow-auto-truncate`). Without it, a prompt above `CTXLEN` is hard-rejected, and a harness
116
+ that scores the error as an answer reports it as a model failure. Set `TRUNCATE=0` if you would
117
+ rather see the error.
118
+ - **`usage.reasoning_tokens`** counts the reasoning span on both the streaming and non-streaming
119
+ paths, and is 0 when no reasoning parser is active. Use it to size a `thinking_budget` from
120
+ what reasoning actually costs.
121
+ - **`escha.__version__`** — `python -c "import escha; print(escha.__version__)"`. Read from the
122
+ installed distribution metadata, so it cannot drift from the wheel it shipped in. Paste it
123
+ into bug reports.
124
+ - **`DETERMINISTIC=1`** sets `--enable-deterministic-inference` — **not available on consumer
125
+ Blackwell (RTX 50 / sm_120)**, where the deterministic attention kernel exceeds the 104 KB
126
+ shared-memory-per-block limit and the server exits at startup. `serve.sh` forwards any extra
127
+ arguments straight to `launch_server`, so a flag it does not model is still reachable.
128
+
129
+ ## Running on your GPU
130
+
131
+ The weights are **10.15 GB**. What changes across cards is the **compute-capability knob** (one
132
+ attention-backend override on the newest cards) and the **VRAM budget** (context length, the
133
+ CUDA-graph batch list, and whether `lm_head` is kept int8-as-stored). The kernel launch route is
134
+ chosen for you at runtime — you never set it.
135
+
136
+ > **Measured on three cards: a 32 GB RTX 5090 (sm_120), a 24 GB RTX 4090 (sm_89) and a 24 GB
137
+ > RTX 3090 (sm_86)**; the 4090 is also where the model's thinking-on benchmarks were produced.
138
+ > Per-GPU tokens/s live on the
139
+ > [model card](https://huggingface.co/EschaLabs/Qwen3.8-27B-Escha-W2#performance-across-gpus). The
140
+ > **16 GB and 40 GB+** rows below are still *configuration* guidance derived from the model's size
141
+ > and the wheel's architecture coverage, **not measurements** — we would rather ship a gap than a
142
+ > number we have not run.
143
+
144
+ ### By compute capability (architecture)
145
+
146
+ | Arch | Compute cap | Cards | What you set |
147
+ |---|---|---|---|
148
+ | Ampere | sm_80 / sm_86 | A100, A6000, RTX 3090 | route auto-selects `lovelace`, but **`ESCHA_ROUTE=blackwell` measured 1.72× faster at batch 1 on an RTX 3090** (23.6 → 40.7 tok/s; parity at batch 2–16) — set it if you serve a single user |
149
+ | Ada | sm_89 | RTX 4090, L40S | nothing — route auto-selects `blackwell` (**the validated card**) |
150
+ | Blackwell (consumer, 32 GB) | sm_120 | RTX 5090 | **`ATTN_BACKEND=triton`** — measured, see *By VRAM* below |
151
+ | Hopper | sm_90 | H100 | nothing (route defaults `blackwell`; A/B `ESCHA_ROUTE` — Hopper is a best-guess) |
152
+ | Blackwell (data-center) | sm_100 | B200 | nothing — route auto-selects `blackwell` |
153
+ | Blackwell (consumer) | sm_120 | RTX 50-series | **`ATTN_BACKEND=triton`** (the default flashinfer backend asserts on this hybrid architecture at sm_120) |
154
+
155
+ Confirm the fatbin covers your card and the active route with:
156
+ ```bash
157
+ python -c "import escha,torch; torch.zeros(1,device='cuda'); print('route', torch.ops.escha.escham_route())"
158
+ # expected: route 2 (blackwell) on sm_89/90/100/120 — incl. RTX 4090/5090; route 1 (lovelace) on sm_80/86
159
+ # Both routes are bit-identical launch geometries — forcing one is a performance choice, not a
160
+ # correctness one. Worth A/B'ing on Ampere (measured 1.72x at bs=1) and on Hopper (best-guess default).
161
+ ```
162
+
163
+ ### By VRAM
164
+
165
+ This checkpoint is text-only, so `SGLANG_VLM_TEXT_ONLY=1` is the `serve.sh` default.
166
+
167
+ **int8-as-stored `lm_head`.** The runtime can keep `lm_head` int8 in VRAM instead of dequantizing
168
+ it to fp16 at load: ~1.2 GB instead of ~2.4 GB, read on **every** decode step, so it buys both VRAM
169
+ and single-user decode bandwidth. `INT8=auto` (the default) turns it on at ≤ 24 GB VRAM
170
+ (`SGLANG_INT8_AUTO_VRAM_GB`) and off above; `INT8=on` / `INT8=off` force it. Note this is narrower
171
+ than the mixture-of-experts runtime: on this dense model only `lm_head` is affected —
172
+ `embed_tokens` is a row *lookup*, so keeping it int8 would save VRAM but ~no decode bandwidth, and
173
+ it stays on the dequant path.
174
+
175
+ **CUDA graphs matter on this model** (`GRAPHS=1`, the `serve.sh` default). It is a hybrid
176
+ architecture that launches many small kernels per token, so eager decode is launch-bound. Use
177
+ `GRAPHS=0` only to debug a capture failure.
178
+
179
+ | VRAM | Example | Suggested launch |
180
+ |---|---|---|
181
+ | **16 GB** | RTX 5060 Ti, RTX 5080 | `ATTN_BACKEND=triton MEM=0.90 CTXLEN=16384 CUDA_GRAPH_BS="1 2 4 8" bash serve.sh` — 10.15 GB of weights leaves a small pool; drop `CTXLEN` first if capture OOMs. Untested. |
182
+ | **24 GB** | RTX 4090, RTX 3090, L40S, A6000 | `MODEL=... bash serve.sh` — the shipped defaults (`MEM=0.72 CTXLEN=65536 MAMBA_RATIO=0.3 RADIX=0 GRAPHS=1 CUDA_GRAPH_BS="1 2 4 8 12 16"`). **Validated on RTX 4090 and RTX 3090.** Single-user profile: the recurrent pool clamps it to **8–9 streams**, so the `12`/`16` capture entries are dropped. For throughput use `MEM=0.86 CTXLEN=32768 MAXREQ=32 MAXMAMBA=32 CUDA_GRAPH_BS="1 2 4 8 12 16 24 32"` (4090: 649 tok/s @ 16; 3090: 383). On a 3090 add `ESCHA_ROUTE=blackwell` for single-user work. |
183
+ | **32 GB** | RTX 5090 | `ATTN_BACKEND=triton MEM=0.85 CTXLEN=65536 MAXREQ=32 MAXMAMBA=32 CUDA_GRAPH_BS="1 2 4 8 12 16 24 32" bash serve.sh` — **measured**: 87.1 tok/s bs=1, 955 tok/s peak at 16 streams, 28.2 GB peak of 32.6. `INT8=on` adds +7.2–7.4% decode at bs=1, paired A/B (and +12.2% KV tokens, not free VRAM — the saved ~1.2 GB is recycled into the pool). |
184
+ | **40 GB+** | A100, H100, L40S-48 | `MEM=0.85 CTXLEN=131072 bash serve.sh`; int8 auto-OFF, raise `CTXLEN`/concurrency freely. Untested. |
185
+
186
+ **Four settings that are worth getting right:**
187
+
188
+ 1. **`CUDA_GRAPH_BS` must include your maximum batch size — and must not exceed 32.** A batch size
189
+ that was not captured runs *eager* (the server logs `cuda graph: False` at that
190
+ `#running-req`), which reads roughly 15% low. 32 is the decode kernel's maximum M
191
+ (`python -c "import torch, escha; print(torch.ops.escha.escham_decode_gemv_max_m())"`); beyond it
192
+ a batch falls through to a large-M path meant for prefill, and the runtime declines to capture a
193
+ graph that would bake in the wrong kernel. The default list stops at 16 because that is where
194
+ aggregate throughput peaks on a 4090 — extend it to `"1 2 4 8 12 16 24 32"` if you actually serve
195
+ more than 16 concurrent streams.
196
+ 2. **Recurrent state bounds concurrency at short context; the KV pool bounds it at long context.**
197
+ Every stream holds ~0.15 GB of `ssm_state` regardless of context length — that is the limit with
198
+ short prompts. With long ones the KV pool binds first: `max_total_num_tokens` (printed at
199
+ startup) divided by your typical ISL+OSL is your real stream ceiling. **If the server logs a
200
+ `#running-req` below the batch you requested, the surplus is queueing and your TTFT includes
201
+ queue time** — measured on a 3090 at 2048/2048: asked 32, ran 18, mean TTFT 140 s, which reads
202
+ like a prefill catastrophe and is not one.
203
+ 3. **`RADIX=0` unless you have measured that prefix caching helps you.** On this hybrid
204
+ architecture the radix cache is incompatible with the overlap scheduler, so `RADIX=1` *silently
205
+ disables it* — the server logs `Disabling overlap schedule since mamba no_buffer is not
206
+ compatible with overlap schedule`. Keep `RADIX=1` only for multi-turn agent traffic that
207
+ genuinely reuses long prefixes.
208
+ 4. **If startup OOMs, step `MEM` DOWN, not up.** `mem-fraction-static` sizes the pool the KV cache
209
+ is carved from, so *too low* also fails, with the opposite message ("Not enough memory. Please
210
+ try to increase --mem-fraction-static"). The two failures are: pool-too-small at startup (raise
211
+ `MEM`) and capture-OOM after the weights load (lower `MEM`, or shorten `CUDA_GRAPH_BS`).
212
+
213
+ **Concurrency.** This is a **hybrid** model: most layers are linear-attention with a per-request
214
+ recurrent-state pool, so max concurrency is governed by that pool (`MAXMAMBA`), *not* by KV memory.
215
+ The effective `max_running_requests` is clamped to `min(MAXREQ, MAXMAMBA // ratio)`, ratio ≈ 4 with
216
+ the radix cache on, ≈ 1 with `RADIX=0`. A startup log line reports the clamp.
217
+
218
+ ## Bounded thinking (`thinking_budget`)
219
+
220
+ **Read this before running any benchmark or agent against a thinking-ON server.** With thinking on,
221
+ the model can spend the entire `max_tokens` budget reasoning and never reach its answer. You get:
222
+
223
+ ```
224
+ finish_reason: "length", content: null # all tokens went to reasoning_content
225
+ ```
226
+
227
+ An eval harness scores that as **wrong**, not as slow, and an agent framework sees a no-op. Most
228
+ harnesses cap `max_tokens` at 512–4k, which is where this bites. How long the model thinks varies
229
+ run to run, so a cap that works on one prompt can fail on the next — you want a *guarantee*, not a
230
+ bigger number. `reasoning_effort: "low"` *asks* for less; it does not bound it.
231
+
232
+ A **thinking budget** is that guarantee: after N reasoning tokens the server forces `</think>`, so
233
+ the model must answer within the remaining budget. `serve.sh` enables the processor by default
234
+ (`ENABLE_CLP=1`); pass the budget per request:
235
+
236
+ ```bash
237
+ MODEL=/path/to/model bash serve.sh # ENABLE_CLP=1 is the default
238
+ python thinking_budget.py --model /path/to/model > clp.json # one-time payload
239
+ ```
240
+
241
+ ```jsonc
242
+ { "messages": [ /* ... */ ], "max_tokens": 32768,
243
+ "chat_template_kwargs": {"enable_thinking": true, "reasoning_effort": "xhigh"},
244
+ "custom_logit_processor": "<contents of clp.json>", // SGLang extension
245
+ "custom_params": {"thinking_budget": 28672} }
246
+ ```
247
+
248
+ Leave real headroom between `thinking_budget` and `max_tokens` for the answer itself — at least
249
+ ~512 tokens, and far more for a task that answers with code. **Every published thinking-on
250
+ benchmark number for this model (GPQA-Diamond, LiveCodeBench) was produced with `thinking_budget`
251
+ 28,672 inside `max_tokens` 32,768** (so 4,096 tokens always remain for the answer), at
252
+ `reasoning_effort: "xhigh"`. The commonsense suite ran thinking-off and uses none of this.
253
+
254
+ `thinking_budget.py` reads `<think>` / `</think>` from your model's own tokenizer, so it works for
255
+ any Escha checkpoint that has a thinking mode. (SGLang's built-in
256
+ `Qwen3ThinkingBudgetLogitProcessor` hardcodes Qwen3's token ids, which are wrong for this model's
257
+ ~248k vocabulary — hence the helper.) Verify against a running server with
258
+ `python thinking_budget.py --model <dir> --demo`.
259
+
260
+ If you would rather not think at all, `THINK=0` patches the model's own chat template to
261
+ default-closed — and also suppresses the reasoning-effort instruction the template would otherwise
262
+ inject into the system prompt — at some cost in reasoning-dependent accuracy.
263
+
264
+ ## Troubleshooting
265
+
266
+ - **`subprocess.CalledProcessError` from `gcc` during "Capture cuda graph".** Not an escha or
267
+ sglang fault — your host C compiler cannot build triton's shim, and the traceback's visible
268
+ frames (`cuda_graph_runner.py`, `triton_backend.py`) all point elsewhere. Run the compiler
269
+ preflight in *Requirements*. On slim container images a `libisl` registered in dpkg but missing
270
+ from disk breaks `cc1` while `gcc --version` still works.
271
+
272
+ - **`finish_reason: "length"` with `content: null` on a thinking-ON server.** The reasoning ran past
273
+ your `max_tokens`. Raise the cap or, better, set a
274
+ [thinking budget](#bounded-thinking-thinking_budget) so an answer is always produced.
275
+
276
+ - **HTTP 400 mentioning "Unexpected reasoning effort".** `reasoning_effort` must be exactly
277
+ `xhigh`, `medium` or `low`. The chat template raises on anything else.
278
+
279
+ - **`... has no SGLang implementation and the Transformers implementation is not compatible`.** This
280
+ error is usually a **red herring** — the fork *does* register this architecture; the real cause is
281
+ an exception while *importing* the model module (typically an over-new `nvidia-cutlass-dsl`,
282
+ which the wheel pins `<4.4.0`). Surface the true traceback with:
283
+ ```bash
284
+ python -c "import sglang.srt.models.qwen3_5"
285
+ ```
286
+
287
+ - **`ValueError: Invalid quantization method ...`.** The wheel's registry ships exactly two
288
+ methods, `escha` (dense) and `eschamoe` (mixture-of-experts). Check your model's
289
+ `quantize_config.json` / `config.json` names one of them, and that you installed *this* wheel
290
+ (`python -c "import escha; print(escha.__version__)"` should print `1.1.0+qwen3dense`) — the
291
+ 1.0.x wheels registered `eschamoe` only and cannot load a dense model at all.
292
+
293
+ - **`ModuleNotFoundError: pybase64` / `msgspec` / `xgrammar` / `flashinfer` at startup.** The wheel
294
+ declares the full runtime dependency closure, so a clean `pip install ./escha-*.whl` pulls
295
+ these automatically. If you see this, you likely installed the wheel with `--no-deps` or into a
296
+ venv where pip could not resolve them — reinstall without `--no-deps`.
297
+
298
+ - **`ImportError: undefined symbol: _ZN3c10...` on `import escha`.** Wrong torch. The wheel does
299
+ not pin it, so an unpinned install leaves you on a newer minor. Check with
300
+ `python -c "import torch;print(torch.__version__)"` — it must be `2.9.x+cu12`. Fix:
301
+ `pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu128`.
302
+
303
+ - **`PermissionError` on a CUDA `bin` directory during startup.** `TRITON_PTXAS_PATH` must point at
304
+ the ptxas **binary**, not at the directory containing it — `[[ -x <dir> ]]` is true for
305
+ directories, so a shell profile that sets the directory passes a naive check and then fails
306
+ inside triton. `serve.sh` detects and repairs this case; if you launch `launch_server` directly,
307
+ fix the variable yourself (or just unset it — triton ships its own ptxas).
308
+
309
+ - **A local benchmark client reporting HTTP 504 on `127.0.0.1`.** Check for an `HTTP_PROXY` /
310
+ `ALL_PROXY` in your environment without `no_proxy=127.0.0.1`. Proxies time out multi-minute
311
+ requests and synthesize a 504 while the server is still working fine.
312
+
313
+ - **OOM at startup naming a PID that is not yours.** A previous `sglang::scheduler` child is still
314
+ holding the GPU — killing the `launch_server` parent does not reap it. Check
315
+ `nvidia-smi --query-compute-apps=pid,used_memory --format=csv` and kill that PID. (Match on the
316
+ process *name*; a `pkill -f sglang` pattern also matches the shell running it.)
317
+
318
+ - **Garbage / fluent-nonsense generations.** Check `transformers` first (must be ≥ 5.8); the
319
+ transformers 5.x "downgrade to 4.57.1" RoPE warning is benign — do not act on it.
320
+
321
+ - **Two greedy requests give different text.** Expected: greedy is not bit-reproducible across
322
+ requests, because fp16 accumulation order depends on batch composition and a long reasoning chain
323
+ diverges once a near-tie flips. Use `DETERMINISTIC=1` when you need reproducibility.
324
+
325
+ - **Structured output (`response_format` / JSON schema) on a memory-tight card.** The grammar path
326
+ needs ~0.5 GB of extra headroom the first time it runs (xgrammar + its sampler bookkeeping). On a
327
+ card packed to the edge, budget for it — lower `CTXLEN` a notch if a first structured request is
328
+ tight.
sglang/escha-1.1.0+qwen3dense-cp312-cp312-manylinux_2_28_x86_64.whl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3897ab095afd5bd45ff225eb27294bd163408871157422a6195534d341f49a51
3
+ size 16750938
sglang/serve.sh ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Serve a Qwen3.8-27B 2-/3-bit `escha` (dense) export with escha-sglang, OpenAI-compatible,
3
+ # usable by any OpenAI-compatible client. The `escha` quant method is auto-detected from
4
+ # config.json — nothing here needs to name it.
5
+ #
6
+ # Linux x86-64 + NVIDIA CUDA 12.x only (native or WSL2). See ../README.md and INSTALL.md.
7
+ #
8
+ # Required: set MODEL to your downloaded model dir (a flat folder of safetensors +
9
+ # config + tokenizer — the model repo has no nested subfolder). Everything else has
10
+ # safe, MEASURED defaults for a 24 GB card.
11
+ # MODEL=./Qwen3.8-27B-Escha-W2 bash serve.sh
12
+ set -euo pipefail
13
+
14
+ # ---- paths ----------------------------------------------------------------------------------
15
+ MODEL=${MODEL:?set MODEL=/path/to/your/escha model dir}
16
+ SERVED_NAME=${SERVED_NAME:-escha-qwen38-27b-w2} # opencode/LM Studio must use this id verbatim
17
+ VENV=${VENV:-} # optional: path to your venv; if set, we activate it
18
+ ESCHA_SRC=${ESCHA_SRC:-} # optional: path to escha *source* tree's src/ (only if not pip-installed)
19
+
20
+ # ---- server ---------------------------------------------------------------------------------
21
+ HOST=${HOST:-127.0.0.1} # 0.0.0.0 to serve other machines — do this behind a VPN or tunnel and
22
+ # set API_KEY; never open the raw port to the internet.
23
+ PORT=${PORT:-30000}
24
+ API_KEY=${API_KEY:-} # REQUIRED if HOST=0.0.0.0 / exposed: clients must send this as the
25
+ # Bearer key. Empty = no auth (fine only for localhost).
26
+ MEM=${MEM:-0.72} # mem-fraction-static. 0.72 is the MEASURED default for a 24 GB card at
27
+ # CTXLEN 65536 with CUDA graphs on: capture needs headroom, and 0.78
28
+ # left none once the int8 head is resident. Too LOW is the other
29
+ # failure mode — the server exits at startup with "Not enough memory.
30
+ # Please try to increase --mem-fraction-static". If capture OOMs,
31
+ # step DOWN 0.70 / 0.68 rather than up. On a >24 GB card raise it.
32
+ CTXLEN=${CTXLEN:-65536} # the model's own limit is 262144; 65536 is what fits a 24 GB card
33
+ # alongside the weights. Raise on a bigger card, lower with MEM.
34
+ CHUNK=${CHUNK:-2048} # chunked-prefill-size. Bounds the activation spike on a long prompt;
35
+ # leave it alone unless you are tuning TTFT on a large card.
36
+ MAXREQ=${MAXREQ:-0} # max concurrent requests; 0 = auto.
37
+ MAXMAMBA=${MAXMAMBA:-0} # --max-mamba-cache-size; 0 = auto.
38
+ MAMBA_RATIO=${MAMBA_RATIO:-0.3} # --mamba-full-memory-ratio. 0.3 (not the 0.9 default) is what
39
+ # leaves the pool room for KV *and* CUDA-graph capture at CTXLEN
40
+ # 65536 on 24 GB. Raise it on a card with spare VRAM.
41
+
42
+ # ---- behavior -------------------------------------------------------------------------------
43
+ GRAPHS=${GRAPHS:-1} # 1 = CUDA graphs ON (DEFAULT). This is a hybrid model: many small
44
+ # kernels per token, so eager decode is launch-bound. Set GRAPHS=0
45
+ # only to debug a capture failure; expect several-fold slower decode.
46
+ CUDA_GRAPH_BS=${CUDA_GRAPH_BS:-"1 2 4 8 12 16"} # batch sizes captured when GRAPHS=1.
47
+ # HEADS UP: at the DEFAULT MEM/MAMBA_RATIO below, a 24 GB card's
48
+ # recurrent pool admits only 8-9 streams, so max_running_requests
49
+ # clamps there and the 12/16 entries here are DROPPED (the startup
50
+ # log prints the list actually captured). They start mattering once
51
+ # you raise MAXREQ/MAXMAMBA/MEM. 32 is the decode kernel's max M, so
52
+ # "1 2 4 8 12 16 24 32" captures fine on a throughput profile. Past
53
+ # 32 a batch falls through to a large-M path meant for prefill, and
54
+ # the server refuses to capture it.
55
+ THINK=${THINK:-1} # 1 = thinking-ON + --reasoning-parser qwen3 (simplest, always works).
56
+ # 0 = thinking-OFF: patches the model's chat template to default-closed
57
+ # AND drops the reasoning parser.
58
+ # A per-request chat_template_kwargs {"enable_thinking": false} is
59
+ # honoured on a THINK=1 server, so THINK=0 is not required for
60
+ # clients that disable thinking per call. With thinking ON, the
61
+ # reasoning arrives in `reasoning_content` and the answer in
62
+ # `content`: read BOTH.
63
+ # This model also takes chat_template_kwargs {"reasoning_effort":
64
+ # "xhigh"|"medium"|"low"} (default xhigh). An unrecognised value
65
+ # makes the template raise, which surfaces as a 400.
66
+ RADIX=${RADIX:-0} # 0 = prefix caching OFF (DEFAULT here). On this hybrid architecture the
67
+ # radix cache without speculative decoding disables the overlap
68
+ # scheduler, which costs more than the prefix reuse returns. Set
69
+ # RADIX=1 only if you have measured a win on your own workload.
70
+ ESCHA_ROUTE=${ESCHA_ROUTE:-} # kernel launch geometry; empty = auto-select per GPU. The two
71
+ # routes are BIT-IDENTICAL, so this is purely a perf choice. On
72
+ # AMPERE (sm_80/86) auto picks `lovelace` and that is the SLOWER one
73
+ # at batch 1: an RTX 3090 measured 23.6 -> 40.7 tok/s (1.72x) with
74
+ # ESCHA_ROUTE=blackwell. Parity at batch 2-16, so set it for
75
+ # single-user serving and leave it auto for batched.
76
+ if [[ -n "$ESCHA_ROUTE" ]]; then export ESCHA_ROUTE; fi # NB: `[ .. ] && export` would abort
77
+ # the script under `set -e` when unset.
78
+ ATTN_BACKEND=${ATTN_BACKEND:-} # full-attention backend override. Empty = sglang default (fine on
79
+ # Ada/4090). REQUIRED on consumer Blackwell (RTX 50 / sm_120): the
80
+ # default resolves to flashinfer but the fork asserts triton/trtllm_mha
81
+ # /fa4 for hybrid models on Blackwell -> set ATTN_BACKEND=triton.
82
+ TOOL_PARSER=${TOOL_PARSER:-qwen3_coder} # matches this template's <function=..>/<parameter=..> XML.
83
+ REASONING_PARSER=${REASONING_PARSER:-qwen3}
84
+ ENABLE_CLP=${ENABLE_CLP:-1} # 1 = --enable-custom-logit-processor (DEFAULT). Needed for the
85
+ # per-request thinking budget (see thinking_budget.py); it is how
86
+ # every published benchmark number for this model was produced.
87
+ TRUNCATE=${TRUNCATE:-1} # 1 = --allow-auto-truncate (DEFAULT). A prompt longer than CTXLEN is
88
+ # truncated instead of HARD-REJECTED. Without it an over-long prompt
89
+ # returns an error that reads like a model failure.
90
+ JSON_WS=${JSON_WS:-1} # 1 = --constrained-json-disable-any-whitespace (DEFAULT).
91
+ # With permissive whitespace, a json_schema `integer`/`number`
92
+ # property that follows a string property can take a spurious
93
+ # leading `-`: the grammar allows a "\n " branch after the
94
+ # colon, and from that off-distribution state the sign/digit choice
95
+ # is near-tied. Compact JSON is what structured-output consumers
96
+ # want anyway. `JSON_WS=0` opts out.
97
+ DETERMINISTIC=${DETERMINISTIC:-0} # 1 = --enable-deterministic-inference. Greedy is NOT
98
+ # bit-reproducible across requests by default (fp16 accumulation
99
+ # order depends on batch composition): long reasoning chains diverge
100
+ # once a near-tie flips. Set 1 for eval reproducibility; expect a
101
+ # throughput cost. NOT AVAILABLE on consumer Blackwell (RTX 50 /
102
+ # sm_120): the deterministic attention kernel asks for 104 KB of
103
+ # shared memory per block, above the sm_120 limit, and the server
104
+ # exits at startup. Works on Ampere / Ada / Hopper.
105
+
106
+ # ---- environment the escha stack needs ------------------------------------------------------
107
+ [[ -n "$VENV" && -f "$VENV/bin/activate" ]] && source "$VENV/bin/activate"
108
+ PYBIN=$(command -v python)
109
+ TORCH_LIB=$("$PYBIN" -c 'import torch,os;print(os.path.join(os.path.dirname(torch.__file__),"lib"))')
110
+ export LD_LIBRARY_PATH="$TORCH_LIB${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # escha._C links libc10
111
+ [[ -n "$ESCHA_SRC" ]] && export PYTHONPATH="$ESCHA_SRC${PYTHONPATH:+:$PYTHONPATH}"
112
+ export SGLANG_MAMBA_CONV_DTYPE=float16 # must match --dtype float16 or causal_conv1d crashes
113
+ export SGLANG_DISABLE_CUDNN_CHECK=1
114
+ # This checkpoint is TEXT-ONLY (the vision tower is in the quant `ignore` list — the checkpoint
115
+ # has zero visual.* tensors). Default to NOT instantiating the vision tower: it saves VRAM on
116
+ # every card, and prevents an image request from silently decoding through a random-init tower
117
+ # (garbage instead of an error).
118
+ export SGLANG_VLM_TEXT_ONLY=${SGLANG_VLM_TEXT_ONLY:-1}
119
+ export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True # correct var name — cuts reserved-vs-allocated
120
+ export PYTORCH_ALLOC_CONF=expandable_segments:True # (alias on newer torch; keep both)
121
+ export HF_HUB_OFFLINE=1
122
+
123
+ # ---- decode/prefill kernel profile ----------------------------------------------------------
124
+ # This is the exact profile every published number for this model was measured on. All five knobs
125
+ # are pinned rather than left to their defaults, because a stale value inherited from a shell
126
+ # would read as a quality regression rather than as a configuration difference.
127
+ #
128
+ # ESCHA_STRICT=1 — refuse to silently fall back to the reference decode path; raise instead.
129
+ # The two flags are mutually exclusive AT IMPORT, so the opposite one must not be in the
130
+ # environment: unset it rather than trusting the caller's shell.
131
+ unset ESCHA_FORCE_REF 2>/dev/null || true
132
+ export ESCHA_STRICT=${ESCHA_STRICT:-1}
133
+ # ESCHA_MULTI=1 — one fused launch per multi-shard projection (q+k+v, gate+up) instead of a
134
+ # per-shard loop. This is the default; pinned because it is load-bearing for decode latency.
135
+ export ESCHA_MULTI=${ESCHA_MULTI:-1}
136
+ # ESCHA_PREFILL=fused — run prefill through the fused GEMM rather than a dense reconstruction.
137
+ export ESCHA_PREFILL=${ESCHA_PREFILL:-fused}
138
+ # ESCHA_PREFILL_ACC=mixed / ESCHA_PREFILL_FP16ACC=0 — prefill accumulation precision. Under
139
+ # ESCHA_STRICT every shard's prefill goes through this path, so these govern prefill numerics
140
+ # for the whole model. "mixed" is the validated setting.
141
+ export ESCHA_PREFILL_ACC=${ESCHA_PREFILL_ACC:-mixed}
142
+ export ESCHA_PREFILL_FP16ACC=${ESCHA_PREFILL_FP16ACC:-0}
143
+ # ESCHA_DENSE_FUSED_EPI=0 — the fused split-K epilogue tail is OFF. It is not a numerics-neutral
144
+ # speedup on this model (a measured multi-point accuracy regression on MATH-500), and it is the
145
+ # only process-global mutable state on the decode side: a leaked environment value would look
146
+ # like a quantization regression. Pinned off.
147
+ export ESCHA_DENSE_FUSED_EPI=${ESCHA_DENSE_FUSED_EPI:-0}
148
+ # ESCHA_ROUTE is now a first-class knob above (see the AMPERE note there) — it is no longer
149
+ # "not set here". Other passthrough knobs, still not set here: ESCHA_KGROUP=0 reverts mixed-rate
150
+ # projections to a per-shard launch (debug only).
151
+
152
+ # ---- int8-as-stored lm_head -----------------------------------------------------------------
153
+ # The runtime can keep lm_head INT8 in VRAM ("int8-as-stored") instead of dequantizing it to fp16
154
+ # at load. On this dense model the fp16 head is ~2.4 GB of the bytes read EVERY decode step vs
155
+ # ~1.2 GB as stored, so it is worth real single-user throughput plus ~1.2 GB of VRAM. Unlike the
156
+ # mixture-of-experts runtime, only lm_head is affected here: embed_tokens is a row LOOKUP, so
157
+ # keeping it int8 would save VRAM but ~no decode bandwidth, and it stays on the dequant path.
158
+ # INT8=auto (default) — VRAM-threshold rule: ON at <= SGLANG_INT8_AUTO_VRAM_GB (24), OFF above.
159
+ # INT8=on — force ON: single-user / latency-sensitive serving on ANY card size.
160
+ # INT8=off — force OFF: batched / high-concurrency throughput serving on >24 GB cards.
161
+ INT8=${INT8:-auto}
162
+ case "$INT8" in
163
+ on) export SGLANG_INT8_LM_HEAD=1 ;;
164
+ off) export SGLANG_INT8_LM_HEAD=0 ;;
165
+ auto) : ;; # leave SGLANG_INT8_LM_HEAD as the caller set it (or unset -> AUTO)
166
+ *) echo "[serve] ERROR: INT8 must be auto|on|off (got '$INT8')" >&2; exit 1 ;;
167
+ esac
168
+
169
+ # ---- ptxas discovery ------------------------------------------------------------------------
170
+ # * Triton — which torch already pulls in — SHIPS a matching ptxas at
171
+ # site-packages/triton/backends/nvidia/bin/ptxas. Probe that FIRST: a CUDA toolkit is NOT a
172
+ # requirement of this runtime.
173
+ # * Validate whatever the caller exported. A common shell profile sets TRITON_PTXAS_PATH to the
174
+ # CUDA *bin directory*; `[[ -x <dir> ]]` is TRUE for directories, so a naive check accepts it
175
+ # and triton then dies mid-startup with a permission error on that directory. Require a
176
+ # regular file (-f) AND executable (-x); if a directory was given, try <dir>/ptxas first.
177
+ _ptxas_ok() { [[ -n "${1:-}" && -f "$1" && -x "$1" ]]; }
178
+ if ! _ptxas_ok "${TRITON_PTXAS_PATH:-}"; then
179
+ if [[ -n "${TRITON_PTXAS_PATH:-}" ]]; then
180
+ if _ptxas_ok "${TRITON_PTXAS_PATH%/}/ptxas"; then
181
+ TRITON_PTXAS_PATH="${TRITON_PTXAS_PATH%/}/ptxas"
182
+ echo "[serve] TRITON_PTXAS_PATH was a directory; using $TRITON_PTXAS_PATH"
183
+ else
184
+ echo "[serve] ignoring TRITON_PTXAS_PATH='$TRITON_PTXAS_PATH' (not an executable file)"
185
+ TRITON_PTXAS_PATH=""
186
+ fi
187
+ fi
188
+ if [[ -z "${TRITON_PTXAS_PATH:-}" ]]; then
189
+ for p in \
190
+ "$("$PYBIN" -c 'import os,triton;print(os.path.join(os.path.dirname(triton.__file__),"backends","nvidia","bin","ptxas"))' 2>/dev/null)" \
191
+ /usr/local/cuda*/bin/ptxas \
192
+ "$(command -v ptxas 2>/dev/null)"; do
193
+ _ptxas_ok "$p" && { TRITON_PTXAS_PATH="$p"; break; }
194
+ done
195
+ fi
196
+ export TRITON_PTXAS_PATH
197
+ fi
198
+ : "${TRITON_PTXAS_PATH:?no usable ptxas found. It normally ships inside the venv with triton (a torch dependency) — check that \`python -c \"import triton\"\` works. Otherwise point TRITON_PTXAS_PATH at a ptxas BINARY (not its directory).}"
199
+
200
+ # Only if you hit it: the first time triton compiles a kernel (during warmup AND during CUDA-graph
201
+ # capture), the multi-GB scheduler must fork() to spawn ptxas. Under the default overcommit
202
+ # heuristic that fork can fail -> the server dies with a triton JIT memory-allocation failure at
203
+ # kernel-compile time. Setting vm.overcommit_memory=1 fixes it. Many boxes never hit this (a warm
204
+ # triton cache, or enough free RAM), so it is advisory. In a container the sysctl is often
205
+ # read-only — then it is the HOST's job.
206
+ if [[ "$(cat /proc/sys/vm/overcommit_memory 2>/dev/null)" == "0" ]]; then
207
+ echo "[serve] note: if startup dies with a triton JIT memory-allocation failure at kernel-" >&2
208
+ echo "[serve] compile time, set 'sudo sysctl -w vm.overcommit_memory=1' (persist via" >&2
209
+ echo "[serve] /etc/sysctl.d/99-escha.conf). In a read-only container this must be set" >&2
210
+ echo "[serve] on the host; otherwise it's safe to ignore." >&2
211
+ fi
212
+
213
+ # Warn when the GPU is already substantially occupied (a foreign server / another model on a
214
+ # shared box is the #1 cause of an immediate, confusing pool-allocation failure at startup).
215
+ _USED_MB=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -dc '0-9')
216
+ if [[ -n "$_USED_MB" && "$_USED_MB" -gt 1024 ]]; then
217
+ echo "[serve] WARNING: GPU already has ${_USED_MB} MiB in use by other processes (nvidia-smi)." >&2
218
+ echo "[serve] The static pool sizing assumes a free GPU — expect an allocation failure" >&2
219
+ echo "[serve] if another server is resident. Check: nvidia-smi" >&2
220
+ fi
221
+
222
+ # ---- assemble flags -------------------------------------------------------------------------
223
+ # Piecewise CUDA graphs stay OFF on this model family in every profile (the fork's piecewise path
224
+ # does not handle its positional encoding), so it is not a knob.
225
+ GRAPH_ARGS=(--disable-piecewise-cuda-graph)
226
+ [[ "$GRAPHS" == "1" ]] && GRAPH_ARGS+=(--cuda-graph-bs $CUDA_GRAPH_BS) || GRAPH_ARGS+=(--disable-cuda-graph)
227
+
228
+ POOL_ARGS=()
229
+ [[ "$MAXREQ" != "0" ]] && POOL_ARGS+=(--max-running-requests "$MAXREQ")
230
+ [[ "$MAXMAMBA" != "0" ]] && POOL_ARGS+=(--max-mamba-cache-size "$MAXMAMBA")
231
+ [[ -n "$MAMBA_RATIO" ]] && POOL_ARGS+=(--mamba-full-memory-ratio "$MAMBA_RATIO")
232
+ RADIX_ARGS=(); [[ "$RADIX" == "0" ]] && RADIX_ARGS+=(--disable-radix-cache)
233
+ ATTN_ARGS=(); [[ -n "$ATTN_BACKEND" ]] && ATTN_ARGS+=(--attention-backend "$ATTN_BACKEND")
234
+ AUTH_ARGS=(); [[ -n "$API_KEY" ]] && AUTH_ARGS+=(--api-key "$API_KEY")
235
+ CLP_ARGS=(); [[ "$ENABLE_CLP" == "1" ]] && CLP_ARGS+=(--enable-custom-logit-processor)
236
+ TRUNC_ARGS=(); [[ "$TRUNCATE" == "1" ]] && TRUNC_ARGS+=(--allow-auto-truncate)
237
+ JSON_ARGS=(); [[ "$JSON_WS" == "1" ]] && JSON_ARGS+=(--constrained-json-disable-any-whitespace)
238
+ DET_ARGS=(); [[ "$DETERMINISTIC" == "1" ]] && DET_ARGS+=(--enable-deterministic-inference)
239
+ if [[ "$HOST" != "127.0.0.1" && -z "$API_KEY" ]]; then
240
+ echo "[serve] WARNING: HOST=$HOST is exposed but API_KEY is empty — no auth. Set API_KEY." >&2
241
+ fi
242
+
243
+ REASON_ARGS=(); TEMPLATE_ARGS=()
244
+ if [[ "$THINK" == "1" ]]; then
245
+ REASON_ARGS+=(--reasoning-parser "$REASONING_PARSER")
246
+ else
247
+ # thinking-OFF: generate a patched chat template (default-closed <think>) from the model's own.
248
+ NOTHINK="${TMPDIR:-/tmp}/escha_nothink_$(basename "$MODEL").jinja"
249
+ "$PYBIN" - "$MODEL" "$NOTHINK" <<'PY'
250
+ import json, os, sys
251
+ mdir, out = sys.argv[1], sys.argv[2]
252
+ src = os.path.join(mdir, "chat_template.jinja")
253
+ tmpl = open(src).read() if os.path.exists(src) else json.load(open(os.path.join(mdir,"tokenizer_config.json")))["chat_template"]
254
+ # Flip the "enable_thinking" default from ON to OFF. TWO edits are needed on this template and
255
+ # only the first is mandatory:
256
+ # (1) the generation-prompt block that decides whether <think> is left open or pre-closed;
257
+ # (2) the reasoning-effort gate near the top, which injects a "think carefully..." system
258
+ # instruction whenever enable_thinking is UNDEFINED. Without (2) a thinking-off server
259
+ # still tells the model to reason at length while pre-closing the block it would reason in.
260
+ # Each edit is attempted independently; (1) is required, (2) is best-effort.
261
+ GEN_PROMPT = [
262
+ ("{%- if enable_thinking is defined and enable_thinking is false %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- else %}\n {{- '<think>\\n' }}\n {%- endif %}",
263
+ "{%- if enable_thinking is defined and enable_thinking is true %}\n {{- '<think>\\n' }}\n {%- else %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}"),
264
+ ("{%- set enable_thinking = enable_thinking if enable_thinking is defined else true %}",
265
+ "{%- set enable_thinking = enable_thinking if enable_thinking is defined else false %}"),
266
+ ]
267
+ EFFORT_GATE = [
268
+ ("{%- if enable_thinking is undefined or enable_thinking is true %}",
269
+ "{%- if enable_thinking is defined and enable_thinking is true %}"),
270
+ ]
271
+ patched = tmpl
272
+ for old, new in GEN_PROMPT:
273
+ if old in patched:
274
+ patched = patched.replace(old, new); break
275
+ else:
276
+ sys.exit("THINK=0: could not find the enable_thinking block to patch in this model's chat "
277
+ "template. Serve with THINK=1, or hand-patch the template and pass it via --chat-template.")
278
+ for old, new in EFFORT_GATE:
279
+ if old in patched:
280
+ patched = patched.replace(old, new); break
281
+ open(out, "w").write(patched)
282
+ print(out)
283
+ PY
284
+ TEMPLATE_ARGS+=(--chat-template "$NOTHINK") # no reasoning parser when thinking is pre-closed
285
+ fi
286
+
287
+ echo "[serve] MODEL=$MODEL NAME=$SERVED_NAME $HOST:$PORT MEM=$MEM CTX=$CTXLEN GRAPHS=$GRAPHS THINK=$THINK RADIX=$RADIX INT8=$INT8"
288
+ echo "[serve] tool-call-parser=$TOOL_PARSER ptxas=$TRITON_PTXAS_PATH"
289
+
290
+ exec "$PYBIN" -m sglang.launch_server \
291
+ --model-path "$MODEL" \
292
+ --served-model-name "$SERVED_NAME" \
293
+ --host "$HOST" --port "$PORT" \
294
+ --dtype float16 \
295
+ --mem-fraction-static "$MEM" \
296
+ --context-length "$CTXLEN" \
297
+ --chunked-prefill-size "$CHUNK" \
298
+ --sampling-backend pytorch \
299
+ --trust-remote-code \
300
+ --tool-call-parser "$TOOL_PARSER" \
301
+ "${REASON_ARGS[@]}" \
302
+ "${TEMPLATE_ARGS[@]}" \
303
+ "${POOL_ARGS[@]}" \
304
+ "${RADIX_ARGS[@]}" \
305
+ "${ATTN_ARGS[@]}" \
306
+ "${AUTH_ARGS[@]}" \
307
+ "${CLP_ARGS[@]}" \
308
+ "${TRUNC_ARGS[@]}" \
309
+ "${GRAPH_ARGS[@]}" \
310
+ "${JSON_ARGS[@]}" \
311
+ "${DET_ARGS[@]}" \
312
+ --log-level info \
313
+ "$@" # forward any extra sglang flag verbatim
sglang/thinking_budget.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Bounded thinking for Escha `qwen3_5` models on SGLang.
3
+
4
+ Problem this solves: with thinking on, the model may spend the whole
5
+ `max_tokens` budget reasoning and never emit an answer. The response then comes
6
+ back as `finish_reason: "length"` with `content: null` (all tokens went to
7
+ `reasoning_content`), which an eval harness scores as *wrong* rather than as
8
+ *slow*, and an agent framework sees as a no-op.
9
+
10
+ A thinking budget fixes it: once the reasoning has run for N tokens, the server
11
+ forces `</think>` so the model must produce its answer inside the remaining
12
+ budget. This is SGLang's stock `ThinkingBudgetLogitProcessor`; the only reason a
13
+ helper is needed is that SGLang's built-in Qwen3 subclass hardcodes Qwen3's
14
+ token ids (151667/151668), which are wrong for this model's ~248k vocab — so we
15
+ look the ids up from the model's own tokenizer.
16
+
17
+ Serve with the processor enabled:
18
+
19
+ ENABLE_CLP=1 MODEL=/path/to/model bash serve.sh
20
+
21
+ Emit the payload once, then reuse it for every request:
22
+
23
+ python thinking_budget.py --model /path/to/model > clp.json
24
+
25
+ Send it (any OpenAI client — the two extra fields are SGLang extensions):
26
+
27
+ {"messages": [...], "max_tokens": 4096,
28
+ "chat_template_kwargs": {"enable_thinking": true, "reasoning_effort": "xhigh"},
29
+ "custom_logit_processor": <contents of clp.json>,
30
+ "custom_params": {"thinking_budget": 2048}}
31
+
32
+ Budget guidance: leave enough headroom between `thinking_budget` and
33
+ `max_tokens` for the answer itself — at least ~512 tokens, and far more for a
34
+ task that answers with code. Every published benchmark number for this model
35
+ was produced with `thinking_budget` 28,672 inside `max_tokens` 32,768 (so
36
+ 4,096 tokens always remain for the answer), at `reasoning_effort: "xhigh"`.
37
+
38
+ `reasoning_effort` is a second, independent lever this model exposes through
39
+ `chat_template_kwargs` — `"xhigh"` (default), `"medium"` or `"low"`. It asks
40
+ for shorter reasoning; the budget *enforces* it. They compose.
41
+
42
+ Self-check (`--demo`) needs a running server and reports, for one prompt, the
43
+ finish_reason / reasoning length / answer length with and without a budget.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import json
50
+ import sys
51
+
52
+ _THINK_OPEN = "<think>"
53
+ _THINK_CLOSE = "</think>"
54
+
55
+
56
+ def token_ids(model_dir: str) -> tuple[int, int, int]:
57
+ """(start, end, newline) token ids from the model's own tokenizer."""
58
+ from transformers import AutoTokenizer
59
+
60
+ tok = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=False)
61
+ start, end = tok.convert_tokens_to_ids([_THINK_OPEN, _THINK_CLOSE])
62
+ unk = getattr(tok, "unk_token_id", None)
63
+ if start is None or end is None or start == unk or end == unk:
64
+ raise SystemExit(
65
+ f"{model_dir}: tokenizer has no {_THINK_OPEN}/{_THINK_CLOSE} tokens — "
66
+ "this model has no thinking mode, so a thinking budget does not apply."
67
+ )
68
+ newline = tok.encode("\n", add_special_tokens=False)
69
+ if len(newline) != 1:
70
+ raise SystemExit(f"unexpected newline tokenization: {newline!r}")
71
+ return int(start), int(end), int(newline[0])
72
+
73
+
74
+ def payload(model_dir: str) -> str:
75
+ """The JSON string to pass as `custom_logit_processor`."""
76
+ from sglang.srt.sampling.custom_logit_processor import (
77
+ ThinkingBudgetLogitProcessor,
78
+ )
79
+
80
+ start, end, newline = token_ids(model_dir)
81
+
82
+ # dill serializes this subclass by value; the base class is resolved by name
83
+ # on the server, so the server must run the same SGLang version.
84
+ class EschaThinkingBudgetLogitProcessor(ThinkingBudgetLogitProcessor):
85
+ THINKING_START_TOKEN_ID = start
86
+ THINKING_END_TOKEN_ID = end
87
+ NEW_LINE_TOKEN_ID = newline
88
+
89
+ return EschaThinkingBudgetLogitProcessor.to_str()
90
+
91
+
92
+ def demo(base_url: str, model: str, clp: str, budget: int, max_tokens: int) -> None:
93
+ import urllib.request
94
+
95
+ prompt = (
96
+ "What is the Hubble constant from Planck, and from local distance-ladder "
97
+ "measurements? Give both numbers with units."
98
+ )
99
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
100
+ for label, body in (
101
+ ("no budget ", {}),
102
+ (f"budget {budget}", {"custom_logit_processor": clp,
103
+ "custom_params": {"thinking_budget": budget}}),
104
+ ):
105
+ req = {"model": model, "messages": [{"role": "user", "content": prompt}],
106
+ "max_tokens": max_tokens, "temperature": 0,
107
+ "chat_template_kwargs": {"enable_thinking": True}, **body}
108
+ with opener.open(urllib.request.Request(
109
+ f"{base_url}/chat/completions", json.dumps(req).encode(),
110
+ {"Content-Type": "application/json"}), timeout=1800) as r:
111
+ out = json.load(r)
112
+ choice = out["choices"][0]
113
+ msg = choice["message"]
114
+ reasoning = msg.get("reasoning_content") or ""
115
+ content = msg.get("content") or ""
116
+ print(f"{label}: finish_reason={choice['finish_reason']:>6} "
117
+ f"completion_tokens={out['usage']['completion_tokens']:>6} "
118
+ f"reasoning_chars={len(reasoning):>6} answer_chars={len(content):>5}"
119
+ f"{' <-- NO ANSWER' if not content else ''}")
120
+
121
+
122
+ def main() -> None:
123
+ ap = argparse.ArgumentParser(description=__doc__,
124
+ formatter_class=argparse.RawDescriptionHelpFormatter)
125
+ ap.add_argument("--model", required=True, help="model directory (for the tokenizer)")
126
+ ap.add_argument("--demo", action="store_true", help="probe a running server")
127
+ ap.add_argument("--base-url", default="http://127.0.0.1:30000/v1")
128
+ ap.add_argument("--served-name", default="escha-qwen38-27b-w2")
129
+ ap.add_argument("--budget", type=int, default=2048)
130
+ ap.add_argument("--max-tokens", type=int, default=4096)
131
+ args = ap.parse_args()
132
+
133
+ clp = payload(args.model)
134
+ if args.demo:
135
+ start, end, newline = token_ids(args.model)
136
+ print(f"# {_THINK_OPEN}={start} {_THINK_CLOSE}={end} newline={newline}",
137
+ file=sys.stderr)
138
+ demo(args.base_url, args.served_name, clp, args.budget, args.max_tokens)
139
+ else:
140
+ print(clp)
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()