Text Generation
Transformers
Safetensors
English
Korean
code
fuse_glm
custom_code
lfm2
glm
mixture-of-experts
routed-experts
coding
code-generation
fp8
torchao
top-k-routing
trust-remote-code
conversational
Instructions to use HCHs/RivetCoder-9B-A4B-FP8 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use HCHs/RivetCoder-9B-A4B-FP8 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="HCHs/RivetCoder-9B-A4B-FP8", trust_remote_code=True) messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("HCHs/RivetCoder-9B-A4B-FP8", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use HCHs/RivetCoder-9B-A4B-FP8 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "HCHs/RivetCoder-9B-A4B-FP8" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HCHs/RivetCoder-9B-A4B-FP8", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/HCHs/RivetCoder-9B-A4B-FP8
- SGLang
How to use HCHs/RivetCoder-9B-A4B-FP8 with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "HCHs/RivetCoder-9B-A4B-FP8" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HCHs/RivetCoder-9B-A4B-FP8", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "HCHs/RivetCoder-9B-A4B-FP8" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HCHs/RivetCoder-9B-A4B-FP8", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use HCHs/RivetCoder-9B-A4B-FP8 with Docker Model Runner:
docker model run hf.co/HCHs/RivetCoder-9B-A4B-FP8
Add Triton grouped-FP8 runtime and microbatch server
Browse files- NOTICE.md +7 -1
- README.md +65 -5
- fast_fp8_runtime.py +525 -0
- licenses/HF-FINEGRAINED-FP8-APACHE-2.0.txt +203 -0
- modeling_fuse_glm.py +49 -0
- provenance/checksums.sha256 +9 -4
- provenance/fast-serving.json +56 -0
- provenance/quantization.json +2 -0
- requirements-serve.txt +3 -0
- serve.py +312 -0
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 |
-
|
|
|
|
| 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`
|
| 143 |
-
|
| 144 |
-
|
|
|
|
| 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.
|
| 150 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 17 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 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())
|