HCHs commited on
Commit
a92180d
·
verified ·
1 Parent(s): cd55fe8

Add Triton grouped-FP8 runtime and microbatch server

Browse files
NOTICE.md CHANGED
@@ -10,11 +10,17 @@ RivetCoder-9B-A4B combines the following sources:
10
  endpoint. No Qwen weights are included.
11
  - Architectural reference: `Akahsizrr/fuse-1-Lite`, revision
12
  `430c959e47556ae53fed18a9d97f7cf30876e6ff`.
 
 
 
 
 
13
 
14
  Modifications and new work include GLM expert selection, a fixed tied
15
  identity-Hadamard bridge, expert/router weight folding, per-layer Top-4 routing,
16
  token gating, bounded residual scaling, Qwen-supervised routing-control training,
17
- and the custom `fuse_glm` Transformers implementation shipped in this repository.
 
18
 
19
  See `provenance/` for the complete local build record with personal filesystem
20
  paths removed.
 
10
  endpoint. No Qwen weights are included.
11
  - Architectural reference: `Akahsizrr/fuse-1-Lite`, revision
12
  `430c959e47556ae53fed18a9d97f7cf30876e6ff`.
13
+ - Fast-serving kernel reference: `kernels-community/finegrained-fp8`, whose
14
+ Triton grouped-FP8 and activation-quantization kernels are Copyright 2026 The
15
+ Hugging Face Inc. team and licensed under Apache License 2.0. RivetCoder's
16
+ adaptation adds per-output scales, TorchAO expert-bank packing, deterministic
17
+ Top-4 reduction, and automatic Windows MSVC environment discovery.
18
 
19
  Modifications and new work include GLM expert selection, a fixed tied
20
  identity-Hadamard bridge, expert/router weight folding, per-layer Top-4 routing,
21
  token gating, bounded residual scaling, Qwen-supervised routing-control training,
22
+ the custom `fuse_glm` Transformers implementation, and the inference-only
23
+ grouped-FP8 serving runtime shipped in this repository.
24
 
25
  See `provenance/` for the complete local build record with personal filesystem
26
  paths removed.
README.md CHANGED
@@ -129,6 +129,61 @@ TorchAO 0.15.0 also reports that its optional C++ extensions are skipped with
129
  the tested PyTorch 2.12 build. The native CUDA FP8 path used by this checkpoint
130
  still passed quantization, serialization, clean reload, and forward validation.
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  ## Limitations
133
 
134
  - This is an experimental fusion with only 60 routing-control optimizer steps.
@@ -138,13 +193,18 @@ still passed quantization, serialization, clean reload, and forward validation.
138
  with llama.cpp, LM Studio, or Ollama.
139
  - Hardware and software combinations other than the tested stack may need
140
  additional compatibility work.
 
 
141
 
142
- See `provenance/quantization.json` for the local quantization and placement
143
- report. Architecture, source-model, expert-selection, and router-training
144
- provenance are retained from the BF16 repository.
 
145
 
146
  ## License and attribution
147
 
148
  The LFM host remains subject to the included LFM Open License v1.0. GLM-derived
149
- expert tensors retain the included MIT license and attribution. Review
150
- `LICENSE`, `NOTICE.md`, and `licenses/` before redistribution or deployment.
 
 
 
129
  the tested PyTorch 2.12 build. The native CUDA FP8 path used by this checkpoint
130
  still passed quantization, serialization, clean reload, and forward validation.
131
 
132
+ ## Fast grouped-FP8 serving
133
+
134
+ The repository includes an inference-only Triton runtime that replaces the
135
+ Python 16-expert loop with two grouped FP8 GEMMs per fused layer: one combined
136
+ gate/up projection and one down projection. It preserves Top-4 routing and the
137
+ reference FP8 logits while releasing the unpacked expert tensors after runtime
138
+ packing.
139
+
140
+ For direct Transformers use, enable it after loading:
141
+
142
+ ```python
143
+ runtime_report = model.enable_fast_fp8_serving()
144
+ print(runtime_report)
145
+ ```
146
+
147
+ For an OpenAI-compatible, queue-to-completion microbatch server:
148
+
149
+ ```powershell
150
+ pip install -r requirements-serve.txt
151
+ python serve.py `
152
+ --model HCHs/RivetCoder-9B-A4B-FP8 `
153
+ --no-local-files-only `
154
+ --host 0.0.0.0 `
155
+ --port 8000 `
156
+ --max-batch-size 16 `
157
+ --batch-wait-ms 3
158
+ ```
159
+
160
+ On Windows, the first Triton JIT requires Visual Studio 2022 C++ Build Tools.
161
+ The runtime automatically imports the installed Developer environment and
162
+ records the resolved `cl.exe` path in its startup report. The first request for
163
+ a new shape includes autotuning; later calls use the Triton cache.
164
+
165
+ RTX 5070 Ti validation with a one-token full forward produced:
166
+
167
+ | Runtime | Latency | Relative throughput |
168
+ |---|---:|---:|
169
+ | Original TorchAO expert loop | 5.100 s | 1.00x |
170
+ | Grouped-FP8 fast path | 1.079 s | 4.73x |
171
+
172
+ The logits were bit-exact (`MAE=0`, `max error=0`, identical top-1), repeated
173
+ execution was deterministic, and resident VRAM was about 8.43 GiB. With the
174
+ fast path enabled, fixed microbatch throughput scaled as follows:
175
+
176
+ | Batch | Forward latency | Sequences/s |
177
+ |---:|---:|---:|
178
+ | 1 | 1.080 s | 0.93 |
179
+ | 4 | 1.087 s | 3.68 |
180
+ | 8 | 1.086 s | 7.36 |
181
+ | 16 | 1.117 s | 14.33 |
182
+
183
+ These are local full-forward measurements, not standardized generation
184
+ benchmarks. Batch 16 increased throughput about 15.5x over batch 1 while adding
185
+ only about 3.4% latency, which is why the bundled server defaults to batch 16.
186
+
187
  ## Limitations
188
 
189
  - This is an experimental fusion with only 60 routing-control optimizer steps.
 
193
  with llama.cpp, LM Studio, or Ollama.
194
  - Hardware and software combinations other than the tested stack may need
195
  additional compatibility work.
196
+ - The bundled server does not stream tokens and batches only requests with the
197
+ same generation parameters.
198
 
199
+ See `provenance/quantization.json` and `provenance/fast-serving.json` for the
200
+ local quantization, placement, parity, and throughput reports. Architecture,
201
+ source-model, expert-selection, and router-training provenance are retained
202
+ from the BF16 repository.
203
 
204
  ## License and attribution
205
 
206
  The LFM host remains subject to the included LFM Open License v1.0. GLM-derived
207
+ expert tensors retain the included MIT license and attribution. The grouped FP8
208
+ Triton kernels are adapted from Hugging Face's Apache-2.0
209
+ `kernels-community/finegrained-fp8`. Review `LICENSE`, `NOTICE.md`, and
210
+ `licenses/` before redistribution or deployment.
fast_fp8_runtime.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Inference-only grouped FP8 expert runtime for RivetCoder.
2
+
3
+ The Triton kernels in this module are adapted from Hugging Face's
4
+ ``kernels-community/finegrained-fp8`` package (Apache-2.0). The adaptation
5
+ adds per-output weight scales so separately quantized gate/up projections can
6
+ be concatenated without requantizing their checkpoint tensors.
7
+
8
+ This module is intentionally imported lazily. Normal BF16 loading, training,
9
+ and CPU execution do not require Triton or TorchAO.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import gc
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ import torch
22
+ import torch.nn.functional as F
23
+ from torch import nn
24
+
25
+
26
+ def ensure_windows_msvc_environment() -> str | None:
27
+ """Populate the MSVC environment Triton's Windows launcher JIT needs."""
28
+
29
+ if os.name != "nt":
30
+ return None
31
+ configured = os.environ.get("CC")
32
+ if configured and (Path(configured).is_file() or shutil.which(configured)):
33
+ return configured
34
+ compiler = shutil.which("cl.exe")
35
+ if compiler:
36
+ os.environ["CC"] = compiler
37
+ return compiler
38
+
39
+ candidates = [
40
+ Path(os.environ.get("ProgramFiles", r"C:\Program Files"))
41
+ / "Microsoft Visual Studio"
42
+ / "2022"
43
+ / edition
44
+ / "Common7"
45
+ / "Tools"
46
+ / "VsDevCmd.bat"
47
+ for edition in ("Community", "Professional", "Enterprise", "BuildTools")
48
+ ]
49
+ vsdevcmd = next((path for path in candidates if path.is_file()), None)
50
+ if vsdevcmd is None:
51
+ raise RuntimeError(
52
+ "Triton needs an MSVC C compiler on Windows. Install Visual Studio 2022 "
53
+ "C++ Build Tools or launch the server from a Developer PowerShell."
54
+ )
55
+
56
+ command = f'call "{vsdevcmd}" -arch=x64 -host_arch=x64 >nul && set'
57
+ completed = subprocess.run(
58
+ command,
59
+ check=True,
60
+ capture_output=True,
61
+ text=True,
62
+ encoding="utf-8",
63
+ errors="replace",
64
+ shell=True,
65
+ executable=os.environ.get("COMSPEC", "cmd.exe"),
66
+ )
67
+ for line in completed.stdout.splitlines():
68
+ name, separator, value = line.partition("=")
69
+ if separator and name:
70
+ os.environ[name] = value
71
+ compiler = shutil.which("cl.exe")
72
+ if compiler is None:
73
+ raise RuntimeError("VsDevCmd completed but cl.exe is still unavailable")
74
+ os.environ["CC"] = compiler
75
+ return compiler
76
+
77
+
78
+ def _load_triton() -> tuple[Any, Any, Any, Any]:
79
+ ensure_windows_msvc_environment()
80
+ try:
81
+ import triton
82
+ import triton.language as tl
83
+ from torch.library import triton_op, wrap_triton
84
+ except ImportError as error:
85
+ raise RuntimeError(
86
+ "Fast FP8 serving requires Triton. Use a PyTorch build that bundles Triton "
87
+ "or install a Windows-compatible Triton package."
88
+ ) from error
89
+ return triton, tl, triton_op, wrap_triton
90
+
91
+
92
+ triton, tl, triton_op, wrap_triton = _load_triton()
93
+
94
+
95
+ @triton.jit
96
+ def _fp8_per_row_quant_kernel(x_ptr, q_ptr, scale_ptr, K: tl.constexpr):
97
+ row = tl.program_id(axis=0)
98
+ offsets = tl.arange(0, K)
99
+ values = tl.load(x_ptr + row * K + offsets).to(tl.float32)
100
+ scale = tl.maximum(tl.max(tl.abs(values), axis=0) / 448.0, 1.0e-12)
101
+ quantized = (values / scale).to(tl.float8e4nv)
102
+ tl.store(q_ptr + row * K + offsets, quantized)
103
+ tl.store(scale_ptr + row, scale)
104
+
105
+
106
+ @triton_op("rivet_fp8::per_row_quant", mutates_args=())
107
+ def _fp8_per_row_quant(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
108
+ if x.ndim != 2 or not x.is_contiguous():
109
+ raise ValueError("FP8 activation input must be a contiguous 2D tensor")
110
+ if x.shape[1] <= 0 or x.shape[1] & (x.shape[1] - 1):
111
+ raise ValueError("FP8 activation width must be a positive power of two")
112
+ quantized = torch.empty_like(x, dtype=torch.float8_e4m3fn)
113
+ scales = torch.empty(x.shape[0], device=x.device, dtype=torch.float32)
114
+ wrap_triton(_fp8_per_row_quant_kernel)[(x.shape[0],)](
115
+ x,
116
+ quantized,
117
+ scales,
118
+ K=x.shape[1],
119
+ )
120
+ return quantized, scales
121
+
122
+
123
+ @triton.autotune(
124
+ configs=[
125
+ triton.Config({}, num_warps=warps, num_stages=stages)
126
+ for warps in (2, 4, 8, 16)
127
+ for stages in (2, 3, 4, 5)
128
+ ],
129
+ key=["N", "K", "BLOCK_M"],
130
+ )
131
+ @triton.jit
132
+ def _grouped_fp8_linear_kernel(
133
+ A,
134
+ B,
135
+ C,
136
+ AScales,
137
+ BScales,
138
+ Offsets,
139
+ TileOffsets,
140
+ S,
141
+ N: tl.constexpr,
142
+ K: tl.constexpr,
143
+ stride_am,
144
+ stride_ak,
145
+ stride_be,
146
+ stride_bk,
147
+ stride_bn,
148
+ stride_cm,
149
+ stride_cn,
150
+ stride_bs_e,
151
+ stride_bs_n,
152
+ NUM_EXPERTS: tl.constexpr,
153
+ BLOCK_N: tl.constexpr,
154
+ BLOCK_K: tl.constexpr,
155
+ BLOCK_M: tl.constexpr,
156
+ SEARCH_STEPS: tl.constexpr,
157
+ ):
158
+ tile_m = tl.program_id(axis=0)
159
+ tile_n = tl.program_id(axis=1)
160
+ total_tiles = tl.load(TileOffsets + NUM_EXPERTS - 1)
161
+ if tile_m >= total_tiles:
162
+ return
163
+
164
+ low = 0
165
+ high = NUM_EXPERTS
166
+ for _ in tl.static_range(SEARCH_STEPS):
167
+ middle = (low + high) >> 1
168
+ middle_value = tl.load(TileOffsets + middle)
169
+ move_right = middle_value <= tile_m
170
+ low = tl.where(move_right, middle + 1, low)
171
+ high = tl.where(move_right, high, middle)
172
+ expert = low.to(tl.int64)
173
+
174
+ previous = tl.maximum(expert - 1, 0)
175
+ expert_start = tl.where(expert == 0, 0, tl.load(Offsets + previous))
176
+ expert_end = tl.load(Offsets + expert)
177
+ expert_rows = expert_end - expert_start
178
+ expert_tile_start = tl.where(expert == 0, 0, tl.load(TileOffsets + previous))
179
+ local_row_start = (tile_m - expert_tile_start) * BLOCK_M
180
+
181
+ row_offsets = local_row_start + tl.arange(0, BLOCK_M)
182
+ valid_rows = row_offsets < expert_rows
183
+ global_rows = expert_start + row_offsets
184
+ output_offsets = tile_n * BLOCK_N + tl.arange(0, BLOCK_N)
185
+ k_offsets = tl.arange(0, BLOCK_K)
186
+
187
+ a_ptrs = A + global_rows[:, None] * stride_am + k_offsets[None, :] * stride_ak
188
+ b_ptrs = (
189
+ B
190
+ + expert * stride_be
191
+ + output_offsets[None, :] * stride_bn
192
+ + k_offsets[:, None] * stride_bk
193
+ )
194
+ accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
195
+ for _ in range(0, tl.cdiv(K, BLOCK_K)):
196
+ a = tl.load(a_ptrs, mask=valid_rows[:, None], other=0.0)
197
+ b = tl.load(b_ptrs)
198
+ accumulator += tl.dot(a, b)
199
+ a_ptrs += BLOCK_K * stride_ak
200
+ b_ptrs += BLOCK_K * stride_bk
201
+
202
+ activation_scale = tl.load(
203
+ AScales + global_rows,
204
+ mask=valid_rows,
205
+ other=0.0,
206
+ )
207
+ weight_scale = tl.load(
208
+ BScales + expert * stride_bs_e + output_offsets * stride_bs_n,
209
+ )
210
+ accumulator *= activation_scale[:, None] * weight_scale[None, :]
211
+
212
+ if C.dtype.element_ty == tl.bfloat16:
213
+ result = accumulator.to(tl.bfloat16)
214
+ elif C.dtype.element_ty == tl.float16:
215
+ result = accumulator.to(tl.float16)
216
+ else:
217
+ result = accumulator
218
+ c_ptrs = C + global_rows[:, None] * stride_cm + output_offsets[None, :] * stride_cn
219
+ tl.store(c_ptrs, result, mask=valid_rows[:, None])
220
+
221
+
222
+ @triton_op("rivet_fp8::grouped_linear", mutates_args=())
223
+ def _grouped_fp8_linear(
224
+ activations: torch.Tensor,
225
+ weights: torch.Tensor,
226
+ weight_scales: torch.Tensor,
227
+ offsets: torch.Tensor,
228
+ tokens_per_expert: torch.Tensor,
229
+ ) -> torch.Tensor:
230
+ if activations.ndim != 2 or not activations.is_contiguous():
231
+ raise ValueError("activations must be contiguous [routes, hidden]")
232
+ if weights.ndim != 3 or not weights.is_contiguous():
233
+ raise ValueError("weights must be contiguous [experts, output, hidden]")
234
+ if weights.dtype != torch.float8_e4m3fn:
235
+ raise TypeError("weights must use torch.float8_e4m3fn")
236
+ experts, output_size, hidden_size = weights.shape
237
+ if activations.shape[1] != hidden_size:
238
+ raise ValueError("activation/weight hidden dimensions do not match")
239
+ if output_size % 128 or hidden_size % 128:
240
+ raise ValueError("grouped FP8 output and hidden dimensions must be divisible by 128")
241
+ if weight_scales.shape != (experts, output_size):
242
+ raise ValueError("weight_scales must have shape [experts, output]")
243
+ if offsets.shape != (experts,) or tokens_per_expert.shape != (experts,):
244
+ raise ValueError("offset/count tensors must have one value per expert")
245
+
246
+ # TorchAO's checkpoint config uses dynamic PerTensor activation scaling.
247
+ # Match that per routed expert (rather than per row) so prefill follows the
248
+ # same quantization semantics as the original expert-by-expert calls.
249
+ expert_ids = torch.repeat_interleave(
250
+ torch.arange(experts, device=activations.device),
251
+ tokens_per_expert.to(torch.long),
252
+ output_size=activations.shape[0],
253
+ )
254
+ row_max = activations.abs().amax(dim=-1)
255
+ expert_max = torch.zeros(experts, device=activations.device, dtype=activations.dtype)
256
+ expert_max.scatter_reduce_(0, expert_ids, row_max, reduce="amax", include_self=True)
257
+ expert_scales = (expert_max / 448.0).float().clamp_min(1.0e-12)
258
+ activation_scales = expert_scales.index_select(0, expert_ids).contiguous()
259
+ quantized = (
260
+ activations.float()
261
+ .div(activation_scales.unsqueeze(-1))
262
+ .clamp(min=-448.0, max=448.0)
263
+ .to(torch.float8_e4m3fn)
264
+ )
265
+ output = activations.new_empty((activations.shape[0], output_size))
266
+ block_m = min(max(triton.next_power_of_2((activations.shape[0] + experts - 1) // experts), 16), 128)
267
+ tiles_per_expert = (tokens_per_expert + block_m - 1) // block_m
268
+ tile_offsets = torch.cumsum(tiles_per_expert, dim=0, dtype=torch.int32)
269
+ max_m_tiles = triton.cdiv(activations.shape[0], block_m) + experts
270
+ grid = (max_m_tiles, triton.cdiv(output_size, 128))
271
+ wrap_triton(_grouped_fp8_linear_kernel)[grid](
272
+ quantized,
273
+ weights,
274
+ output,
275
+ activation_scales,
276
+ weight_scales,
277
+ offsets,
278
+ tile_offsets,
279
+ activations.shape[0],
280
+ output_size,
281
+ hidden_size,
282
+ quantized.stride(0),
283
+ quantized.stride(1),
284
+ weights.stride(0),
285
+ weights.stride(2),
286
+ weights.stride(1),
287
+ output.stride(0),
288
+ output.stride(1),
289
+ weight_scales.stride(0),
290
+ weight_scales.stride(1),
291
+ NUM_EXPERTS=experts,
292
+ BLOCK_N=128,
293
+ BLOCK_K=128,
294
+ BLOCK_M=block_m,
295
+ SEARCH_STEPS=experts.bit_length(),
296
+ )
297
+ return output
298
+
299
+
300
+ def grouped_fp8_linear(
301
+ activations: torch.Tensor,
302
+ weights: torch.Tensor,
303
+ weight_scales: torch.Tensor,
304
+ offsets: torch.Tensor,
305
+ tokens_per_expert: torch.Tensor,
306
+ ) -> torch.Tensor:
307
+ return torch.ops.rivet_fp8.grouped_linear(
308
+ activations,
309
+ weights,
310
+ weight_scales,
311
+ offsets,
312
+ tokens_per_expert,
313
+ )
314
+
315
+
316
+ def _float8_parts(linear: nn.Linear) -> tuple[torch.Tensor, torch.Tensor]:
317
+ weight = linear.weight
318
+ qdata = getattr(weight, "qdata", None)
319
+ scale = getattr(weight, "scale", None)
320
+ if qdata is None or scale is None or "Float8" not in type(weight).__name__:
321
+ raise TypeError("fast serving requires TorchAO Float8Tensor Linear weights")
322
+ if qdata.dtype != torch.float8_e4m3fn or scale.numel() != 1:
323
+ raise TypeError("fast serving currently supports per-tensor E4M3 TorchAO weights")
324
+ return qdata, scale.reshape(())
325
+
326
+
327
+ class PackedFp8ExpertBank(nn.Module):
328
+ """One layer's 16 experts packed into two grouped FP8 projections."""
329
+
330
+ def __init__(
331
+ self,
332
+ gate_up_qdata: torch.Tensor,
333
+ gate_up_scales: torch.Tensor,
334
+ down_qdata: torch.Tensor,
335
+ down_scales: torch.Tensor,
336
+ *,
337
+ gate_clamp_max: float,
338
+ up_clamp_min: float,
339
+ up_clamp_max: float,
340
+ ) -> None:
341
+ super().__init__()
342
+ self.register_buffer("gate_up_qdata", gate_up_qdata, persistent=False)
343
+ self.register_buffer("gate_up_scales", gate_up_scales, persistent=False)
344
+ self.register_buffer("down_qdata", down_qdata, persistent=False)
345
+ self.register_buffer("down_scales", down_scales, persistent=False)
346
+ self.num_experts = int(gate_up_qdata.shape[0])
347
+ self.intermediate_size = int(gate_up_qdata.shape[1] // 2)
348
+ self.hidden_size = int(gate_up_qdata.shape[2])
349
+ self.gate_clamp_max = float(gate_clamp_max)
350
+ self.up_clamp_min = float(up_clamp_min)
351
+ self.up_clamp_max = float(up_clamp_max)
352
+
353
+ @classmethod
354
+ @torch.no_grad()
355
+ def from_experts(
356
+ cls,
357
+ experts: nn.ModuleList,
358
+ *,
359
+ gate_clamp_max: float,
360
+ up_clamp_min: float,
361
+ up_clamp_max: float,
362
+ ) -> "PackedFp8ExpertBank":
363
+ if not experts:
364
+ raise ValueError("cannot pack an empty expert list")
365
+ first_gate, _ = _float8_parts(experts[0].gate_proj)
366
+ first_down, _ = _float8_parts(experts[0].down_proj)
367
+ num_experts = len(experts)
368
+ intermediate_size, hidden_size = first_gate.shape
369
+ if tuple(first_down.shape) != (hidden_size, intermediate_size):
370
+ raise ValueError("unexpected down projection shape")
371
+ device = first_gate.device
372
+ gate_up_qdata = torch.empty(
373
+ (num_experts, 2 * intermediate_size, hidden_size),
374
+ device=device,
375
+ dtype=torch.float8_e4m3fn,
376
+ )
377
+ gate_up_scales = torch.empty(
378
+ (num_experts, 2 * intermediate_size), device=device, dtype=torch.float32
379
+ )
380
+ down_qdata = torch.empty(
381
+ (num_experts, hidden_size, intermediate_size),
382
+ device=device,
383
+ dtype=torch.float8_e4m3fn,
384
+ )
385
+ down_scales = torch.empty((num_experts, hidden_size), device=device, dtype=torch.float32)
386
+
387
+ for index, expert in enumerate(experts):
388
+ gate_qdata, gate_scale = _float8_parts(expert.gate_proj)
389
+ up_qdata, up_scale = _float8_parts(expert.up_proj)
390
+ down_expert_qdata, down_scale = _float8_parts(expert.down_proj)
391
+ if tuple(gate_qdata.shape) != (intermediate_size, hidden_size):
392
+ raise ValueError("expert gate projection shapes are inconsistent")
393
+ if tuple(up_qdata.shape) != (intermediate_size, hidden_size):
394
+ raise ValueError("expert up projection shapes are inconsistent")
395
+ if tuple(down_expert_qdata.shape) != (hidden_size, intermediate_size):
396
+ raise ValueError("expert down projection shapes are inconsistent")
397
+ gate_up_qdata[index, :intermediate_size].copy_(gate_qdata)
398
+ gate_up_qdata[index, intermediate_size:].copy_(up_qdata)
399
+ gate_up_scales[index, :intermediate_size].copy_(gate_scale.expand(intermediate_size))
400
+ gate_up_scales[index, intermediate_size:].copy_(up_scale.expand(intermediate_size))
401
+ down_qdata[index].copy_(down_expert_qdata)
402
+ down_scales[index].copy_(down_scale.expand(hidden_size))
403
+
404
+ return cls(
405
+ gate_up_qdata,
406
+ gate_up_scales,
407
+ down_qdata,
408
+ down_scales,
409
+ gate_clamp_max=gate_clamp_max,
410
+ up_clamp_min=up_clamp_min,
411
+ up_clamp_max=up_clamp_max,
412
+ )
413
+
414
+ def forward(
415
+ self,
416
+ hidden_states: torch.Tensor,
417
+ selected_indices: torch.Tensor,
418
+ selected_weights: torch.Tensor,
419
+ ) -> torch.Tensor:
420
+ token_count = hidden_states.shape[0]
421
+ route_experts = selected_indices.reshape(-1)
422
+ route_tokens = torch.arange(token_count, device=hidden_states.device).repeat_interleave(
423
+ selected_indices.shape[-1]
424
+ )
425
+ order = torch.argsort(route_experts, stable=True)
426
+ sorted_experts = route_experts.index_select(0, order)
427
+ sorted_tokens = route_tokens.index_select(0, order)
428
+ sorted_hidden = hidden_states.index_select(0, sorted_tokens).contiguous()
429
+ sorted_route_weights = selected_weights.reshape(-1).index_select(0, order)
430
+ tokens_per_expert = torch.bincount(
431
+ sorted_experts, minlength=self.num_experts
432
+ ).to(dtype=torch.int32)
433
+ offsets = torch.cumsum(tokens_per_expert, dim=0, dtype=torch.int32)
434
+
435
+ gate_up = grouped_fp8_linear(
436
+ sorted_hidden,
437
+ self.gate_up_qdata,
438
+ self.gate_up_scales,
439
+ offsets,
440
+ tokens_per_expert,
441
+ )
442
+ gate, up = gate_up.split(self.intermediate_size, dim=-1)
443
+ intermediate = F.silu(gate.clamp(max=self.gate_clamp_max)) * up.clamp(
444
+ min=self.up_clamp_min,
445
+ max=self.up_clamp_max,
446
+ )
447
+ routed = grouped_fp8_linear(
448
+ intermediate.contiguous(),
449
+ self.down_qdata,
450
+ self.down_scales,
451
+ offsets,
452
+ tokens_per_expert,
453
+ )
454
+ weighted = routed * sorted_route_weights.to(routed.dtype).unsqueeze(-1)
455
+ # ``order`` is a permutation, so restoring route order and reducing a
456
+ # contiguous [token, top_k, hidden] view avoids duplicate-index atomics.
457
+ # This makes decode deterministic and is friendlier to CUDA graphs.
458
+ inverse_order = torch.argsort(order)
459
+ route_outputs = weighted.index_select(0, inverse_order).reshape(
460
+ token_count, selected_indices.shape[-1], self.hidden_size
461
+ )
462
+ # The reference dispatcher visits experts in ascending expert-index
463
+ # order. Preserve that BF16 accumulation order to minimize long-stack
464
+ # drift across 30 residual layers.
465
+ expert_order = torch.argsort(selected_indices, dim=-1, stable=True)
466
+ route_outputs = route_outputs.gather(
467
+ 1,
468
+ expert_order.unsqueeze(-1).expand(-1, -1, self.hidden_size),
469
+ )
470
+ output = torch.zeros_like(hidden_states)
471
+ for route_slot in range(selected_indices.shape[-1]):
472
+ output = output + route_outputs[:, route_slot]
473
+ return output
474
+
475
+
476
+ @torch.no_grad()
477
+ def install_fast_fp8_runtime(model: nn.Module) -> dict[str, Any]:
478
+ """Pack every expert layer and enable the inference-only fast path.
479
+
480
+ The transformation releases the original per-expert modules to avoid
481
+ duplicating their FP8 storage. It is intentionally one-way for the current
482
+ process; reload the checkpoint to recover trainable/module-list form.
483
+ """
484
+
485
+ if model.training:
486
+ raise RuntimeError("call model.eval() before enabling fast FP8 serving")
487
+ if not torch.cuda.is_available():
488
+ raise RuntimeError("fast FP8 serving requires CUDA")
489
+ ensure_windows_msvc_environment()
490
+
491
+ wrappers = tuple(model.fusion_layers())
492
+ packed_layers = 0
493
+ released_experts = 0
494
+ packed_bytes = 0
495
+ for wrapper in wrappers:
496
+ if getattr(wrapper, "fast_expert_bank", None) is not None:
497
+ continue
498
+ bank = PackedFp8ExpertBank.from_experts(
499
+ wrapper.experts,
500
+ gate_clamp_max=wrapper.experts[0].gate_clamp_max,
501
+ up_clamp_min=wrapper.experts[0].up_clamp_min,
502
+ up_clamp_max=wrapper.experts[0].up_clamp_max,
503
+ )
504
+ released_experts += len(wrapper.experts)
505
+ packed_bytes += sum(buffer.numel() * buffer.element_size() for buffer in bank.buffers())
506
+ wrapper.fast_expert_bank = bank
507
+ wrapper.experts = nn.ModuleList()
508
+ wrapper.serving_mode = True
509
+ wrapper.last_router_state = None
510
+ wrapper.last_router_diagnostics = None
511
+ packed_layers += 1
512
+ # TorchAO tensor subclasses can participate in reference cycles. Explicit
513
+ # collection is necessary before the allocator can release the unpacked
514
+ # per-expert qdata that the packed banks replaced.
515
+ gc.collect()
516
+ torch.cuda.empty_cache()
517
+
518
+ return {
519
+ "backend": "triton-grouped-fp8",
520
+ "packed_layers": packed_layers,
521
+ "released_experts": released_experts,
522
+ "packed_bytes": packed_bytes,
523
+ "cuda_allocated_bytes": torch.cuda.memory_allocated(),
524
+ "compiler": os.environ.get("CC"),
525
+ }
licenses/HF-FINEGRAINED-FP8-APACHE-2.0.txt ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2018- The Hugging Face team. All rights reserved.
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright [yyyy] [name of copyright owner]
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
modeling_fuse_glm.py CHANGED
@@ -214,6 +214,20 @@ class TopKFoldedExpertRouter(nn.Module):
214
  auxiliary_loss = self.num_experts * torch.sum(probability_fraction * token_fraction)
215
  return logits, selected_indices, selected_weights, auxiliary_loss
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
  class FuseGlmFeedForward(nn.Module):
219
  """Preserve the native LFM FFN and add a sparse expert sidecar in parallel.
@@ -279,6 +293,8 @@ class FuseGlmFeedForward(nn.Module):
279
  self.raw_residual_scale = nn.Parameter(torch.zeros(()))
280
  self.last_router_state: RouterState | None = None
281
  self.last_router_diagnostics: RouterDiagnostics | None = None
 
 
282
 
283
  @property
284
  def residual_scale(self) -> torch.Tensor:
@@ -309,6 +325,8 @@ class FuseGlmFeedForward(nn.Module):
309
  selected_indices: torch.Tensor,
310
  selected_weights: torch.Tensor,
311
  ) -> torch.Tensor:
 
 
312
  output = torch.zeros_like(hidden_states)
313
  for expert_index, expert in enumerate(self.experts):
314
  token_positions, route_slots = torch.where(selected_indices == expert_index)
@@ -320,6 +338,27 @@ class FuseGlmFeedForward(nn.Module):
320
  output = output.index_add(0, token_positions, expert_output * route_weight)
321
  return output
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  def _make_diagnostics(
324
  self,
325
  state: RouterState,
@@ -361,6 +400,8 @@ class FuseGlmFeedForward(nn.Module):
361
  self.last_router_state = None
362
  self.last_router_diagnostics = self._disabled_diagnostics(hidden_states)
363
  return base_output
 
 
364
 
365
  original_shape = hidden_states.shape
366
  flat_hidden = hidden_states.reshape(-1, original_shape[-1])
@@ -551,6 +592,14 @@ class FuseGlmForCausalLM(Lfm2ForCausalLM):
551
  wrapper.coding_enabled = bool(enabled)
552
  return self
553
 
 
 
 
 
 
 
 
 
554
  @property
555
  def coding_enabled(self) -> bool:
556
  layers = self.fusion_layers()
 
214
  auxiliary_loss = self.num_experts * torch.sum(probability_fraction * token_fraction)
215
  return logits, selected_indices, selected_weights, auxiliary_loss
216
 
217
+ def forward_for_serving(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
218
+ """Route without training-only softmax, one-hot, or auxiliary loss work."""
219
+
220
+ logits = F.linear(hidden_states.float(), self.proj.weight.float())
221
+ scores = torch.sigmoid(logits)
222
+ selected_indices = torch.topk(
223
+ scores + self.e_score_correction_bias.float(), self.top_k, dim=-1
224
+ ).indices
225
+ selected_scores = scores.gather(-1, selected_indices)
226
+ selected_weights = selected_scores / selected_scores.sum(
227
+ dim=-1, keepdim=True
228
+ ).clamp_min(1e-12)
229
+ return selected_indices, selected_weights
230
+
231
 
232
  class FuseGlmFeedForward(nn.Module):
233
  """Preserve the native LFM FFN and add a sparse expert sidecar in parallel.
 
293
  self.raw_residual_scale = nn.Parameter(torch.zeros(()))
294
  self.last_router_state: RouterState | None = None
295
  self.last_router_diagnostics: RouterDiagnostics | None = None
296
+ self.fast_expert_bank: nn.Module | None = None
297
+ self.serving_mode = False
298
 
299
  @property
300
  def residual_scale(self) -> torch.Tensor:
 
325
  selected_indices: torch.Tensor,
326
  selected_weights: torch.Tensor,
327
  ) -> torch.Tensor:
328
+ if self.fast_expert_bank is not None:
329
+ return self.fast_expert_bank(hidden_states, selected_indices, selected_weights)
330
  output = torch.zeros_like(hidden_states)
331
  for expert_index, expert in enumerate(self.experts):
332
  token_positions, route_slots = torch.where(selected_indices == expert_index)
 
338
  output = output.index_add(0, token_positions, expert_output * route_weight)
339
  return output
340
 
341
+ def _forward_for_serving(
342
+ self, hidden_states: torch.Tensor, base_output: torch.Tensor
343
+ ) -> torch.Tensor:
344
+ """Inference fast path without router state or diagnostic construction."""
345
+
346
+ original_shape = hidden_states.shape
347
+ flat_hidden = hidden_states.reshape(-1, original_shape[-1])
348
+ token_gate = torch.sigmoid(
349
+ F.linear(
350
+ flat_hidden.float(),
351
+ self.token_gate.weight.float(),
352
+ self.token_gate.bias.float(),
353
+ )
354
+ ).to(hidden_states.dtype)
355
+ selected_indices, selected_weights = self.router.forward_for_serving(flat_hidden)
356
+ expert_delta = self._dispatch(flat_hidden, selected_indices, selected_weights)
357
+ expert_delta = expert_delta * token_gate
358
+ return base_output + self.residual_scale.to(expert_delta.dtype) * expert_delta.reshape(
359
+ original_shape
360
+ )
361
+
362
  def _make_diagnostics(
363
  self,
364
  state: RouterState,
 
400
  self.last_router_state = None
401
  self.last_router_diagnostics = self._disabled_diagnostics(hidden_states)
402
  return base_output
403
+ if self.serving_mode and not self.training:
404
+ return self._forward_for_serving(hidden_states, base_output)
405
 
406
  original_shape = hidden_states.shape
407
  flat_hidden = hidden_states.reshape(-1, original_shape[-1])
 
592
  wrapper.coding_enabled = bool(enabled)
593
  return self
594
 
595
+ def enable_fast_fp8_serving(self) -> dict[str, Any]:
596
+ """Pack TorchAO experts into the Triton grouped-FP8 serving runtime."""
597
+
598
+ from .fast_fp8_runtime import install_fast_fp8_runtime
599
+
600
+ self.eval()
601
+ return install_fast_fp8_runtime(self)
602
+
603
  @property
604
  def coding_enabled(self) -> bool:
605
  layers = self.fusion_layers()
provenance/checksums.sha256 CHANGED
@@ -3,9 +3,11 @@
3
  f3e412383b66fc807c3d0fbe9e6f8a4a58b2638df55d8380e9cab64af30e8675 chat_template.jinja
4
  1b441cb88813e8c01f8a9ae98452adfe655ba680e1964c787583634ecb8cb473 config.json
5
  922dfb636b84d09d2f92ba0c5bc226d00d7b41999c7e9aef1939ccb022192427 configuration_fuse_glm.py
 
6
  e34e51ccf5a169f0d9cecf8119e9f719132fa0cf5fae7f8b650893821348e62a generation_config.json
7
  4d28ca14dedc0b3d0fcc2b3339f0e79931faa33874f3d24f522183a8fc70068c LICENSE
8
  30b85b6b9659f2e78aa259f8faf5d920a68dee7c9ced3fa6dba1f19f2bc4fca1 licenses/GLM-MIT.txt
 
9
  4d28ca14dedc0b3d0fcc2b3339f0e79931faa33874f3d24f522183a8fc70068c licenses/LFM-OPEN-LICENSE-v1.0.txt
10
  7a3c863c63fb2c52b7f881b483ffc7d1f4b352b2de932b718f62b2b2134de1e8 model-00001-of-00005.safetensors
11
  e1668117e8a2ad1c7d4ff9a12c1687c9e907a39738dcafd1a48647d38a6efed3 model-00002-of-00005.safetensors
@@ -13,23 +15,26 @@ e1668117e8a2ad1c7d4ff9a12c1687c9e907a39738dcafd1a48647d38a6efed3 model-00002-of
13
  ebb55bc7f9cd7139d3e21c0677f2b2b7f99ec25925837729b0c07a6efa2b0074 model-00004-of-00005.safetensors
14
  5aec98e4957c1263db1e79f2014f468620ec1b512831256329d8091f762fc61a model-00005-of-00005.safetensors
15
  95b79151fed3d2716210fb3046282a47bf73e2016c0ae59f413b59e221e50fc5 model.safetensors.index.json
16
- 2fe578e98265fa3704aa6971d1234784c4e1ef6ebf9a6df6661c831e340d3f33 modeling_fuse_glm.py
17
- a1829a85639489b9b4cef7dbb520e34c914bdc4bc1a726f68de5714083fe59b0 NOTICE.md
18
  b7d698b9414c814a1d78dce6a414c3340f3cd0cd949fc2c6b192bd2e35688710 provenance/assembly.json
19
  4dbb9025d648edd432e0fcbd6cc1f6dafa96e4aa0e0222171923feb17965aa25 provenance/base-bf16-checksums.sha256
20
  92eda133f175fa573b88b92fca5995518965117e534f3b732c63e64862488ff0 provenance/bridge.json
21
  69ae72f0e5add90269c5e949366c10e87d1de64fc815d14d0f330010f0fd1400 provenance/expert-selection.json
 
22
  036c4343e5d62e72613b3bb0ad1238d9f8000088d1118439798ac7ade13d7a2f provenance/folding.json
23
  2ba3ae47063c1b4a957b8c5983c48d08c0dacb01c5624f856b8414ac7ccfc2c8 provenance/fusion-plan.json
24
  a0ab71b19384546ea62e09d0c55eee623f59b92fff5140f1baa883d2386ff12f provenance/pre-repack-index.json
25
- 15d69ac2fd857a1749578273f84f1385b004cd1e3fb9204e5d7684cc6c534e8c provenance/quantization.json
26
  f6c13f0c8a28f1029132af53138b91352db35791b5da4eb0748cde4a42c59e51 provenance/router-training.json
27
  b8bf61e3cd2abfe2ad4b9b59e71ee48c7b680485963f3bc4e3b19df7849a3016 provenance/selected-expert-tensors.json
28
  ac437e75d524dd58e1c6934ad0838c62e11c9d0954d24873cb17862f958f64a5 provenance/source-models.json
29
  88fd032686145809f3dee3e8082a7a3b1cffe2874661289b59472a56ed5fcc1c provenance/training-data-manifest.json
30
  f426ea62b798bb2340b8d9cfdea2f53f309044dbe9caa061c208e358ddc39ea9 provenance/training-summary.json
31
  19f75f7117ca582e393d9f79452af231f93339c55dfcc6bebd2b953f87ed3152 provenance/validation-metrics.json
32
- 3cc74de6c69b37d403f553e376348e1c1addcbae099f90054a4f95640c6a93e3 README.md
 
33
  ce31a666514d30d302472da9387348b4958872f7e307a7ee443ae09f8d1b19b5 requirements.txt
 
34
  14c60d6814b7f64c69711d5c5d5561d3de9cc3896feebf9613ae8a8523a3497d tokenizer_config.json
35
  695be7802a0e4b8a81048f0ff5ebb7fc811a0ba5a6be63dbb24deb5a81096f41 tokenizer.json
 
3
  f3e412383b66fc807c3d0fbe9e6f8a4a58b2638df55d8380e9cab64af30e8675 chat_template.jinja
4
  1b441cb88813e8c01f8a9ae98452adfe655ba680e1964c787583634ecb8cb473 config.json
5
  922dfb636b84d09d2f92ba0c5bc226d00d7b41999c7e9aef1939ccb022192427 configuration_fuse_glm.py
6
+ 8ced7754937def623087e269d2a0111125ee21046a47b479dd03c8043eb63a78 fast_fp8_runtime.py
7
  e34e51ccf5a169f0d9cecf8119e9f719132fa0cf5fae7f8b650893821348e62a generation_config.json
8
  4d28ca14dedc0b3d0fcc2b3339f0e79931faa33874f3d24f522183a8fc70068c LICENSE
9
  30b85b6b9659f2e78aa259f8faf5d920a68dee7c9ced3fa6dba1f19f2bc4fca1 licenses/GLM-MIT.txt
10
+ 77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049 licenses/HF-FINEGRAINED-FP8-APACHE-2.0.txt
11
  4d28ca14dedc0b3d0fcc2b3339f0e79931faa33874f3d24f522183a8fc70068c licenses/LFM-OPEN-LICENSE-v1.0.txt
12
  7a3c863c63fb2c52b7f881b483ffc7d1f4b352b2de932b718f62b2b2134de1e8 model-00001-of-00005.safetensors
13
  e1668117e8a2ad1c7d4ff9a12c1687c9e907a39738dcafd1a48647d38a6efed3 model-00002-of-00005.safetensors
 
15
  ebb55bc7f9cd7139d3e21c0677f2b2b7f99ec25925837729b0c07a6efa2b0074 model-00004-of-00005.safetensors
16
  5aec98e4957c1263db1e79f2014f468620ec1b512831256329d8091f762fc61a model-00005-of-00005.safetensors
17
  95b79151fed3d2716210fb3046282a47bf73e2016c0ae59f413b59e221e50fc5 model.safetensors.index.json
18
+ 200287c00eff2afaa68c9e8355fa8c2c73bcab4c2c79c86fb69c7a12ef885d25 modeling_fuse_glm.py
19
+ e70d7940c5bc65ec7dda47699654521ade102a99163903ef0d67646ffc151af2 NOTICE.md
20
  b7d698b9414c814a1d78dce6a414c3340f3cd0cd949fc2c6b192bd2e35688710 provenance/assembly.json
21
  4dbb9025d648edd432e0fcbd6cc1f6dafa96e4aa0e0222171923feb17965aa25 provenance/base-bf16-checksums.sha256
22
  92eda133f175fa573b88b92fca5995518965117e534f3b732c63e64862488ff0 provenance/bridge.json
23
  69ae72f0e5add90269c5e949366c10e87d1de64fc815d14d0f330010f0fd1400 provenance/expert-selection.json
24
+ cbcbfc68c1e543df155dbee63fe7eac265ee8b0f0ac4e5f19592da211a3bcae8 provenance/fast-serving.json
25
  036c4343e5d62e72613b3bb0ad1238d9f8000088d1118439798ac7ade13d7a2f provenance/folding.json
26
  2ba3ae47063c1b4a957b8c5983c48d08c0dacb01c5624f856b8414ac7ccfc2c8 provenance/fusion-plan.json
27
  a0ab71b19384546ea62e09d0c55eee623f59b92fff5140f1baa883d2386ff12f provenance/pre-repack-index.json
28
+ dfed0c2864aa6c8b0e187b956d20d03a36f155c4bc1bed5901f5d190b75cb2f7 provenance/quantization.json
29
  f6c13f0c8a28f1029132af53138b91352db35791b5da4eb0748cde4a42c59e51 provenance/router-training.json
30
  b8bf61e3cd2abfe2ad4b9b59e71ee48c7b680485963f3bc4e3b19df7849a3016 provenance/selected-expert-tensors.json
31
  ac437e75d524dd58e1c6934ad0838c62e11c9d0954d24873cb17862f958f64a5 provenance/source-models.json
32
  88fd032686145809f3dee3e8082a7a3b1cffe2874661289b59472a56ed5fcc1c provenance/training-data-manifest.json
33
  f426ea62b798bb2340b8d9cfdea2f53f309044dbe9caa061c208e358ddc39ea9 provenance/training-summary.json
34
  19f75f7117ca582e393d9f79452af231f93339c55dfcc6bebd2b953f87ed3152 provenance/validation-metrics.json
35
+ 8407f808d4182aefa1316828e855ea6ad8267a1822a939169d6cc94ec6a1f79a README.md
36
+ da44e453254bbea8d1e9169c37ebd65abcbad816a0889f0ab2d9037ace351e10 requirements-serve.txt
37
  ce31a666514d30d302472da9387348b4958872f7e307a7ee443ae09f8d1b19b5 requirements.txt
38
+ 46da82ff64a9bf9ffa1fcdfcd78a419e0e5497a8eb271233a282c3705db44811 serve.py
39
  14c60d6814b7f64c69711d5c5d5561d3de9cc3896feebf9613ae8a8523a3497d tokenizer_config.json
40
  695be7802a0e4b8a81048f0ff5ebb7fc811a0ba5a6be63dbb24deb5a81096f41 tokenizer.json
provenance/fast-serving.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema": "rivetcoder-fast-fp8-serving",
3
+ "schema_version": 1,
4
+ "created_at": "2026-08-27T23:51:49+09:00",
5
+ "hardware": {
6
+ "gpu": "NVIDIA GeForce RTX 5070 Ti",
7
+ "compute_capability": [12, 0],
8
+ "vram_mib": 16303
9
+ },
10
+ "software": {
11
+ "platform": "Windows 11",
12
+ "python": "3.12.10",
13
+ "torch": "2.12.0+cu130",
14
+ "torchao": "0.15.0",
15
+ "transformers": "5.16.1",
16
+ "triton": "3.5.1",
17
+ "compiler": "MSVC 19.44.35222"
18
+ },
19
+ "implementation": {
20
+ "backend": "Triton grouped FP8",
21
+ "packed_layers": 30,
22
+ "packed_experts": 480,
23
+ "top_k": 4,
24
+ "expert_gemms_per_layer": 2,
25
+ "gate_up_shape": [16, 4096, 2048],
26
+ "down_shape": [16, 2048, 2048],
27
+ "activation_scaling": "dynamic per routed expert",
28
+ "weight_scaling": "checkpoint-preserving per projection",
29
+ "kernel_reference": "kernels-community/finegrained-fp8",
30
+ "kernel_license": "Apache-2.0"
31
+ },
32
+ "full_model_parity": {
33
+ "baseline_seconds": 5.100,
34
+ "fast_seconds": 1.079,
35
+ "speedup": 4.73,
36
+ "mean_absolute_logit_error": 0.0,
37
+ "maximum_absolute_logit_error": 0.0,
38
+ "top1_equal": true,
39
+ "repeat_maximum_absolute_error": 0.0,
40
+ "resident_vram_gib": 8.428,
41
+ "packing_peak_vram_gib": 14.053
42
+ },
43
+ "microbatch_forward": [
44
+ {"batch": 1, "seconds": 1.0800, "sequences_per_second": 0.9259},
45
+ {"batch": 4, "seconds": 1.0868, "sequences_per_second": 3.6806},
46
+ {"batch": 8, "seconds": 1.0863, "sequences_per_second": 7.3645},
47
+ {"batch": 16, "seconds": 1.1169, "sequences_per_second": 14.3259}
48
+ ],
49
+ "server": {
50
+ "protocol": "OpenAI-compatible chat completions",
51
+ "batching": "queue-to-completion fixed microbatch",
52
+ "default_max_batch_size": 16,
53
+ "default_batch_wait_ms": 3.0,
54
+ "streaming": false
55
+ }
56
+ }
provenance/quantization.json CHANGED
@@ -42,6 +42,8 @@
42
  "clean_reload_seconds": 338.0,
43
  "clean_reload_fp8_parameters": 1636,
44
  "clean_reload_non_cuda_parameters": 0,
 
 
45
  "resident_cuda_allocated_bytes": 9039455232,
46
  "forward_finite": true,
47
  "forward_logits_shape": [1, 6, 128000]
 
42
  "clean_reload_seconds": 338.0,
43
  "clean_reload_fp8_parameters": 1636,
44
  "clean_reload_non_cuda_parameters": 0,
45
+ "fast_serving_validated": true,
46
+ "fast_serving_logits_bit_exact": true,
47
  "resident_cuda_allocated_bytes": 9039455232,
48
  "forward_finite": true,
49
  "forward_logits_shape": [1, 6, 128000]
requirements-serve.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ -r requirements.txt
2
+ fastapi>=0.115
3
+ uvicorn>=0.34
serve.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """OpenAI-compatible, microbatched RivetCoder FP8 serving entry point."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import asyncio
8
+ import concurrent.futures
9
+ import json
10
+ import queue
11
+ import threading
12
+ import time
13
+ import uuid
14
+ from collections import deque
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import torch
20
+ from transformers import AutoModelForCausalLM, AutoTokenizer
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class PendingCompletion:
25
+ messages: list[dict[str, Any]]
26
+ max_tokens: int
27
+ temperature: float
28
+ top_p: float
29
+ future: concurrent.futures.Future[str]
30
+
31
+ @property
32
+ def batch_key(self) -> tuple[int, float, float]:
33
+ return self.max_tokens, self.temperature, self.top_p
34
+
35
+
36
+ class MicrobatchEngine:
37
+ """Collect compatible requests briefly, then generate them as one batch."""
38
+
39
+ def __init__(
40
+ self,
41
+ model: Any,
42
+ tokenizer: Any,
43
+ *,
44
+ max_batch_size: int,
45
+ batch_wait_ms: float,
46
+ ) -> None:
47
+ self.model = model
48
+ self.tokenizer = tokenizer
49
+ self.max_batch_size = int(max_batch_size)
50
+ self.batch_wait_seconds = float(batch_wait_ms) / 1000.0
51
+ self.incoming: queue.Queue[PendingCompletion | None] = queue.Queue()
52
+ self.deferred: deque[PendingCompletion] = deque()
53
+ self.thread = threading.Thread(target=self._worker, name="rivetcoder-gpu", daemon=True)
54
+ self.thread.start()
55
+
56
+ def submit(
57
+ self,
58
+ messages: list[dict[str, Any]],
59
+ *,
60
+ max_tokens: int,
61
+ temperature: float,
62
+ top_p: float,
63
+ ) -> concurrent.futures.Future[str]:
64
+ future: concurrent.futures.Future[str] = concurrent.futures.Future()
65
+ self.incoming.put(
66
+ PendingCompletion(
67
+ messages=messages,
68
+ max_tokens=max_tokens,
69
+ temperature=temperature,
70
+ top_p=top_p,
71
+ future=future,
72
+ )
73
+ )
74
+ return future
75
+
76
+ def close(self) -> None:
77
+ self.incoming.put(None)
78
+
79
+ def _next_request(self) -> PendingCompletion | None:
80
+ if self.deferred:
81
+ return self.deferred.popleft()
82
+ return self.incoming.get()
83
+
84
+ def _collect_batch(self, first: PendingCompletion) -> list[PendingCompletion]:
85
+ batch = [first]
86
+ key = first.batch_key
87
+ deadline = time.perf_counter() + self.batch_wait_seconds
88
+ while len(batch) < self.max_batch_size:
89
+ remaining = deadline - time.perf_counter()
90
+ if remaining <= 0:
91
+ break
92
+ try:
93
+ item = self.incoming.get(timeout=remaining)
94
+ except queue.Empty:
95
+ break
96
+ if item is None:
97
+ self.incoming.put(None)
98
+ break
99
+ if item.batch_key == key:
100
+ batch.append(item)
101
+ else:
102
+ self.deferred.append(item)
103
+ return batch
104
+
105
+ def _worker(self) -> None:
106
+ while True:
107
+ first = self._next_request()
108
+ if first is None:
109
+ return
110
+ batch = self._collect_batch(first)
111
+ try:
112
+ results = self._generate(batch)
113
+ except BaseException as error:
114
+ for item in batch:
115
+ item.future.set_exception(error)
116
+ continue
117
+ for item, text in zip(batch, results, strict=True):
118
+ item.future.set_result(text)
119
+
120
+ def _generate(self, batch: list[PendingCompletion]) -> list[str]:
121
+ rendered = [
122
+ self.tokenizer.apply_chat_template(
123
+ item.messages,
124
+ add_generation_prompt=True,
125
+ tokenize=False,
126
+ )
127
+ for item in batch
128
+ ]
129
+ encoded = self.tokenizer(
130
+ rendered,
131
+ add_special_tokens=False,
132
+ padding=True,
133
+ return_tensors="pt",
134
+ ).to("cuda")
135
+ prompt_width = encoded["input_ids"].shape[-1]
136
+ temperature = batch[0].temperature
137
+ with torch.no_grad():
138
+ generated = self.model.generate(
139
+ **encoded,
140
+ max_new_tokens=batch[0].max_tokens,
141
+ do_sample=temperature > 0,
142
+ temperature=max(temperature, 1.0e-5),
143
+ top_p=batch[0].top_p,
144
+ use_cache=True,
145
+ logits_to_keep=1,
146
+ pad_token_id=self.tokenizer.pad_token_id,
147
+ eos_token_id=self.tokenizer.eos_token_id,
148
+ )
149
+ return self.tokenizer.batch_decode(
150
+ generated[:, prompt_width:],
151
+ skip_special_tokens=True,
152
+ )
153
+
154
+
155
+ def load_runtime(args: argparse.Namespace) -> tuple[Any, Any, dict[str, Any]]:
156
+ tokenizer = AutoTokenizer.from_pretrained(
157
+ args.model,
158
+ trust_remote_code=True,
159
+ local_files_only=args.local_files_only,
160
+ )
161
+ tokenizer.padding_side = "left"
162
+ model = AutoModelForCausalLM.from_pretrained(
163
+ args.model,
164
+ trust_remote_code=True,
165
+ local_files_only=args.local_files_only,
166
+ dtype=torch.bfloat16,
167
+ device_map=0,
168
+ attn_implementation="sdpa",
169
+ ).eval()
170
+ model.config.use_cache = True
171
+ model.set_coding_enabled(True)
172
+ if not hasattr(model, "enable_fast_fp8_serving"):
173
+ raise RuntimeError(
174
+ "The model package does not contain the grouped-FP8 runtime. "
175
+ "Use the updated RivetCoder FP8 package."
176
+ )
177
+ report = model.enable_fast_fp8_serving()
178
+ return tokenizer, model, report
179
+
180
+
181
+ def warmup(model: Any, tokenizer: Any, batch_sizes: list[int]) -> None:
182
+ text = tokenizer.apply_chat_template(
183
+ [{"role": "user", "content": "Return the integer 1."}],
184
+ add_generation_prompt=True,
185
+ tokenize=False,
186
+ )
187
+ for batch_size in batch_sizes:
188
+ encoded = tokenizer(
189
+ [text] * batch_size,
190
+ add_special_tokens=False,
191
+ padding=True,
192
+ return_tensors="pt",
193
+ ).to("cuda")
194
+ with torch.no_grad():
195
+ model.generate(
196
+ **encoded,
197
+ max_new_tokens=2,
198
+ do_sample=False,
199
+ use_cache=True,
200
+ logits_to_keep=1,
201
+ pad_token_id=tokenizer.pad_token_id,
202
+ eos_token_id=tokenizer.eos_token_id,
203
+ )
204
+
205
+
206
+ def build_app(engine: MicrobatchEngine, runtime_report: dict[str, Any], model_name: str) -> Any:
207
+ try:
208
+ from fastapi import FastAPI, HTTPException
209
+ except ImportError as error:
210
+ raise RuntimeError("Serving requires fastapi and uvicorn") from error
211
+
212
+ app = FastAPI(title="RivetCoder FP8 Server")
213
+ created = int(time.time())
214
+
215
+ @app.get("/health")
216
+ async def health() -> dict[str, Any]:
217
+ return {
218
+ "status": "ok",
219
+ "model": model_name,
220
+ "runtime": runtime_report,
221
+ "max_batch_size": engine.max_batch_size,
222
+ "batch_wait_ms": engine.batch_wait_seconds * 1000.0,
223
+ "cuda_allocated_gib": torch.cuda.memory_allocated() / 1024**3,
224
+ }
225
+
226
+ @app.get("/v1/models")
227
+ async def models() -> dict[str, Any]:
228
+ return {
229
+ "object": "list",
230
+ "data": [{"id": model_name, "object": "model", "created": created, "owned_by": "HCHs"}],
231
+ }
232
+
233
+ @app.post("/v1/chat/completions")
234
+ async def chat_completions(payload: dict[str, Any]) -> dict[str, Any]:
235
+ if payload.get("stream", False):
236
+ raise HTTPException(status_code=400, detail="Streaming is not implemented in the microbatch server")
237
+ messages = payload.get("messages")
238
+ if not isinstance(messages, list) or not messages:
239
+ raise HTTPException(status_code=400, detail="messages must be a non-empty list")
240
+ max_tokens = int(payload.get("max_tokens", 512))
241
+ if max_tokens < 1 or max_tokens > 4096:
242
+ raise HTTPException(status_code=400, detail="max_tokens must be between 1 and 4096")
243
+ temperature = float(payload.get("temperature", 0.2))
244
+ top_p = float(payload.get("top_p", 0.95))
245
+ future = engine.submit(
246
+ messages,
247
+ max_tokens=max_tokens,
248
+ temperature=temperature,
249
+ top_p=top_p,
250
+ )
251
+ try:
252
+ text = await asyncio.wrap_future(future)
253
+ except Exception as error:
254
+ raise HTTPException(status_code=500, detail=str(error)) from error
255
+ completion_id = f"chatcmpl-{uuid.uuid4().hex}"
256
+ return {
257
+ "id": completion_id,
258
+ "object": "chat.completion",
259
+ "created": int(time.time()),
260
+ "model": model_name,
261
+ "choices": [
262
+ {
263
+ "index": 0,
264
+ "message": {"role": "assistant", "content": text},
265
+ "finish_reason": "stop",
266
+ }
267
+ ],
268
+ }
269
+
270
+ return app
271
+
272
+
273
+ def parse_args() -> argparse.Namespace:
274
+ parser = argparse.ArgumentParser(description=__doc__)
275
+ parser.add_argument(
276
+ "--model",
277
+ default=str(Path("RivetCoder-9B-A4B-FP8")),
278
+ help="Local FP8 model directory or Hugging Face model id",
279
+ )
280
+ parser.add_argument("--host", default="127.0.0.1")
281
+ parser.add_argument("--port", type=int, default=8000)
282
+ parser.add_argument("--max-batch-size", type=int, default=16)
283
+ parser.add_argument("--batch-wait-ms", type=float, default=3.0)
284
+ parser.add_argument("--warmup-batches", default="1,8,16")
285
+ parser.add_argument("--local-files-only", action=argparse.BooleanOptionalAction, default=True)
286
+ return parser.parse_args()
287
+
288
+
289
+ def main() -> int:
290
+ args = parse_args()
291
+ if not torch.cuda.is_available():
292
+ raise SystemExit("CUDA is required")
293
+ torch.set_float32_matmul_precision("high")
294
+ tokenizer, model, runtime_report = load_runtime(args)
295
+ warmup_batches = [int(value) for value in args.warmup_batches.split(",") if value]
296
+ warmup(model, tokenizer, warmup_batches)
297
+ engine = MicrobatchEngine(
298
+ model,
299
+ tokenizer,
300
+ max_batch_size=args.max_batch_size,
301
+ batch_wait_ms=args.batch_wait_ms,
302
+ )
303
+ app = build_app(engine, runtime_report, str(args.model))
304
+ print(json.dumps({"runtime": runtime_report, "listen": f"http://{args.host}:{args.port}"}, indent=2))
305
+ import uvicorn
306
+
307
+ uvicorn.run(app, host=args.host, port=args.port, workers=1)
308
+ return 0
309
+
310
+
311
+ if __name__ == "__main__":
312
+ raise SystemExit(main())