Instructions to use Agnes-AI/Agnes-2.5-Flash-Base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Agnes-AI/Agnes-2.5-Flash-Base with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="Agnes-AI/Agnes-2.5-Flash-Base", trust_remote_code=True)# Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("Agnes-AI/Agnes-2.5-Flash-Base", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use Agnes-AI/Agnes-2.5-Flash-Base with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "Agnes-AI/Agnes-2.5-Flash-Base" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Agnes-AI/Agnes-2.5-Flash-Base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/Agnes-AI/Agnes-2.5-Flash-Base
- SGLang
How to use Agnes-AI/Agnes-2.5-Flash-Base 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 "Agnes-AI/Agnes-2.5-Flash-Base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Agnes-AI/Agnes-2.5-Flash-Base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'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 "Agnes-AI/Agnes-2.5-Flash-Base" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "Agnes-AI/Agnes-2.5-Flash-Base", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use Agnes-AI/Agnes-2.5-Flash-Base with Docker Model Runner:
docker model run hf.co/Agnes-AI/Agnes-2.5-Flash-Base
Upload folder using huggingface_hub
Browse files- model-00016-of-00035.safetensors +3 -0
- model-00017-of-00035.safetensors +3 -0
- model-00018-of-00035.safetensors +3 -0
- model-00019-of-00035.safetensors +3 -0
- model-00020-of-00035.safetensors +3 -0
- model-00021-of-00035.safetensors +3 -0
- model-00022-of-00035.safetensors +3 -0
- model-00023-of-00035.safetensors +3 -0
- model-00024-of-00035.safetensors +3 -0
- model-00025-of-00035.safetensors +3 -0
- model-00026-of-00035.safetensors +3 -0
- model-00027-of-00035.safetensors +3 -0
- model-00028-of-00035.safetensors +3 -0
- model-00029-of-00035.safetensors +3 -0
- model-00030-of-00035.safetensors +3 -0
- model-00031-of-00035.safetensors +3 -0
- model-00032-of-00035.safetensors +3 -0
- model-00033-of-00035.safetensors +3 -0
- model-00034-of-00035.safetensors +3 -0
- model-00035-of-00035.safetensors +3 -0
- model-00036-of-00036.safetensors +3 -0
- model-parallel-ffn.safetensors +3 -0
- model.safetensors.index.json +0 -0
- modeling_agnes.py +1784 -0
- serve.sh +21 -0
- sglang_patch/kernels/ops/layernorm/mhc_head.py +151 -0
- sglang_patch/srt/arg_groups/overrides.py +2230 -0
- sglang_patch/srt/arg_groups/speculative_hook.py +792 -0
- sglang_patch/srt/batch_overlap/operations_strategy.py +378 -0
- sglang_patch/srt/configs/agnes.py +110 -0
- sglang_patch/srt/configs/model_config.py +2055 -0
- sglang_patch/srt/entrypoints/openai/chat_encoding.py +97 -0
- sglang_patch/srt/entrypoints/openai/serving_chat.py +2264 -0
- sglang_patch/srt/function_call/function_call_parser.py +286 -0
- sglang_patch/srt/layers/attention/dsv4/compressor.py +480 -0
- sglang_patch/srt/layers/attention/dsv4/indexer.py +887 -0
- sglang_patch/srt/layers/quantization/modelslim/modelslim.py +485 -0
- sglang_patch/srt/mem_cache/kv_cache_configurator.py +1867 -0
- sglang_patch/srt/models/agnes.py +0 -0
- sglang_patch/srt/models/agnes_dspark.py +892 -0
- sglang_patch/srt/models/agnes_nextn.py +283 -0
- sglang_patch/srt/models/deepseek_v2.py +0 -0
- sglang_patch/srt/parser/reasoning_parser.py +1495 -0
- sglang_patch/srt/parser/template_detection.py +707 -0
- sglang_patch/srt/server_args.py +0 -0
- sglang_patch/srt/utils/hf_transformers/common.py +541 -0
- tokenizer.json +0 -0
- tokenizer_config.json +34 -0
model-00016-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1da9dfeda86c4d8bdbe0e0220347260d9a3096462757073d043df97909499b28
|
| 3 |
+
size 6449204804
|
model-00017-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:709daa789a7fe689885aac68e0318e5d9ff3c9ebe0d8caed11757b161d4de593
|
| 3 |
+
size 6458523600
|
model-00018-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9dc0cc3eaa551fcc0918ee0e779a9c16e767924a5ef8ee92d3f8b79908fc7b4d
|
| 3 |
+
size 6445125364
|
model-00019-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:21fbeb7933260748dd021c39d6c1806b4369337894ccc3253f3a7af131a1e53f
|
| 3 |
+
size 6458523472
|
model-00020-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4e4699b807ce99d6895ac617d497caa3faffe85128d890a2ce313a01966d8cc7
|
| 3 |
+
size 6449197348
|
model-00021-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c249772e3c3c657ef301c76ed0f4c0f628145df14f8b8d3e0940cf3a20d3a81e
|
| 3 |
+
size 6458523448
|
model-00022-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:26656dc243c9e21f36520317b5e6f205f23c73be389e967e8558b97f19b3b186
|
| 3 |
+
size 6445125396
|
model-00023-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:544219a4c26717ff7c48159d4ae784995dc722d3e0c3bf46eada5f07775c36f1
|
| 3 |
+
size 6458523536
|
model-00024-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9259a0f5c55aa2237c16167caa66db76f6eaecb37f84b56ebfc12d76c00fd043
|
| 3 |
+
size 6449197260
|
model-00025-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:54ea8d09de13508ebce7d9e609cf195ca0dcbf7ea8e16cd2a57752aa1f2194df
|
| 3 |
+
size 6458523608
|
model-00026-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:28d247fe3cbccdd48bb297107824090cb9cd331b286f0f201f5c3dc39783158c
|
| 3 |
+
size 6446436500
|
model-00027-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d7765703b16007cc52519066a3e37a291115e184c54de4a606d94ce4383e1e72
|
| 3 |
+
size 6457212480
|
model-00028-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:838ca1c68d974bbd6780bc20bb4873df55dc3898dd2d5e9862d1b8d5b63b3072
|
| 3 |
+
size 6116586562
|
model-00029-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d453ea5b1139459c277a2ea336ed7065ef67db3f6516b416d1bd0d192cd2aa17
|
| 3 |
+
size 8304
|
model-00030-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f73d6dcf572c96b1204ed32301104c6f6aaf67a2372cc77add9ed336dab3f966
|
| 3 |
+
size 11012456
|
model-00031-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:71997f843485ea05aaff30b11b84fb3f9e8d4062f1424ab42884c442b82f60d4
|
| 3 |
+
size 4171198532
|
model-00032-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba08839f9991c0ebd6f648d7df4dcf0c5e6bd9ec5be844a387473ae6ea91c396
|
| 3 |
+
size 4192576620
|
model-00033-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a50d44978124ecc8b1404e7ef180951690e94224b9414ebdedb76871b5d405a
|
| 3 |
+
size 4171198532
|
model-00034-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6e426cd19c2ac355de42119e306920f6165bab73a6f197c7bede9e0a99d6e63
|
| 3 |
+
size 4192576620
|
model-00035-of-00035.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0bfd040848b2f9d1d2100779e7d57a940bd5d3581ea403bb560622c6df619208
|
| 3 |
+
size 4171198532
|
model-00036-of-00036.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:04c1d3f8536009af1a55739f99146acea6ef08fa0d8bb6f155e156c46464ffed
|
| 3 |
+
size 2127629656
|
model-parallel-ffn.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:42c8e4de1726b28fedf86d059754093b083b6a2f4cc636fa2c873ba053695e66
|
| 3 |
+
size 1132767752
|
model.safetensors.index.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_agnes.py
ADDED
|
@@ -0,0 +1,1784 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 The Agnes Model Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# This modeling code is an independent implementation of the Agnes model
|
| 4 |
+
# that work's Apache-2.0 licensing and attribution accordingly.
|
| 5 |
+
#
|
| 6 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 7 |
+
# you may not use this file except in compliance with the License.
|
| 8 |
+
# You may obtain a copy of the License at
|
| 9 |
+
#
|
| 10 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 11 |
+
#
|
| 12 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 13 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 14 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 15 |
+
# See the License for the specific language governing permissions and
|
| 16 |
+
# limitations under the License.
|
| 17 |
+
from collections.abc import Callable
|
| 18 |
+
from typing import Optional
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
from torch import nn
|
| 23 |
+
|
| 24 |
+
from transformers import initialization as init
|
| 25 |
+
from transformers.activations import ACT2FN
|
| 26 |
+
from transformers.cache_utils import Cache, DynamicCache, DynamicSlidingWindowLayer
|
| 27 |
+
from transformers.generation import GenerationMixin
|
| 28 |
+
from transformers.integrations import use_experts_implementation
|
| 29 |
+
from transformers.masking_utils import create_sliding_window_causal_mask
|
| 30 |
+
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
| 31 |
+
from transformers.modeling_layers import GradientCheckpointingLayer
|
| 32 |
+
from transformers.modeling_outputs import MoeCausalLMOutputWithPast, MoeModelOutputWithPast
|
| 33 |
+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
|
| 34 |
+
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
|
| 35 |
+
from transformers.processing_utils import Unpack
|
| 36 |
+
from transformers.utils import TransformersKwargs, auto_docstring, can_return_tuple
|
| 37 |
+
from transformers.utils.generic import maybe_autocast, merge_with_config_defaults
|
| 38 |
+
from transformers.utils.output_capturing import OutputRecorder, capture_outputs
|
| 39 |
+
from transformers.conversion_mapping import register_checkpoint_conversion_mapping
|
| 40 |
+
from transformers.core_model_loading import (
|
| 41 |
+
Concatenate,
|
| 42 |
+
MergeModulelist,
|
| 43 |
+
WeightConverter,
|
| 44 |
+
WeightRenaming,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
from .configuration_agnes import AgnesConfig
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ==========================================================================
|
| 51 |
+
# Normalization primitives
|
| 52 |
+
# ==========================================================================
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# Root-mean-square normalisation.
|
| 56 |
+
#
|
| 57 |
+
# Agnes fuses the RMS statistic, the reciprocal-sqrt rescale and (optionally)
|
| 58 |
+
# the learned gain into a single explicit autograd node rather than letting the
|
| 59 |
+
# framework trace the gradient through the individual pointwise ops. The
|
| 60 |
+
# statistic is always accumulated in float32 for stability; the closed-form
|
| 61 |
+
# backward below is the analytic Jacobian of that statistic, so the saved
|
| 62 |
+
# tensors are just the fp32 input, the per-row reciprocal std and (when
|
| 63 |
+
# present) the gain — no intermediate activation graph is retained.
|
| 64 |
+
#
|
| 65 |
+
# For a row x (length n), with r = 1 / sqrt(mean(x^2) + eps) and x_hat = x * r:
|
| 66 |
+
# y = gain * x_hat
|
| 67 |
+
# dx = r * (g - x_hat * mean(g * x_hat)) where g = dy * gain
|
| 68 |
+
# d(gain) = sum_over_rows(dy * x_hat)
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class _FusedRMSNorm(torch.autograd.Function):
|
| 73 |
+
"""Weighted RMS norm with a hand-derived backward (see module comment)."""
|
| 74 |
+
|
| 75 |
+
@staticmethod
|
| 76 |
+
def forward(ctx, x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
|
| 77 |
+
out_dtype = x.dtype
|
| 78 |
+
xf = x.float()
|
| 79 |
+
rstd = torch.rsqrt(xf.square().mean(-1, keepdim=True) + eps)
|
| 80 |
+
x_hat = (xf * rstd).to(out_dtype)
|
| 81 |
+
ctx.save_for_backward(xf, rstd, weight)
|
| 82 |
+
ctx.out_dtype = out_dtype
|
| 83 |
+
return weight * x_hat
|
| 84 |
+
|
| 85 |
+
@staticmethod
|
| 86 |
+
def backward(ctx, grad_out: torch.Tensor):
|
| 87 |
+
xf, rstd, weight = ctx.saved_tensors
|
| 88 |
+
n = xf.shape[-1]
|
| 89 |
+
x_hat = xf * rstd
|
| 90 |
+
g = grad_out.float() * weight.float()
|
| 91 |
+
# projection of g onto the manifold orthogonal to x_hat, then rescale by r
|
| 92 |
+
row_corr = (g * x_hat).sum(-1, keepdim=True) / n
|
| 93 |
+
grad_x = (rstd * (g - x_hat * row_corr)).to(ctx.out_dtype)
|
| 94 |
+
reduce_axes = tuple(range(grad_out.dim() - 1))
|
| 95 |
+
grad_weight = (grad_out.float() * x_hat).sum(reduce_axes).to(weight.dtype)
|
| 96 |
+
return grad_x, grad_weight, None
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class _FusedUnweightedRMSNorm(torch.autograd.Function):
|
| 100 |
+
"""Gain-free RMS norm; backward is the weighted case with gain fixed to 1."""
|
| 101 |
+
|
| 102 |
+
@staticmethod
|
| 103 |
+
def forward(ctx, x: torch.Tensor, eps: float) -> torch.Tensor:
|
| 104 |
+
rstd = torch.rsqrt(x.float().square().mean(-1, keepdim=True) + eps).to(x.dtype)
|
| 105 |
+
ctx.save_for_backward(x, rstd)
|
| 106 |
+
return x * rstd
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
def backward(ctx, grad_out: torch.Tensor):
|
| 110 |
+
x, rstd = ctx.saved_tensors
|
| 111 |
+
rf = rstd.float()
|
| 112 |
+
x_hat = x.float() * rf
|
| 113 |
+
n = x.shape[-1]
|
| 114 |
+
g = grad_out.float()
|
| 115 |
+
row_corr = (g * x_hat).sum(-1, keepdim=True) / n
|
| 116 |
+
grad_x = (rf * (g - x_hat * row_corr)).to(grad_out.dtype)
|
| 117 |
+
return grad_x, None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class AgnesRMSNorm(nn.Module):
|
| 121 |
+
"""RMS norm with a learned per-channel gain."""
|
| 122 |
+
|
| 123 |
+
def __init__(self, hidden_size, eps: float = 1e-6) -> None:
|
| 124 |
+
super().__init__()
|
| 125 |
+
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 126 |
+
self.variance_epsilon = eps
|
| 127 |
+
|
| 128 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 129 |
+
return _FusedRMSNorm.apply(hidden_states, self.weight, self.variance_epsilon)
|
| 130 |
+
|
| 131 |
+
def extra_repr(self):
|
| 132 |
+
return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class AgnesUnweightedRMSNorm(nn.Module):
|
| 136 |
+
"""RMS norm without a learned gain (used inside the attention / mHC blocks)."""
|
| 137 |
+
|
| 138 |
+
def __init__(self, eps: float = 1.0e-6):
|
| 139 |
+
super().__init__()
|
| 140 |
+
self.eps = eps
|
| 141 |
+
|
| 142 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 143 |
+
return _FusedUnweightedRMSNorm.apply(x, self.eps)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# ==========================================================================
|
| 148 |
+
# Feed-forward network and mixture-of-experts routing
|
| 149 |
+
# ==========================================================================
|
| 150 |
+
|
| 151 |
+
class AgnesMLP(nn.Module):
|
| 152 |
+
def __init__(self, config: AgnesConfig, intermediate_size: int | None = None):
|
| 153 |
+
super().__init__()
|
| 154 |
+
self.config = config
|
| 155 |
+
self.hidden_size = config.hidden_size
|
| 156 |
+
self.intermediate_size = intermediate_size if intermediate_size is not None else config.intermediate_size
|
| 157 |
+
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
|
| 158 |
+
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
|
| 159 |
+
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias)
|
| 160 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 161 |
+
self.limit = config.swiglu_limit
|
| 162 |
+
|
| 163 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 164 |
+
# SwiGLU with a symmetric pre-activation clip: the gate branch is capped
|
| 165 |
+
# from above, the value branch on both sides, before they are multiplied
|
| 166 |
+
# and projected back down.
|
| 167 |
+
activated = self.act_fn(self.gate_proj(x).clamp(max=self.limit))
|
| 168 |
+
value = self.up_proj(x).clamp(min=-self.limit, max=self.limit)
|
| 169 |
+
return self.down_proj(activated * value)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@use_experts_implementation
|
| 173 |
+
class AgnesExperts(nn.Module):
|
| 174 |
+
"""Routed experts held as stacked 3-D weight tensors (one slab per expert)."""
|
| 175 |
+
|
| 176 |
+
def __init__(self, config: AgnesConfig):
|
| 177 |
+
super().__init__()
|
| 178 |
+
self.num_experts = config.num_local_experts
|
| 179 |
+
self.hidden_dim = config.hidden_size
|
| 180 |
+
self.intermediate_dim = config.intermediate_size
|
| 181 |
+
self.gate_up_proj = nn.Parameter(torch.empty(self.num_experts, 2 * self.intermediate_dim, self.hidden_dim))
|
| 182 |
+
self.down_proj = nn.Parameter(torch.empty(self.num_experts, self.hidden_dim, self.intermediate_dim))
|
| 183 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 184 |
+
self.limit = config.swiglu_limit
|
| 185 |
+
|
| 186 |
+
def forward(
|
| 187 |
+
self, hidden_states: torch.Tensor, top_k_index: torch.Tensor, top_k_weights: torch.Tensor
|
| 188 |
+
) -> torch.Tensor:
|
| 189 |
+
# Reference (unfused) dispatch: walk only the experts that at least one
|
| 190 |
+
# token selected, gather that expert's tokens, run its SwiGLU, and
|
| 191 |
+
# scatter-add the weighted result back. The heavy fused kernels are
|
| 192 |
+
# swapped in by `@use_experts_implementation`; this body is the fallback.
|
| 193 |
+
out = torch.zeros_like(hidden_states)
|
| 194 |
+
with torch.no_grad():
|
| 195 |
+
# [experts, slot, token] boolean routing tensor
|
| 196 |
+
routing = F.one_hot(top_k_index, num_classes=self.num_experts).permute(2, 1, 0)
|
| 197 |
+
used = (routing.sum(dim=(-1, -2)) > 0).nonzero().flatten()
|
| 198 |
+
for e in used.tolist():
|
| 199 |
+
if e == self.num_experts:
|
| 200 |
+
continue
|
| 201 |
+
slot, tok = torch.where(routing[e])
|
| 202 |
+
hidden = self._apply_gate(F.linear(hidden_states[tok], self.gate_up_proj[e]))
|
| 203 |
+
hidden = F.linear(hidden, self.down_proj[e]) * top_k_weights[tok, slot, None]
|
| 204 |
+
out.index_add_(0, tok, hidden.to(out.dtype))
|
| 205 |
+
return out
|
| 206 |
+
|
| 207 |
+
def _apply_gate(self, gate_up: torch.Tensor) -> torch.Tensor:
|
| 208 |
+
# Method (not inlined) so the fused grouped_mm / batched_mm expert backends
|
| 209 |
+
# from `@use_experts_implementation` apply the identical clip + SiLU on top
|
| 210 |
+
# of their packed gate/up output instead of bypassing it.
|
| 211 |
+
gate, value = gate_up.chunk(2, dim=-1)
|
| 212 |
+
gated = self.act_fn(gate.clamp(max=self.limit))
|
| 213 |
+
return gated * value.clamp(min=-self.limit, max=self.limit)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class AgnesTopKRouter(nn.Module):
|
| 217 |
+
def __init__(self, config: AgnesConfig):
|
| 218 |
+
super().__init__()
|
| 219 |
+
self.top_k = config.num_experts_per_tok
|
| 220 |
+
self.num_experts = config.num_local_experts
|
| 221 |
+
self.hidden_dim = config.hidden_size
|
| 222 |
+
self.weight = nn.Parameter(torch.empty(self.num_experts, self.hidden_dim))
|
| 223 |
+
self.score_fn = ACT2FN[config.scoring_func]
|
| 224 |
+
self.routed_scaling_factor = config.routed_scaling_factor
|
| 225 |
+
self.register_buffer("e_score_correction_bias", torch.zeros(self.num_experts), persistent=True)
|
| 226 |
+
|
| 227 |
+
def forward(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 228 |
+
tokens = hidden_states.reshape(-1, self.hidden_dim)
|
| 229 |
+
router_logits = F.linear(tokens, self.weight)
|
| 230 |
+
affinity = self.score_fn(router_logits)
|
| 231 |
+
# bias only steers the top-k *selection*; the gathered gate values are the
|
| 232 |
+
# un-biased affinities, renormalised to sum to one over the chosen experts.
|
| 233 |
+
chosen = torch.topk(affinity + self.e_score_correction_bias, self.top_k, dim=-1, sorted=False).indices
|
| 234 |
+
gate = affinity.gather(1, chosen)
|
| 235 |
+
gate = gate / (gate.sum(dim=-1, keepdim=True) + 1e-20)
|
| 236 |
+
return router_logits, gate * self.routed_scaling_factor, chosen
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
class AgnesHashRouter(nn.Module):
|
| 240 |
+
r"""
|
| 241 |
+
Static hash routing used by the leading `agnes_hash_moe` MoE layers. Which
|
| 242 |
+
experts a token visits is fixed up front by a frozen `tid2eid[input_ids]`
|
| 243 |
+
lookup table (token id -> expert ids) rather than by a learned arg-top-k. The
|
| 244 |
+
learned `weight` is still evaluated to produce the affinities that *weight*
|
| 245 |
+
those experts' outputs; only the selection itself is frozen.
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
def __init__(self, config: AgnesConfig):
|
| 249 |
+
super().__init__()
|
| 250 |
+
self.top_k = config.num_experts_per_tok
|
| 251 |
+
self.num_experts = config.num_local_experts
|
| 252 |
+
self.hidden_dim = config.hidden_size
|
| 253 |
+
self.weight = nn.Parameter(torch.empty(self.num_experts, self.hidden_dim))
|
| 254 |
+
self.score_fn = ACT2FN[config.scoring_func]
|
| 255 |
+
self.routed_scaling_factor = config.routed_scaling_factor
|
| 256 |
+
self.register_buffer("tid2eid", torch.zeros(config.vocab_size, self.top_k, dtype=torch.long), persistent=True)
|
| 257 |
+
|
| 258 |
+
def forward(
|
| 259 |
+
self, hidden_states: torch.Tensor, input_ids: torch.Tensor
|
| 260 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 261 |
+
tokens = hidden_states.reshape(-1, self.hidden_dim)
|
| 262 |
+
router_logits = F.linear(tokens, self.weight)
|
| 263 |
+
affinity = self.score_fn(router_logits)
|
| 264 |
+
chosen = self.tid2eid[input_ids.reshape(-1)].long() # frozen per-token expert ids
|
| 265 |
+
gate = affinity.gather(1, chosen)
|
| 266 |
+
gate = gate / (gate.sum(dim=-1, keepdim=True) + 1e-20)
|
| 267 |
+
return router_logits, gate * self.routed_scaling_factor, chosen
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class AgnesSparseMoeBlock(nn.Module):
|
| 271 |
+
def __init__(self, config: AgnesConfig, layer_idx: int):
|
| 272 |
+
super().__init__()
|
| 273 |
+
self.is_hash = config.mlp_layer_types[layer_idx] == "agnes_hash_moe"
|
| 274 |
+
self.gate = AgnesHashRouter(config) if self.is_hash else AgnesTopKRouter(config)
|
| 275 |
+
self.experts = AgnesExperts(config)
|
| 276 |
+
self.shared_experts = AgnesMLP(config)
|
| 277 |
+
pffn_size = getattr(config, "parallel_ffn_intermediate_size", 0) or 0
|
| 278 |
+
# parallel dense FFN branch; down_proj is zero-initialized at export so the
|
| 279 |
+
# block is exactly identity-preserving until trained
|
| 280 |
+
self.parallel_ffn = (
|
| 281 |
+
AgnesMLP(config, intermediate_size=pffn_size) if pffn_size and not self.is_hash else None
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
def forward(self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None) -> torch.Tensor:
|
| 285 |
+
b, s, d = hidden_states.shape
|
| 286 |
+
tokens = hidden_states.reshape(-1, d)
|
| 287 |
+
# hash layers additionally consume the raw token ids for the frozen lookup
|
| 288 |
+
if self.is_hash:
|
| 289 |
+
_, gate_w, gate_idx = self.gate(hidden_states, input_ids)
|
| 290 |
+
else:
|
| 291 |
+
_, gate_w, gate_idx = self.gate(hidden_states)
|
| 292 |
+
result = self.experts(tokens, gate_idx, gate_w).view(b, s, d)
|
| 293 |
+
result = result + self.shared_experts(hidden_states)
|
| 294 |
+
if self.parallel_ffn is not None: # zero-init identity branch until trained
|
| 295 |
+
result = result + self.parallel_ffn(hidden_states)
|
| 296 |
+
return result
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# ==========================================================================
|
| 301 |
+
# Residual hyper-connections (mHC)
|
| 302 |
+
# ==========================================================================
|
| 303 |
+
|
| 304 |
+
class AgnesHyperConnection(nn.Module):
|
| 305 |
+
r"""
|
| 306 |
+
Manifold-constrained hyper-connection (mHC). Where a plain block does a
|
| 307 |
+
single scalar residual add, Agnes keeps `hc_mult` parallel residual streams
|
| 308 |
+
and learns how to (i) collapse them into the sublayer input, (ii) place the
|
| 309 |
+
sublayer output back across them, and (iii) re-mix the streams themselves.
|
| 310 |
+
|
| 311 |
+
A single small projection (`fn`, biased by `base`, per-branch `scale`) maps
|
| 312 |
+
the `hc_mult` streams — after an unweighted RMS norm and a flatten — into
|
| 313 |
+
three groups of logits:
|
| 314 |
+
|
| 315 |
+
* `pre` (length `hc_mult`) : sigmoid gate that collapses the streams
|
| 316 |
+
into one sequence for the sublayer.
|
| 317 |
+
* `post` (length `hc_mult`) : `2·sigmoid` gate in [0, 2] that spreads
|
| 318 |
+
the sublayer output back over streams.
|
| 319 |
+
* `comb` (`hc_mult × hc_mult`) : softmaxed then Sinkhorn-projected onto
|
| 320 |
+
the doubly-stochastic manifold; mixes
|
| 321 |
+
the streams across the residual.
|
| 322 |
+
|
| 323 |
+
The decoder layer holds two of these, one at the attention site and one at
|
| 324 |
+
the MLP site.
|
| 325 |
+
"""
|
| 326 |
+
|
| 327 |
+
def __init__(self, config: AgnesConfig):
|
| 328 |
+
super().__init__()
|
| 329 |
+
self.hc_mult = config.hc_mult
|
| 330 |
+
self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
|
| 331 |
+
self.hc_eps = config.hc_eps
|
| 332 |
+
self.input_norm = AgnesUnweightedRMSNorm(eps=config.rms_norm_eps)
|
| 333 |
+
n_logits = (2 + self.hc_mult) * self.hc_mult
|
| 334 |
+
self.fn = nn.Parameter(torch.empty(n_logits, self.hc_mult * config.hidden_size))
|
| 335 |
+
self.base = nn.Parameter(torch.empty(n_logits))
|
| 336 |
+
# one learned scale each for the pre / post / comb logit groups
|
| 337 |
+
self.scale = nn.Parameter(torch.empty(3))
|
| 338 |
+
|
| 339 |
+
def forward(self, hidden_streams: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 340 |
+
r"""Return `(post, comb, collapsed)`: the output-placement gate, the
|
| 341 |
+
doubly-stochastic stream-mixing matrix, and the collapsed sublayer input.
|
| 342 |
+
`comb` is driven onto the doubly-stochastic manifold by Sinkhorn-Knopp —
|
| 343 |
+
an initial column normalisation followed by `hc_sinkhorn_iters - 1`
|
| 344 |
+
alternating row/column passes."""
|
| 345 |
+
n = self.hc_mult
|
| 346 |
+
normed = self.input_norm(hidden_streams.flatten(start_dim=2).float())
|
| 347 |
+
pre_raw, post_raw, comb_raw = F.linear(normed, self.fn.float()).split([n, n, n * n], dim=-1)
|
| 348 |
+
pre_bias, post_bias, comb_bias = self.base.split([n, n, n * n])
|
| 349 |
+
pre_scale, post_scale, comb_scale = self.scale.unbind(0)
|
| 350 |
+
|
| 351 |
+
pre = torch.sigmoid(pre_raw * pre_scale + pre_bias) + self.hc_eps
|
| 352 |
+
post = 2 * torch.sigmoid(post_raw * post_scale + post_bias)
|
| 353 |
+
|
| 354 |
+
comb = comb_raw.view(*comb_raw.shape[:-1], n, n) * comb_scale + comb_bias.view(n, n)
|
| 355 |
+
comb = torch.softmax(comb, dim=-1) + self.hc_eps
|
| 356 |
+
comb = comb / (comb.sum(dim=-2, keepdim=True) + self.hc_eps)
|
| 357 |
+
for _ in range(self.hc_sinkhorn_iters - 1):
|
| 358 |
+
comb = comb / (comb.sum(dim=-1, keepdim=True) + self.hc_eps)
|
| 359 |
+
comb = comb / (comb.sum(dim=-2, keepdim=True) + self.hc_eps)
|
| 360 |
+
|
| 361 |
+
# weighted sum over the stream axis -> single sequence fed to the sublayer
|
| 362 |
+
collapsed = (pre.unsqueeze(-1) * hidden_streams).sum(dim=2).to(hidden_streams.dtype)
|
| 363 |
+
return post, comb, collapsed
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class AgnesHyperHead(nn.Module):
|
| 367 |
+
"""Collapses the `hc_mult` residual streams back into one tensor at the top
|
| 368 |
+
of the stack, just before the final shared RMS norm in `AgnesModel`."""
|
| 369 |
+
|
| 370 |
+
def __init__(self, config: AgnesConfig):
|
| 371 |
+
super().__init__()
|
| 372 |
+
self.hc_mult = config.hc_mult
|
| 373 |
+
self.input_norm = AgnesUnweightedRMSNorm(eps=config.rms_norm_eps)
|
| 374 |
+
self.eps = config.hc_eps
|
| 375 |
+
self.hc_fn = nn.Parameter(torch.empty(self.hc_mult, self.hc_mult * config.hidden_size))
|
| 376 |
+
self.hc_base = nn.Parameter(torch.empty(self.hc_mult))
|
| 377 |
+
self.hc_scale = nn.Parameter(torch.empty(1))
|
| 378 |
+
|
| 379 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 380 |
+
# same gate-and-sum as the `pre` branch of AgnesHyperConnection, but with
|
| 381 |
+
# no sublayer to feed — this is the terminal stream collapse.
|
| 382 |
+
gate_logits = F.linear(self.input_norm(x.flatten(2).float()), self.hc_fn.float())
|
| 383 |
+
gate = torch.sigmoid(gate_logits * self.hc_scale.float() + self.hc_base.float()) + self.eps
|
| 384 |
+
return (gate.unsqueeze(-1) * x).sum(dim=2).to(x.dtype)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
# ==========================================================================
|
| 389 |
+
# Rotary position embedding
|
| 390 |
+
# ==========================================================================
|
| 391 |
+
|
| 392 |
+
class AgnesRotaryEmbedding(nn.Module):
|
| 393 |
+
"""
|
| 394 |
+
Rotary embedding that maintains one inverse-frequency buffer per *rope
|
| 395 |
+
label* and applies interleaved (rather than half-split) rotation with
|
| 396 |
+
partial coverage of each head.
|
| 397 |
+
|
| 398 |
+
Two things are specific to Agnes here:
|
| 399 |
+
|
| 400 |
+
* Interleaved layout — a single `θ_i` is shared by the two channels of
|
| 401 |
+
each consecutive pair, so only `rope_head_dim // 2` frequencies are
|
| 402 |
+
stored. The doubling back up to the full rope width happens next to the
|
| 403 |
+
rotation itself (in `apply_agnes_rope`) instead of being baked into the
|
| 404 |
+
cached cos/sin, which keeps the pairing explicit at the point of use.
|
| 405 |
+
|
| 406 |
+
* Rope labels are independent of the attention schedule. The architectural
|
| 407 |
+
`layer_types` (`agnes_local_attention` / `agnes_sparse_attention` /
|
| 408 |
+
`agnes_pooled_attention`) are decoupled from the rope labels
|
| 409 |
+
(`main` / `compress`), which live as keys in `config.rope_parameters`
|
| 410 |
+
and differ only in their `rope_theta` base. Buffers are therefore built
|
| 411 |
+
by iterating `rope_parameters` rather than the attention schedule.
|
| 412 |
+
"""
|
| 413 |
+
|
| 414 |
+
inv_freq: torch.Tensor # fix linting for `register_buffer`
|
| 415 |
+
|
| 416 |
+
def __init__(self, config: AgnesConfig):
|
| 417 |
+
super().__init__()
|
| 418 |
+
self.max_seq_len_cached = config.max_position_embeddings
|
| 419 |
+
self.original_max_seq_len = config.max_position_embeddings
|
| 420 |
+
self.config = config
|
| 421 |
+
# Only the nested per-rope-type sub-dicts are real layer types — the top-level
|
| 422 |
+
# `rope_type` key that ``convert_rope_params_to_dict`` may leave on
|
| 423 |
+
# ``config.rope_parameters`` is a flat-shape leftover, not a layer.
|
| 424 |
+
self.layer_types = [k for k, v in config.rope_parameters.items() if isinstance(v, dict)]
|
| 425 |
+
self.rope_type = {}
|
| 426 |
+
for layer_type in self.layer_types:
|
| 427 |
+
rope_params = config.rope_parameters[layer_type]
|
| 428 |
+
self.rope_type[layer_type] = rope_params["rope_type"]
|
| 429 |
+
rope_init_fn = self.compute_default_rope_parameters
|
| 430 |
+
if self.rope_type[layer_type] != "default":
|
| 431 |
+
rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type[layer_type]]
|
| 432 |
+
inv_freq, attention_scaling = rope_init_fn(config, layer_type=layer_type)
|
| 433 |
+
self.register_buffer(f"{layer_type}_inv_freq", inv_freq, persistent=False)
|
| 434 |
+
self.register_buffer(f"{layer_type}_original_inv_freq", inv_freq.clone(), persistent=False)
|
| 435 |
+
setattr(self, f"{layer_type}_attention_scaling", attention_scaling)
|
| 436 |
+
|
| 437 |
+
@staticmethod
|
| 438 |
+
def compute_default_rope_parameters(
|
| 439 |
+
config: AgnesConfig | None = None,
|
| 440 |
+
device: Optional["torch.device"] = None,
|
| 441 |
+
seq_len: int | None = None,
|
| 442 |
+
layer_type: str | None = None,
|
| 443 |
+
) -> tuple["torch.Tensor", float]:
|
| 444 |
+
"""Plain (non-scaled) inverse frequencies for one rope label.
|
| 445 |
+
|
| 446 |
+
Returns `(inv_freq, attention_factor)`. `attention_factor` is always 1.0
|
| 447 |
+
for this default variant (the scaled variants such as YaRN come from
|
| 448 |
+
`ROPE_INIT_FUNCTIONS` instead). Only the leading `partial_rotary_factor`
|
| 449 |
+
fraction of each head receives rotation, so the frequency table has
|
| 450 |
+
`rope_dim // 2` entries. `seq_len` is accepted for signature parity and
|
| 451 |
+
unused here.
|
| 452 |
+
"""
|
| 453 |
+
rope_cfg = config.rope_parameters[layer_type]
|
| 454 |
+
base = rope_cfg["rope_theta"]
|
| 455 |
+
head_dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
|
| 456 |
+
rope_dim = int(head_dim * rope_cfg.get("partial_rotary_factor", 1.0))
|
| 457 |
+
|
| 458 |
+
exponents = torch.arange(0, rope_dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / rope_dim
|
| 459 |
+
inv_freq = 1.0 / (base**exponents)
|
| 460 |
+
return inv_freq, 1.0
|
| 461 |
+
|
| 462 |
+
@torch.no_grad()
|
| 463 |
+
@dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope)
|
| 464 |
+
def forward(self, x, position_ids, layer_type=None):
|
| 465 |
+
# cos/sin carry one entry per interleaved pair; the widening to full rope
|
| 466 |
+
# width is left to `apply_agnes_rope` so the pairing stays where it is used.
|
| 467 |
+
inv_freq = getattr(self, f"{layer_type}_inv_freq")
|
| 468 |
+
scaling = getattr(self, f"{layer_type}_attention_scaling")
|
| 469 |
+
# outer product positions (B, S) x frequencies (rope_dim/2) -> (B, S, rope_dim/2)
|
| 470 |
+
freq_col = inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
|
| 471 |
+
pos_row = position_ids[:, None, :].float()
|
| 472 |
+
device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
|
| 473 |
+
with maybe_autocast(device_type=device_type, enabled=False):
|
| 474 |
+
angles = (freq_col.float() @ pos_row.float()).transpose(1, 2)
|
| 475 |
+
cos = angles.cos() * scaling
|
| 476 |
+
sin = angles.sin() * scaling
|
| 477 |
+
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def _swap_pairs(x: torch.Tensor) -> torch.Tensor:
|
| 481 |
+
"""Interleaved-pair rotation partner: within every adjacent `(a, b)` pair
|
| 482 |
+
return `(-b, a)`. Equivalent to the `[-x_odd, x_even]` interleave used by
|
| 483 |
+
the rotation formula below."""
|
| 484 |
+
pairs = x.unflatten(-1, (-1, 2))
|
| 485 |
+
a, b = pairs[..., 0], pairs[..., 1]
|
| 486 |
+
return torch.stack((-b, a), dim=-1).flatten(-2)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def apply_agnes_rope(
|
| 490 |
+
x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, unsqueeze_dim: int = 1
|
| 491 |
+
) -> torch.Tensor:
|
| 492 |
+
"""Apply interleaved rotary position embedding to the trailing rope slice.
|
| 493 |
+
|
| 494 |
+
The cached `cos` / `sin` carry one entry per interleaved pair. They are
|
| 495 |
+
widened to the full rope dimension right here (`repeat_interleave(2)`), the
|
| 496 |
+
leading "no-position" channels are passed through untouched, and only the
|
| 497 |
+
final `2 * cos.shape[-1]` channels are rotated via
|
| 498 |
+
`x * cos + swap(x) * sin`. The rotation math is done in float32 and cast
|
| 499 |
+
back to `x`'s dtype. Each head is laid out as `[nope | rope]`.
|
| 500 |
+
"""
|
| 501 |
+
cos = cos.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
|
| 502 |
+
sin = sin.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
|
| 503 |
+
rope_dim = cos.shape[-1]
|
| 504 |
+
split_at = x.shape[-1] - rope_dim
|
| 505 |
+
nope, rope = x[..., :split_at], x[..., split_at:]
|
| 506 |
+
rotated = (rope.float() * cos + _swap_pairs(rope).float() * sin).to(x.dtype)
|
| 507 |
+
return torch.cat([nope, rotated], dim=-1)
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# ==========================================================================
|
| 512 |
+
# Attention KV caches
|
| 513 |
+
# ==========================================================================
|
| 514 |
+
|
| 515 |
+
class AgnesHCACache(DynamicSlidingWindowLayer):
|
| 516 |
+
r"""Cache layer for HCA blocks. Holds the long-range compressor's
|
| 517 |
+
buffer / running compressed entries / count on top of the sliding-window K=V
|
| 518 |
+
branch. HCA uses *non-overlapping* windows, so there is *no* overlap state,
|
| 519 |
+
and HCA has *no* indexer either.
|
| 520 |
+
|
| 521 |
+
State is dict-keyed by entry name — HCA only uses `"compressor"`, but
|
| 522 |
+
:class:`AgnesCSACache` adds `"indexer"` to the same dicts so a single
|
| 523 |
+
set of methods (`store_compression_weights` / `update_compressor_states`)
|
| 524 |
+
serves both:
|
| 525 |
+
|
| 526 |
+
* `compressed_kv[name]` — the running list of compressed KV entries
|
| 527 |
+
emitted so far (one every `compress_rate` source tokens; the long-range
|
| 528 |
+
KVs the attention concatenates onto its sliding-window keys / values).
|
| 529 |
+
* `buffer_kv[name]` / `buffer_gate[name]` — source tokens that arrived
|
| 530 |
+
between two full windows; once the buffer hits `compress_rate` tokens
|
| 531 |
+
the compressor closes a window, emits one entry, and drains the buffer.
|
| 532 |
+
* `entry_count[name]` — number of compressed entries emitted so far, so
|
| 533 |
+
`entry_count[name] * compress_rate` is the absolute position of the
|
| 534 |
+
*next* window's first source token. Tracked separately from
|
| 535 |
+
`position_ids` so prefill -> decode -> prefill stays consistent.
|
| 536 |
+
"""
|
| 537 |
+
|
| 538 |
+
layer_type = "agnes_pooled_attention"
|
| 539 |
+
|
| 540 |
+
def __init__(self, config: "AgnesConfig"):
|
| 541 |
+
super().__init__(config)
|
| 542 |
+
self.compress_rate = config.compress_rates["agnes_pooled_attention"]
|
| 543 |
+
self.buffer_kv: dict[str, torch.Tensor | None] = {"compressor": None}
|
| 544 |
+
self.buffer_gate: dict[str, torch.Tensor | None] = {"compressor": None}
|
| 545 |
+
self.compressed_kv: dict[str, torch.Tensor | None] = {"compressor": None}
|
| 546 |
+
self.entry_count: dict[str, int] = {"compressor": 0}
|
| 547 |
+
|
| 548 |
+
def update(self, key_states: torch.Tensor, value_states: torch.Tensor, *args, **kwargs):
|
| 549 |
+
"""Sliding-window key/value update. Because Agnes is shared-KV MQA, keys
|
| 550 |
+
and values are one and the same buffer, so both returns are the full
|
| 551 |
+
(pre-trim) concatenation while the retained cache keeps only the last
|
| 552 |
+
`sliding_window - 1` positions."""
|
| 553 |
+
if not self.is_initialized:
|
| 554 |
+
self.lazy_initialization(key_states, value_states)
|
| 555 |
+
self.values = self.keys
|
| 556 |
+
self.cumulative_length += key_states.shape[-2]
|
| 557 |
+
combined = torch.cat([self.keys, key_states], dim=-2)
|
| 558 |
+
self.keys = combined[:, :, -self.sliding_window + 1 :, :]
|
| 559 |
+
self.values = self.keys
|
| 560 |
+
return combined, combined
|
| 561 |
+
|
| 562 |
+
def store_compression_weights(
|
| 563 |
+
self, name: str, kv: torch.Tensor, gate: torch.Tensor
|
| 564 |
+
) -> tuple[torch.Tensor, torch.Tensor, int]:
|
| 565 |
+
r"""Prepend the leftover buffered `(kv, gate)` for entry `name`, split
|
| 566 |
+
off the longest window-aligned prefix (the part that can be compressed
|
| 567 |
+
now), stash the remainder back in the buffer, and hand back
|
| 568 |
+
`(chunk_kv, chunk_gate, first_window_position)`. The compressor then
|
| 569 |
+
softmax-pools each `compress_rate`-token window of that chunk (with its
|
| 570 |
+
`position_bias`) into one compressed entry.
|
| 571 |
+
"""
|
| 572 |
+
first_window_position = self.entry_count[name] * self.compress_rate
|
| 573 |
+
held_kv, held_gate = self.buffer_kv[name], self.buffer_gate[name]
|
| 574 |
+
if held_kv is not None and held_kv.shape[1]:
|
| 575 |
+
kv = torch.cat([held_kv, kv], dim=1)
|
| 576 |
+
gate = torch.cat([held_gate, gate], dim=1)
|
| 577 |
+
# split at the largest multiple of compress_rate; carry the tail forward
|
| 578 |
+
cut = (kv.shape[1] // self.compress_rate) * self.compress_rate
|
| 579 |
+
self.buffer_kv[name], self.buffer_gate[name] = kv[:, cut:], gate[:, cut:]
|
| 580 |
+
return kv[:, :cut], gate[:, :cut], first_window_position
|
| 581 |
+
|
| 582 |
+
def update_compressor_states(self, name: str, compressed: torch.Tensor) -> torch.Tensor:
|
| 583 |
+
r"""Append the newly emitted compressed entries to the running
|
| 584 |
+
`compressed_kv[name]`, advance `entry_count[name]`, and return the
|
| 585 |
+
accumulated tensor."""
|
| 586 |
+
running = self.compressed_kv[name]
|
| 587 |
+
if running is None:
|
| 588 |
+
self.compressed_kv[name] = compressed
|
| 589 |
+
elif compressed.shape[1] > 0:
|
| 590 |
+
self.compressed_kv[name] = torch.cat([running, compressed], dim=1)
|
| 591 |
+
self.entry_count[name] += compressed.shape[1]
|
| 592 |
+
return self.compressed_kv[name]
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
class AgnesCSACache(AgnesHCACache):
|
| 596 |
+
r"""CSA cache. On top of :class:`AgnesHCACache` it registers a second entry
|
| 597 |
+
name, `"indexer"`, in each of the inherited state dicts, and it keeps a small
|
| 598 |
+
per-name *overlap* buffer required by the two-series windowing.
|
| 599 |
+
|
| 600 |
+
Why the overlap buffer exists: for CSA, `kv_proj` / `gate_proj` emit
|
| 601 |
+
`2 * head_dim` features per token — two interleaved series, Ca in
|
| 602 |
+
`[..., :head_dim]` and Cb in `[..., head_dim:]`. The pooled entry for window
|
| 603 |
+
`w` blends window `w-1`'s Ca with window `w`'s Cb (softmax-gated), giving an
|
| 604 |
+
effective receptive width of `2 * compress_rate_csa` at stride
|
| 605 |
+
`compress_rate_csa`. The only cross-window dependency is therefore the
|
| 606 |
+
previous window's Ca slice, so at a forward boundary we persist exactly
|
| 607 |
+
`chunk[:, -1, :, :head_dim]` (Ca of the last full window) in
|
| 608 |
+
`overlap_kv[name]` / `overlap_gate[name]`; Cb is never revisited.
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
layer_type = "agnes_sparse_attention"
|
| 612 |
+
|
| 613 |
+
def __init__(self, config: "AgnesConfig"):
|
| 614 |
+
super().__init__(config)
|
| 615 |
+
self.compress_rate = config.compress_rates["agnes_sparse_attention"]
|
| 616 |
+
self.buffer_kv["indexer"] = None
|
| 617 |
+
self.buffer_gate["indexer"] = None
|
| 618 |
+
self.compressed_kv["indexer"] = None
|
| 619 |
+
self.entry_count["indexer"] = 0
|
| 620 |
+
self.overlap_kv: dict[str, torch.Tensor | None] = {"compressor": None, "indexer": None}
|
| 621 |
+
self.overlap_gate: dict[str, torch.Tensor | None] = {"compressor": None, "indexer": None}
|
| 622 |
+
|
| 623 |
+
def update_overlap_state(
|
| 624 |
+
self, name: str, chunk_kv: torch.Tensor, chunk_gate: torch.Tensor, head_dim: int
|
| 625 |
+
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
| 626 |
+
r"""Swap the overlap buffer: return the Ca slice saved on the previous
|
| 627 |
+
call (or `None` on the first) and store this call's last-window Ca slice
|
| 628 |
+
for the next one. Only `[..., :head_dim]` (Ca) is kept, since Cb is
|
| 629 |
+
already baked into an emitted entry and never read again.
|
| 630 |
+
"""
|
| 631 |
+
carried_kv, carried_gate = self.overlap_kv[name], self.overlap_gate[name]
|
| 632 |
+
self.overlap_kv[name] = chunk_kv[:, -1, :, :head_dim].clone()
|
| 633 |
+
self.overlap_gate[name] = chunk_gate[:, -1, :, :head_dim].clone()
|
| 634 |
+
return carried_kv, carried_gate
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
# ==========================================================================
|
| 639 |
+
# Long-range compressors and lightning indexer
|
| 640 |
+
# ==========================================================================
|
| 641 |
+
|
| 642 |
+
class AgnesHCACompressor(nn.Module):
|
| 643 |
+
"""
|
| 644 |
+
Heavily Compressed Attention compressor. Compresses
|
| 645 |
+
every `compress_rate_hca` (m'=128) source tokens into a single compressed KV
|
| 646 |
+
entry.
|
| 647 |
+
|
| 648 |
+
Each closed window of m' tokens produces one compressed entry:
|
| 649 |
+
`C^{Comp}_i = Σ_{j∈window} softmax(Z_j + B)_j ⊙ C_j`. RoPE on the trailing
|
| 650 |
+
`rope_head_dim` slice is applied at the deterministic absolute position
|
| 651 |
+
`i * compress_rate_hca + first_window_position` so cross-call concatenation
|
| 652 |
+
stays causality-correct. Returns the running list of *all* compressed
|
| 653 |
+
entries emitted so far (shape `[B, 1, T, head_dim]` with
|
| 654 |
+
`T = entry_count["compressor"]`), so the attention can attend over the
|
| 655 |
+
full long-range history.
|
| 656 |
+
|
| 657 |
+
When `past_key_values is None` runs in stateless single-shot mode: compress
|
| 658 |
+
every complete window from `hidden_states` and discard the remainder
|
| 659 |
+
(instead of caching it).
|
| 660 |
+
"""
|
| 661 |
+
|
| 662 |
+
rope_layer_type = "compress"
|
| 663 |
+
|
| 664 |
+
def __init__(self, config: AgnesConfig):
|
| 665 |
+
super().__init__()
|
| 666 |
+
self.compress_rate = config.compress_rates["agnes_pooled_attention"]
|
| 667 |
+
self.head_dim = config.head_dim
|
| 668 |
+
self.kv_proj = nn.Linear(config.hidden_size, self.head_dim, bias=False)
|
| 669 |
+
self.gate_proj = nn.Linear(config.hidden_size, self.head_dim, bias=False)
|
| 670 |
+
self.position_bias = nn.Parameter(torch.empty(self.compress_rate, self.head_dim))
|
| 671 |
+
self.kv_norm = AgnesRMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 672 |
+
self.rotary_emb = AgnesRotaryEmbedding(config)
|
| 673 |
+
|
| 674 |
+
def forward(
|
| 675 |
+
self,
|
| 676 |
+
hidden_states: torch.Tensor,
|
| 677 |
+
q_residual: torch.Tensor,
|
| 678 |
+
position_ids: torch.Tensor,
|
| 679 |
+
past_key_values: Cache | None,
|
| 680 |
+
layer_idx: int,
|
| 681 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 682 |
+
batch = hidden_states.shape[0]
|
| 683 |
+
cache_layer: AgnesHCACache = past_key_values.layers[layer_idx] if past_key_values is not None else None
|
| 684 |
+
kv = self.kv_proj(hidden_states)
|
| 685 |
+
gate = self.gate_proj(hidden_states)
|
| 686 |
+
# stateless mode drops the ragged tail; cached mode buffers it for later
|
| 687 |
+
if cache_layer is None:
|
| 688 |
+
aligned = (kv.shape[1] // self.compress_rate) * self.compress_rate
|
| 689 |
+
chunk_kv, chunk_gate, first_window_position = kv[:, :aligned], gate[:, :aligned], 0
|
| 690 |
+
else:
|
| 691 |
+
chunk_kv, chunk_gate, first_window_position = cache_layer.store_compression_weights("compressor", kv, gate)
|
| 692 |
+
|
| 693 |
+
if chunk_kv.shape[1] > 0: # at least one full window is ready
|
| 694 |
+
n_windows = chunk_kv.shape[1] // self.compress_rate
|
| 695 |
+
chunk_kv = chunk_kv.view(batch, n_windows, self.compress_rate, -1)
|
| 696 |
+
biased_gate = chunk_gate.view(batch, n_windows, self.compress_rate, -1) + self.position_bias
|
| 697 |
+
# softmax pooling within each window (fp32 for stability), then norm
|
| 698 |
+
pool_w = biased_gate.softmax(dim=2, dtype=torch.float32).to(chunk_kv.dtype)
|
| 699 |
+
compressed = self.kv_norm((chunk_kv * pool_w).sum(dim=2))
|
| 700 |
+
abs_pos = torch.arange(n_windows, device=compressed.device) * self.compress_rate + first_window_position
|
| 701 |
+
abs_pos = abs_pos.unsqueeze(0).expand(batch, -1)
|
| 702 |
+
cos, sin = self.rotary_emb(compressed, position_ids=abs_pos, layer_type=self.rope_layer_type)
|
| 703 |
+
compressed = apply_agnes_rope(compressed.unsqueeze(1), cos, sin).squeeze(1)
|
| 704 |
+
else:
|
| 705 |
+
compressed = chunk_kv.new_zeros((batch, 0, self.head_dim))
|
| 706 |
+
|
| 707 |
+
if cache_layer is not None:
|
| 708 |
+
compressed = cache_layer.update_compressor_states("compressor", compressed)
|
| 709 |
+
compressed_kv = compressed.unsqueeze(1)
|
| 710 |
+
|
| 711 |
+
n_entries = compressed_kv.shape[2]
|
| 712 |
+
seq_len = position_ids.shape[1]
|
| 713 |
+
if seq_len == 1 or n_entries == 0:
|
| 714 |
+
return compressed_kv, None
|
| 715 |
+
|
| 716 |
+
# a query at position t may only see entry w once t has passed window w,
|
| 717 |
+
# i.e. w < (t + 1) // compress_rate; everything else is masked to -inf.
|
| 718 |
+
entry_pos = torch.arange(n_entries, device=compressed_kv.device)
|
| 719 |
+
ready = (position_ids + 1) // self.compress_rate # [B, S]
|
| 720 |
+
block_bias = compressed_kv.new_zeros((batch, 1, seq_len, n_entries))
|
| 721 |
+
block_bias = block_bias.masked_fill(
|
| 722 |
+
entry_pos.view(1, 1, 1, -1) >= ready.unsqueeze(1).unsqueeze(-1),
|
| 723 |
+
float("-inf"),
|
| 724 |
+
)
|
| 725 |
+
return compressed_kv, block_bias
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
class AgnesIndexerScorer(nn.Module):
|
| 729 |
+
r"""Lightning-indexer scoring head: `∑_h w_{t,h} · ReLU(q_{t,h} · K^IComp_s)`."""
|
| 730 |
+
|
| 731 |
+
def __init__(self, config: AgnesConfig):
|
| 732 |
+
super().__init__()
|
| 733 |
+
self.softmax_scale = config.index_head_dim**-0.5
|
| 734 |
+
self.weights_scaling = config.index_n_heads**-0.5
|
| 735 |
+
self.weights_proj = nn.Linear(config.hidden_size, config.index_n_heads, bias=False)
|
| 736 |
+
|
| 737 |
+
def forward(self, q: torch.Tensor, compressed_kv: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 738 |
+
# per-head query·key, ReLU-rectified and scaled -> [B, S, H, T]
|
| 739 |
+
qk = torch.matmul(q.float(), compressed_kv.transpose(-1, -2).float().unsqueeze(1))
|
| 740 |
+
qk = F.relu(qk) * self.softmax_scale
|
| 741 |
+
head_w = self.weights_proj(hidden_states).float() * self.weights_scaling # [B, S, H]
|
| 742 |
+
# collapse the head axis with the learned per-head weights -> [B, S, T]
|
| 743 |
+
return (qk * head_w.unsqueeze(-1)).sum(dim=2)
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
class AgnesIndexer(nn.Module):
|
| 747 |
+
r"""Lightning indexer for CSA. For each query it keeps only the top
|
| 748 |
+
`config.index_topk` of the compressed entries, cutting the effective KV set
|
| 749 |
+
from `seq_len / compress_rate_csa` down to `index_topk`.
|
| 750 |
+
|
| 751 |
+
It builds a miniature compressor of its own at `index_head_dim` over the
|
| 752 |
+
same windows as the outer CSA compressor, scores each query against those
|
| 753 |
+
compressed keys as `sum_h w_h * ReLU(q_h . k)`, and returns the winning
|
| 754 |
+
indices.
|
| 755 |
+
|
| 756 |
+
A dedicated rotary lives here because RoPE has to be applied twice, to two
|
| 757 |
+
tensors whose positions differ: the compressed keys sit at the fixed window
|
| 758 |
+
positions `i * compress_rate + first_window_position`, whereas the queries
|
| 759 |
+
sit at the (per-call) `position_ids`. Both use the compressor's theta
|
| 760 |
+
(`compress_rope_theta`) so that only relative offsets survive the dot
|
| 761 |
+
product; since the query positions change every call, cos/sin can't be
|
| 762 |
+
precomputed, so the indexer calls its rotary twice per forward (label
|
| 763 |
+
always `"compress"`).
|
| 764 |
+
"""
|
| 765 |
+
|
| 766 |
+
rope_layer_type = "compress"
|
| 767 |
+
|
| 768 |
+
def __init__(self, config: AgnesConfig):
|
| 769 |
+
super().__init__()
|
| 770 |
+
self.compress_rate = config.compress_rates["agnes_sparse_attention"]
|
| 771 |
+
self.num_heads = config.index_n_heads
|
| 772 |
+
self.head_dim = config.index_head_dim
|
| 773 |
+
self.index_topk = config.index_topk
|
| 774 |
+
self.kv_proj = nn.Linear(config.hidden_size, 2 * self.head_dim, bias=False)
|
| 775 |
+
self.gate_proj = nn.Linear(config.hidden_size, 2 * self.head_dim, bias=False)
|
| 776 |
+
self.position_bias = nn.Parameter(torch.empty(self.compress_rate, 2 * self.head_dim))
|
| 777 |
+
self.kv_norm = AgnesRMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 778 |
+
self.q_b_proj = nn.Linear(config.q_lora_rank, self.num_heads * self.head_dim, bias=False)
|
| 779 |
+
self.rotary_emb = AgnesRotaryEmbedding(config)
|
| 780 |
+
self.scorer = AgnesIndexerScorer(config)
|
| 781 |
+
|
| 782 |
+
def forward(
|
| 783 |
+
self,
|
| 784 |
+
hidden_states: torch.Tensor,
|
| 785 |
+
q_residual: torch.Tensor,
|
| 786 |
+
position_ids: torch.Tensor,
|
| 787 |
+
past_key_values: Cache | None,
|
| 788 |
+
layer_idx: int,
|
| 789 |
+
) -> torch.LongTensor:
|
| 790 |
+
batch, seq_len, _ = hidden_states.shape
|
| 791 |
+
cache_layer: AgnesCSACache = past_key_values.layers[layer_idx] if past_key_values is not None else None
|
| 792 |
+
kv = self.kv_proj(hidden_states)
|
| 793 |
+
gate = self.gate_proj(hidden_states)
|
| 794 |
+
|
| 795 |
+
if cache_layer is None:
|
| 796 |
+
aligned = (kv.shape[1] // self.compress_rate) * self.compress_rate
|
| 797 |
+
chunk_kv, chunk_gate, first_window_position = kv[:, :aligned], gate[:, :aligned], 0
|
| 798 |
+
else:
|
| 799 |
+
chunk_kv, chunk_gate, first_window_position = cache_layer.store_compression_weights("indexer", kv, gate)
|
| 800 |
+
|
| 801 |
+
if chunk_kv.shape[1] > 0:
|
| 802 |
+
n_windows = chunk_kv.shape[1] // self.compress_rate
|
| 803 |
+
m = self.compress_rate
|
| 804 |
+
chunk_kv = chunk_kv.view(batch, n_windows, m, -1)
|
| 805 |
+
chunk_gate = chunk_gate.view(batch, n_windows, m, -1) + self.position_bias
|
| 806 |
+
|
| 807 |
+
# widen each window to 2m slots: current-window Cb in the top half,
|
| 808 |
+
# previous-window Ca in the bottom half (same scheme as outer CSA).
|
| 809 |
+
win_kv = chunk_kv.new_zeros((batch, n_windows, 2 * m, self.head_dim))
|
| 810 |
+
win_gate = chunk_gate.new_full((batch, n_windows, 2 * m, self.head_dim), float("-inf"))
|
| 811 |
+
win_kv[:, :, m:] = chunk_kv[..., self.head_dim :]
|
| 812 |
+
win_gate[:, :, m:] = chunk_gate[..., self.head_dim :]
|
| 813 |
+
if n_windows > 1:
|
| 814 |
+
win_kv[:, 1:, :m] = chunk_kv[:, :-1, :, : self.head_dim]
|
| 815 |
+
win_gate[:, 1:, :m] = chunk_gate[:, :-1, :, : self.head_dim]
|
| 816 |
+
if cache_layer is not None:
|
| 817 |
+
carried_kv, carried_gate = cache_layer.update_overlap_state("indexer", chunk_kv, chunk_gate, self.head_dim)
|
| 818 |
+
if carried_kv is not None:
|
| 819 |
+
win_kv[:, 0, :m] = carried_kv.to(win_kv.dtype)
|
| 820 |
+
win_gate[:, 0, :m] = carried_gate.to(win_gate.dtype)
|
| 821 |
+
|
| 822 |
+
pool_w = win_gate.softmax(dim=2, dtype=torch.float32).to(win_kv.dtype)
|
| 823 |
+
compressed = self.kv_norm((win_kv * pool_w).sum(dim=2))
|
| 824 |
+
abs_pos = torch.arange(n_windows, device=compressed.device) * self.compress_rate + first_window_position
|
| 825 |
+
abs_pos = abs_pos.unsqueeze(0).expand(batch, -1)
|
| 826 |
+
cos, sin = self.rotary_emb(compressed, position_ids=abs_pos, layer_type=self.rope_layer_type)
|
| 827 |
+
compressed = apply_agnes_rope(compressed.unsqueeze(1), cos, sin).squeeze(1)
|
| 828 |
+
else:
|
| 829 |
+
compressed = chunk_kv.new_zeros((batch, 0, self.head_dim))
|
| 830 |
+
|
| 831 |
+
compressed_kv = (
|
| 832 |
+
compressed if cache_layer is None else cache_layer.update_compressor_states("indexer", compressed)
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
cos_q, sin_q = self.rotary_emb(hidden_states, position_ids=position_ids, layer_type=self.rope_layer_type)
|
| 836 |
+
q = self.q_b_proj(q_residual).view(batch, seq_len, -1, self.head_dim).transpose(1, 2)
|
| 837 |
+
q = apply_agnes_rope(q, cos_q, sin_q).transpose(1, 2)
|
| 838 |
+
|
| 839 |
+
scores = self.scorer(q, compressed_kv, hidden_states) # [B, S, T]
|
| 840 |
+
n_entries = compressed_kv.shape[1]
|
| 841 |
+
k = min(self.index_topk, n_entries)
|
| 842 |
+
|
| 843 |
+
# a query can only see entries whose window has fully closed before it; the
|
| 844 |
+
# `topk` is taken after masking future entries to -inf, and any pick that
|
| 845 |
+
# still points past the ready frontier (too few blocks yet) is flagged -1.
|
| 846 |
+
if n_entries > 0:
|
| 847 |
+
ready = (position_ids + 1) // self.compress_rate # [B, S]
|
| 848 |
+
entry_pos = torch.arange(n_entries, device=scores.device)
|
| 849 |
+
future = entry_pos.view(1, 1, -1) >= ready.unsqueeze(-1) # [B, S, T]
|
| 850 |
+
scores = scores.masked_fill(future, float("-inf"))
|
| 851 |
+
picks = scores.topk(k, dim=-1).indices # [B, S, k]
|
| 852 |
+
stale = picks >= ready.unsqueeze(-1)
|
| 853 |
+
return torch.where(stale, torch.full_like(picks, -1), picks)
|
| 854 |
+
|
| 855 |
+
return scores.topk(k, dim=-1).indices
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
class AgnesCSACompressor(nn.Module):
|
| 859 |
+
"""Compressed Sparse Attention compressor. It pools every `compress_rate_csa`
|
| 860 |
+
(m=4) source tokens into one entry and pairs the result with a Lightning
|
| 861 |
+
Indexer that keeps only the top `index_topk` entries per query before core
|
| 862 |
+
attention runs.
|
| 863 |
+
|
| 864 |
+
`kv_proj` / `gate_proj` / `position_bias` all emit `2 * head_dim` features,
|
| 865 |
+
holding two series per token: Ca in `[..., :head_dim]` (contributes to the
|
| 866 |
+
*next* window's entry) and Cb in `[..., head_dim:]` (contributes to the
|
| 867 |
+
*current* one). Entry `w` softmax-blends window `w-1`'s Ca with window `w`'s
|
| 868 |
+
Cb across `2 * compress_rate_csa` slots — width `2 * compress_rate_csa`,
|
| 869 |
+
stride `compress_rate_csa`. Window 0 needs the previous forward's last Ca
|
| 870 |
+
slice, which the cache returns via `overlap_kv`; with no cache (or on the
|
| 871 |
+
first call) that half is left as zero-kv / `-inf`-gate so it contributes
|
| 872 |
+
nothing to the softmax.
|
| 873 |
+
"""
|
| 874 |
+
|
| 875 |
+
rope_layer_type = "compress"
|
| 876 |
+
|
| 877 |
+
def __init__(self, config: AgnesConfig):
|
| 878 |
+
super().__init__()
|
| 879 |
+
self.compress_rate = config.compress_rates["agnes_sparse_attention"]
|
| 880 |
+
self.head_dim = config.head_dim
|
| 881 |
+
self.kv_proj = nn.Linear(config.hidden_size, 2 * self.head_dim, bias=False)
|
| 882 |
+
self.gate_proj = nn.Linear(config.hidden_size, 2 * self.head_dim, bias=False)
|
| 883 |
+
self.position_bias = nn.Parameter(torch.empty(self.compress_rate, 2 * self.head_dim))
|
| 884 |
+
self.kv_norm = AgnesRMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 885 |
+
self.rotary_emb = AgnesRotaryEmbedding(config)
|
| 886 |
+
self.indexer = AgnesIndexer(config)
|
| 887 |
+
|
| 888 |
+
def forward(
|
| 889 |
+
self,
|
| 890 |
+
hidden_states: torch.Tensor,
|
| 891 |
+
q_residual: torch.Tensor,
|
| 892 |
+
position_ids: torch.Tensor,
|
| 893 |
+
past_key_values: Cache | None,
|
| 894 |
+
layer_idx: int,
|
| 895 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 896 |
+
batch, seq_len, _ = hidden_states.shape
|
| 897 |
+
cache_layer: AgnesCSACache = past_key_values.layers[layer_idx] if past_key_values is not None else None
|
| 898 |
+
kv = self.kv_proj(hidden_states)
|
| 899 |
+
gate = self.gate_proj(hidden_states)
|
| 900 |
+
|
| 901 |
+
if cache_layer is None:
|
| 902 |
+
aligned = (kv.shape[1] // self.compress_rate) * self.compress_rate
|
| 903 |
+
chunk_kv, chunk_gate, first_window_position = kv[:, :aligned], gate[:, :aligned], 0
|
| 904 |
+
else:
|
| 905 |
+
chunk_kv, chunk_gate, first_window_position = cache_layer.store_compression_weights("compressor", kv, gate)
|
| 906 |
+
|
| 907 |
+
if chunk_kv.shape[1] > 0:
|
| 908 |
+
n_windows = chunk_kv.shape[1] // self.compress_rate
|
| 909 |
+
m = self.compress_rate
|
| 910 |
+
chunk_kv = chunk_kv.view(batch, n_windows, m, -1)
|
| 911 |
+
chunk_gate = chunk_gate.view(batch, n_windows, m, -1) + self.position_bias
|
| 912 |
+
|
| 913 |
+
# 2m-wide slot layout: top half = current window's Cb, bottom half =
|
| 914 |
+
# previous window's Ca. For window 0 the bottom half is either the Ca
|
| 915 |
+
# slice returned by the cache, or (no cache / first call) left as
|
| 916 |
+
# zero-kv + -inf-gate so it drops out of the softmax.
|
| 917 |
+
win_kv = chunk_kv.new_zeros((batch, n_windows, 2 * m, self.head_dim))
|
| 918 |
+
win_gate = chunk_gate.new_full((batch, n_windows, 2 * m, self.head_dim), float("-inf"))
|
| 919 |
+
win_kv[:, :, m:] = chunk_kv[..., self.head_dim :]
|
| 920 |
+
win_gate[:, :, m:] = chunk_gate[..., self.head_dim :]
|
| 921 |
+
if n_windows > 1:
|
| 922 |
+
win_kv[:, 1:, :m] = chunk_kv[:, :-1, :, : self.head_dim]
|
| 923 |
+
win_gate[:, 1:, :m] = chunk_gate[:, :-1, :, : self.head_dim]
|
| 924 |
+
if cache_layer is not None:
|
| 925 |
+
carried_kv, carried_gate = cache_layer.update_overlap_state(
|
| 926 |
+
"compressor", chunk_kv, chunk_gate, self.head_dim
|
| 927 |
+
)
|
| 928 |
+
if carried_kv is not None:
|
| 929 |
+
win_kv[:, 0, :m] = carried_kv.to(win_kv.dtype)
|
| 930 |
+
win_gate[:, 0, :m] = carried_gate.to(win_gate.dtype)
|
| 931 |
+
|
| 932 |
+
# fp32 softmax pooling: in bf16/fp16 close logits over wide windows
|
| 933 |
+
# can collapse together, so accumulate the weights in float.
|
| 934 |
+
pool_w = win_gate.softmax(dim=2, dtype=torch.float32).to(win_kv.dtype)
|
| 935 |
+
compressed = self.kv_norm((win_kv * pool_w).sum(dim=2))
|
| 936 |
+
abs_pos = torch.arange(n_windows, device=compressed.device) * self.compress_rate + first_window_position
|
| 937 |
+
abs_pos = abs_pos.unsqueeze(0).expand(batch, -1)
|
| 938 |
+
cos, sin = self.rotary_emb(compressed, position_ids=abs_pos, layer_type=self.rope_layer_type)
|
| 939 |
+
compressed = apply_agnes_rope(compressed.unsqueeze(1), cos, sin).squeeze(1)
|
| 940 |
+
else:
|
| 941 |
+
compressed = chunk_kv.new_zeros((batch, 0, self.head_dim))
|
| 942 |
+
|
| 943 |
+
if cache_layer is not None:
|
| 944 |
+
compressed = cache_layer.update_compressor_states("compressor", compressed)
|
| 945 |
+
compressed_kv = compressed.unsqueeze(1)
|
| 946 |
+
|
| 947 |
+
# ask the indexer which entries each query keeps; it returns -1 for picks
|
| 948 |
+
# that are not yet attendable. Clamp those to a scratch column, scatter
|
| 949 |
+
# zeros for the kept ones, then slice the scratch column back off so the
|
| 950 |
+
# bias is -inf everywhere except the valid picks.
|
| 951 |
+
picks = self.indexer(hidden_states, q_residual, position_ids, past_key_values, layer_idx) # [B, S, k]
|
| 952 |
+
n_entries = compressed_kv.shape[2]
|
| 953 |
+
kept = picks >= 0 # [B, S, k]
|
| 954 |
+
gather_idx = torch.where(kept, picks, torch.full_like(picks, n_entries))
|
| 955 |
+
block_bias = compressed_kv.new_full((batch, 1, seq_len, n_entries + 1), float("-inf"))
|
| 956 |
+
block_bias.scatter_(-1, gather_idx.unsqueeze(1), 0.0)
|
| 957 |
+
return compressed_kv, block_bias[..., :n_entries]
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
# ==========================================================================
|
| 962 |
+
# Core attention
|
| 963 |
+
# ==========================================================================
|
| 964 |
+
|
| 965 |
+
class AgnesGroupedLinear(nn.Linear):
|
| 966 |
+
"""Block-diagonal grouped linear used by the grouped output projection
|
| 967 |
+
The core attention's stacked output is `num_attention_heads* head_dim`-dim,
|
| 968 |
+
which is *very* large (Agnes-Flash: 32768; Agnes-Pro: 65536). A direct
|
| 969 |
+
`num_attention_heads*head_dim → hidden_size` projection would dominate the per-token cost.
|
| 970 |
+
|
| 971 |
+
Agnes sidesteps that by splitting the heads into `g` groups, projecting
|
| 972 |
+
each `num_attention_heads * head_dim/g`-dim group independently to a `d_g`-dim intermediate output
|
| 973 |
+
(with `d_g < num_attention_heads * head_dim/g`), and then mixing the resulting `g·d_g` vector to
|
| 974 |
+
`hidden_size` through a single follow-up linear (`self_attn.o_b_proj`). This
|
| 975 |
+
module owns the per-group block (`self_attn.o_a_proj`).
|
| 976 |
+
|
| 977 |
+
For Agnes-Flash (num_attention_heads=64, head_dim=512, o_groups=8, o_lora_rank=1024,
|
| 978 |
+
hidden_size=4096), g=8 groups of 4096-dim each are projected to 1024-dim, then
|
| 979 |
+
mixed to 4096-dim; for Agnes-Pro (num_attention_heads=128, head_dim=512, o_groups=16,
|
| 980 |
+
o_lora_rank=1024, hidden_size=7168), g=16 groups of 4096-dim each are projected
|
| 981 |
+
to 1024-dim, then mixed to 7168-dim.
|
| 982 |
+
"""
|
| 983 |
+
|
| 984 |
+
def __init__(self, in_features_per_group: int, out_features: int, n_groups: int, bias: bool = False):
|
| 985 |
+
super().__init__(in_features_per_group, out_features, bias=bias)
|
| 986 |
+
self.n_groups = n_groups
|
| 987 |
+
|
| 988 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 989 |
+
lead = x.shape[:-2]
|
| 990 |
+
in_dim = x.shape[-1]
|
| 991 |
+
# per-group matmul: bring the group axis to the front so a single bmm
|
| 992 |
+
# applies each group's block independently.
|
| 993 |
+
blocks = self.weight.view(self.n_groups, -1, in_dim).transpose(1, 2)
|
| 994 |
+
grouped = x.reshape(-1, self.n_groups, in_dim).transpose(0, 1)
|
| 995 |
+
out = torch.bmm(grouped, blocks).transpose(0, 1)
|
| 996 |
+
return out.reshape(*lead, self.n_groups, -1)
|
| 997 |
+
|
| 998 |
+
|
| 999 |
+
def _broadcast_kv_heads(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
|
| 1000 |
+
"""Broadcast the single shared KV head out to `n_rep` query heads. Same
|
| 1001 |
+
result as `torch.repeat_interleave(x, n_rep, dim=1)`, taking
|
| 1002 |
+
`(B, n_kv, S, D)` to `(B, n_kv * n_rep, S, D)`, but expressed as an
|
| 1003 |
+
`expand` + `reshape` to avoid materialising the intermediate copy.
|
| 1004 |
+
"""
|
| 1005 |
+
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
|
| 1006 |
+
if n_rep == 1:
|
| 1007 |
+
return hidden_states
|
| 1008 |
+
hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
|
| 1009 |
+
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
def _eager_attention(
|
| 1013 |
+
module: nn.Module,
|
| 1014 |
+
query: torch.Tensor,
|
| 1015 |
+
key: torch.Tensor,
|
| 1016 |
+
value: torch.Tensor,
|
| 1017 |
+
attention_mask: torch.Tensor | None,
|
| 1018 |
+
scaling: float,
|
| 1019 |
+
dropout: float | int = 0.0,
|
| 1020 |
+
**kwargs,
|
| 1021 |
+
):
|
| 1022 |
+
# Shared-KV MQA: broadcast the one stored KV head across all query heads.
|
| 1023 |
+
key_states = _broadcast_kv_heads(key, module.num_key_value_groups)
|
| 1024 |
+
value_states = _broadcast_kv_heads(value, module.num_key_value_groups)
|
| 1025 |
+
logits = torch.matmul(query, key_states.transpose(2, 3)) * scaling
|
| 1026 |
+
if attention_mask is not None:
|
| 1027 |
+
logits = logits + attention_mask
|
| 1028 |
+
|
| 1029 |
+
# Append one learned per-head sink logit as an extra "always-visible" column,
|
| 1030 |
+
# then take the softmax over [keys | sink] and read back only the key
|
| 1031 |
+
# columns. The shift by the row max is a plain numerical-stability
|
| 1032 |
+
# subtraction (matters in bf16/fp16, invariant of the softmax).
|
| 1033 |
+
sink = module.sinks.reshape(1, -1, 1, 1).expand(query.shape[0], -1, query.shape[-2], -1)
|
| 1034 |
+
logits = torch.cat([logits, sink], dim=-1)
|
| 1035 |
+
logits = logits - logits.amax(dim=-1, keepdim=True)
|
| 1036 |
+
probs = F.softmax(logits, dim=-1, dtype=logits.dtype)[..., :-1]
|
| 1037 |
+
probs = nn.functional.dropout(probs, p=dropout, training=module.training).to(value_states.dtype)
|
| 1038 |
+
attn_output = torch.matmul(probs, value_states).transpose(1, 2).contiguous()
|
| 1039 |
+
return attn_output, probs
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
+
_COMPRESSOR_BY_LAYER_TYPE = {
|
| 1043 |
+
"agnes_local_attention": None,
|
| 1044 |
+
"agnes_sparse_attention": AgnesCSACompressor,
|
| 1045 |
+
"agnes_pooled_attention": AgnesHCACompressor,
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
|
| 1049 |
+
class AgnesAttention(nn.Module):
|
| 1050 |
+
r"""
|
| 1051 |
+
Agnes attention block. It departs from a textbook multi-head block in five
|
| 1052 |
+
ways:
|
| 1053 |
+
|
| 1054 |
+
* Shared-KV multi-query attention: `num_key_value_heads = 1`; `kv_proj`
|
| 1055 |
+
emits that single KV head and it is read as both key and value.
|
| 1056 |
+
* Partial, interleaved RoPE over the leading `rope_head_dim` of each head.
|
| 1057 |
+
The conjugate rotation (position `-i`) is re-applied to the output rope
|
| 1058 |
+
slice so each entry's contribution depends only on query/key relative
|
| 1059 |
+
distance.
|
| 1060 |
+
* A learned per-head attention-sink logit that absorbs probability mass
|
| 1061 |
+
away from the real keys.
|
| 1062 |
+
* A grouped low-rank output projection, to keep the wide stacked-head
|
| 1063 |
+
output affordable.
|
| 1064 |
+
* Three interchangeable cache regimes: plain sliding window, sliding+CSA,
|
| 1065 |
+
and sliding+HCA.
|
| 1066 |
+
"""
|
| 1067 |
+
|
| 1068 |
+
def __init__(self, config: AgnesConfig, layer_idx: int):
|
| 1069 |
+
super().__init__()
|
| 1070 |
+
self.config = config
|
| 1071 |
+
self.layer_idx = layer_idx
|
| 1072 |
+
self.layer_type = config.layer_types[layer_idx]
|
| 1073 |
+
# Sliding-only layers use the "main" (plain θ=10000) rope; CSA/HCA layers
|
| 1074 |
+
# share the same yarn-scaled "compress" rope as their compressor.
|
| 1075 |
+
self.rope_layer_type = "main" if self.layer_type == "agnes_local_attention" else "compress"
|
| 1076 |
+
self.num_heads = config.num_attention_heads
|
| 1077 |
+
self.num_key_value_groups = config.num_attention_heads # single KV head, broadcast to all
|
| 1078 |
+
self.head_dim = config.head_dim
|
| 1079 |
+
self.sliding_window = config.sliding_window
|
| 1080 |
+
self.attention_dropout = config.attention_dropout
|
| 1081 |
+
self.is_causal = True
|
| 1082 |
+
self.scaling = self.head_dim**-0.5
|
| 1083 |
+
|
| 1084 |
+
self.q_a_proj = nn.Linear(config.hidden_size, config.q_lora_rank, bias=False)
|
| 1085 |
+
self.q_a_norm = AgnesRMSNorm(config.q_lora_rank, eps=config.rms_norm_eps)
|
| 1086 |
+
self.q_b_proj = nn.Linear(config.q_lora_rank, self.num_heads * self.head_dim, bias=False)
|
| 1087 |
+
self.q_b_norm = AgnesUnweightedRMSNorm(eps=config.rms_norm_eps)
|
| 1088 |
+
self.kv_proj = nn.Linear(config.hidden_size, self.head_dim, bias=False)
|
| 1089 |
+
self.kv_norm = AgnesRMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
| 1090 |
+
self.o_a_proj = AgnesGroupedLinear(
|
| 1091 |
+
self.num_heads * self.head_dim // config.o_groups, config.o_groups * config.o_lora_rank, config.o_groups
|
| 1092 |
+
)
|
| 1093 |
+
self.o_b_proj = nn.Linear(config.o_groups * config.o_lora_rank, config.hidden_size, bias=False)
|
| 1094 |
+
self.sinks = nn.Parameter(torch.empty(self.num_heads))
|
| 1095 |
+
self.compressor = (
|
| 1096 |
+
_COMPRESSOR_BY_LAYER_TYPE[self.layer_type](config) if self.layer_type != "agnes_local_attention" else None
|
| 1097 |
+
)
|
| 1098 |
+
|
| 1099 |
+
def forward(
|
| 1100 |
+
self,
|
| 1101 |
+
hidden_states: torch.Tensor,
|
| 1102 |
+
position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]] | tuple[torch.Tensor, torch.Tensor],
|
| 1103 |
+
position_ids: torch.Tensor,
|
| 1104 |
+
attention_mask: torch.Tensor | None,
|
| 1105 |
+
past_key_values: Cache | None = None,
|
| 1106 |
+
**kwargs: Unpack[FlashAttentionKwargs],
|
| 1107 |
+
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
| 1108 |
+
lead = hidden_states.shape[:-1]
|
| 1109 |
+
head_shape = (*lead, -1, self.head_dim)
|
| 1110 |
+
# the model hands down a {"main", "compress"} cos/sin dict; this layer
|
| 1111 |
+
# takes whichever entry matches its rope label (local -> main, else compress).
|
| 1112 |
+
cos, sin = position_embeddings[self.rope_layer_type]
|
| 1113 |
+
|
| 1114 |
+
# low-rank query path: down-proj + norm, then up-proj into heads, a second
|
| 1115 |
+
# (unweighted) norm, and finally rotary.
|
| 1116 |
+
q_lora = self.q_a_norm(self.q_a_proj(hidden_states))
|
| 1117 |
+
q = self.q_b_norm(self.q_b_proj(q_lora).view(*head_shape).transpose(1, 2))
|
| 1118 |
+
q = apply_agnes_rope(q, cos, sin)
|
| 1119 |
+
|
| 1120 |
+
kv = self.kv_norm(self.kv_proj(hidden_states)).view(*head_shape).transpose(1, 2)
|
| 1121 |
+
kv = apply_agnes_rope(kv, cos, sin)
|
| 1122 |
+
|
| 1123 |
+
if past_key_values is not None: # shared-KV sliding window (K is V)
|
| 1124 |
+
kv = past_key_values.update(kv, kv, self.layer_idx)[0]
|
| 1125 |
+
|
| 1126 |
+
block_bias = None
|
| 1127 |
+
if self.compressor is not None: # long-range CSA / HCA entries
|
| 1128 |
+
compressed_kv, block_bias = self.compressor(
|
| 1129 |
+
hidden_states, q_lora, position_ids, past_key_values, self.layer_idx
|
| 1130 |
+
)
|
| 1131 |
+
kv = torch.cat([kv, compressed_kv], dim=2)
|
| 1132 |
+
|
| 1133 |
+
# The compressor path concatenates extra entries onto the KV axis after the
|
| 1134 |
+
# standard sliding-window cache update, so a tensor `attention_mask` (built
|
| 1135 |
+
# for the pre-concat KV length) needs to be extended to cover them. The
|
| 1136 |
+
# compressor returns a `block_bias` carrying per-query causality + indexer
|
| 1137 |
+
# validity over those new slots — cat it in instead of zero-padding (which
|
| 1138 |
+
# would let every query see every compressed slot).
|
| 1139 |
+
if isinstance(attention_mask, torch.Tensor) and kv.shape[2] > attention_mask.shape[-1]:
|
| 1140 |
+
if block_bias is not None:
|
| 1141 |
+
attention_mask = torch.cat([attention_mask, block_bias.to(attention_mask.dtype)], dim=-1)
|
| 1142 |
+
else:
|
| 1143 |
+
attention_mask = F.pad(attention_mask, (0, kv.shape[2] - attention_mask.shape[-1]), value=0.0)
|
| 1144 |
+
|
| 1145 |
+
attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface(
|
| 1146 |
+
self.config._attn_implementation, _eager_attention
|
| 1147 |
+
)
|
| 1148 |
+
attn_output, attn_weights = attention_interface(
|
| 1149 |
+
self,
|
| 1150 |
+
q,
|
| 1151 |
+
kv,
|
| 1152 |
+
kv,
|
| 1153 |
+
attention_mask,
|
| 1154 |
+
dropout=0.0 if not self.training else self.attention_dropout,
|
| 1155 |
+
scaling=self.scaling,
|
| 1156 |
+
sliding_window=self.sliding_window,
|
| 1157 |
+
s_aux=self.sinks,
|
| 1158 |
+
**kwargs,
|
| 1159 |
+
)
|
| 1160 |
+
|
| 1161 |
+
# Because V shares K's storage it also carries K's rotary; undo it on the
|
| 1162 |
+
# output's rope slice with the conjugate rotation (`-sin`) at the query
|
| 1163 |
+
# position, so what reaches the output projection is rope-free. The
|
| 1164 |
+
# transpose pair only re-lays [B, H, S, D] as the [B, S, H, D] that
|
| 1165 |
+
# apply_agnes_rope expects.
|
| 1166 |
+
attn_output = apply_agnes_rope(attn_output.transpose(1, 2), cos, -sin).transpose(1, 2)
|
| 1167 |
+
|
| 1168 |
+
# grouped low-rank output projection: split heads into o_groups, project
|
| 1169 |
+
# each group down, concatenate, then mix up to hidden_size.
|
| 1170 |
+
heads = attn_output.reshape(*lead, self.config.o_groups, -1)
|
| 1171 |
+
mixed = self.o_a_proj(heads).flatten(2)
|
| 1172 |
+
return self.o_b_proj(mixed), attn_weights
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
|
| 1176 |
+
# ==========================================================================
|
| 1177 |
+
# Decoder block
|
| 1178 |
+
# ==========================================================================
|
| 1179 |
+
|
| 1180 |
+
class AgnesDecoderLayer(GradientCheckpointingLayer):
|
| 1181 |
+
r"""Agnes decoder block. Unlike a textbook residual block, the residual
|
| 1182 |
+
here is not a single tensor but a stack of `hc_mult` parallel streams kept
|
| 1183 |
+
in shape `[B, S, hc_mult, D]` for the whole block. Two
|
| 1184 |
+
:class:`AgnesHyperConnection` modules (one at the attention site, one at the
|
| 1185 |
+
MLP site) collapse those streams into the sublayer input and re-expand the
|
| 1186 |
+
sublayer output back across them. Their stream-mixing matrix is projected
|
| 1187 |
+
onto the doubly-stochastic manifold by Sinkhorn-Knopp iteration, which keeps
|
| 1188 |
+
the residual transform non-expansive across a deep stack.
|
| 1189 |
+
"""
|
| 1190 |
+
|
| 1191 |
+
def __init__(self, config: AgnesConfig, layer_idx: int):
|
| 1192 |
+
super().__init__()
|
| 1193 |
+
self.layer_idx = layer_idx
|
| 1194 |
+
self.self_attn = AgnesAttention(config, layer_idx)
|
| 1195 |
+
self.mlp = AgnesSparseMoeBlock(config, layer_idx)
|
| 1196 |
+
self.input_layernorm = AgnesRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 1197 |
+
self.post_attention_layernorm = AgnesRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 1198 |
+
self.attn_hc = AgnesHyperConnection(config)
|
| 1199 |
+
self.ffn_hc = AgnesHyperConnection(config)
|
| 1200 |
+
|
| 1201 |
+
def forward(
|
| 1202 |
+
self,
|
| 1203 |
+
hidden_states: torch.Tensor,
|
| 1204 |
+
input_ids: torch.Tensor | None = None,
|
| 1205 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 1206 |
+
) -> torch.Tensor:
|
| 1207 |
+
# The stream stack stays [B, S, hc_mult, hidden] throughout. Each site
|
| 1208 |
+
# runs the same recipe: collapse streams -> sublayer -> place the output
|
| 1209 |
+
# back across streams (`post`) and carry the old streams through the
|
| 1210 |
+
# doubly-stochastic mixer (`comb`). `post`/`comb` arrive in fp32 (Sinkhorn
|
| 1211 |
+
# runs in float) and are cast down before mixing. `comb` is used
|
| 1212 |
+
# transposed — sum over its FIRST axis, i.e. comb.T @ streams — because
|
| 1213 |
+
# the Sinkhorn matrix is doubly-stochastic but not symmetric.
|
| 1214 |
+
dtype = hidden_states.dtype
|
| 1215 |
+
|
| 1216 |
+
post, comb, collapsed = self.attn_hc(hidden_states)
|
| 1217 |
+
attn_out, _ = self.self_attn(self.input_layernorm(collapsed), **kwargs)
|
| 1218 |
+
placed = post.to(dtype).unsqueeze(-1) * attn_out.unsqueeze(-2)
|
| 1219 |
+
carried = torch.matmul(comb.to(dtype).transpose(-1, -2), hidden_states)
|
| 1220 |
+
hidden_states = placed + carried
|
| 1221 |
+
|
| 1222 |
+
post, comb, collapsed = self.ffn_hc(hidden_states)
|
| 1223 |
+
mlp_out = self.mlp(self.post_attention_layernorm(collapsed), input_ids=input_ids)
|
| 1224 |
+
placed = post.to(dtype).unsqueeze(-1) * mlp_out.unsqueeze(-2)
|
| 1225 |
+
carried = torch.matmul(comb.to(dtype).transpose(-1, -2), hidden_states)
|
| 1226 |
+
return placed + carried
|
| 1227 |
+
|
| 1228 |
+
|
| 1229 |
+
|
| 1230 |
+
# ==========================================================================
|
| 1231 |
+
# Auxiliary load-balancing loss
|
| 1232 |
+
# ==========================================================================
|
| 1233 |
+
|
| 1234 |
+
def load_balancing_loss_func(
|
| 1235 |
+
gate_logits: torch.Tensor | tuple[torch.Tensor] | None,
|
| 1236 |
+
num_experts: int | None = None,
|
| 1237 |
+
top_k=2,
|
| 1238 |
+
attention_mask: torch.Tensor | None = None,
|
| 1239 |
+
) -> torch.Tensor | int:
|
| 1240 |
+
r"""
|
| 1241 |
+
Computes auxiliary load balancing loss as in Switch Transformer - implemented in Pytorch.
|
| 1242 |
+
|
| 1243 |
+
See Switch Transformer (https://huggingface.co/papers/2101.03961) for more details. This function implements the loss
|
| 1244 |
+
function presented in equations (4) - (6) of the paper. It aims at penalizing cases where the routing between
|
| 1245 |
+
experts is too unbalanced.
|
| 1246 |
+
|
| 1247 |
+
Args:
|
| 1248 |
+
gate_logits:
|
| 1249 |
+
Logits from the `gate`, should be a tuple of model.config.num_hidden_layers tensors of
|
| 1250 |
+
shape [batch_size X sequence_length, num_experts].
|
| 1251 |
+
num_experts:
|
| 1252 |
+
Number of experts
|
| 1253 |
+
top_k:
|
| 1254 |
+
The number of experts to route per-token, can be also interpreted as the `top-k` routing
|
| 1255 |
+
parameter.
|
| 1256 |
+
attention_mask (`torch.Tensor`, *optional*):
|
| 1257 |
+
The attention_mask used in forward function
|
| 1258 |
+
shape [batch_size X sequence_length] if not None.
|
| 1259 |
+
|
| 1260 |
+
Returns:
|
| 1261 |
+
The auxiliary loss.
|
| 1262 |
+
"""
|
| 1263 |
+
if gate_logits is None or not isinstance(gate_logits, tuple):
|
| 1264 |
+
return 0
|
| 1265 |
+
|
| 1266 |
+
if isinstance(gate_logits, tuple):
|
| 1267 |
+
compute_device = gate_logits[0].device
|
| 1268 |
+
concatenated_gate_logits = torch.cat([layer_gate.to(compute_device) for layer_gate in gate_logits], dim=0)
|
| 1269 |
+
|
| 1270 |
+
routing_weights = torch.nn.functional.softmax(concatenated_gate_logits, dim=-1)
|
| 1271 |
+
|
| 1272 |
+
_, selected_experts = torch.topk(routing_weights, top_k, dim=-1)
|
| 1273 |
+
|
| 1274 |
+
expert_mask = torch.nn.functional.one_hot(selected_experts, num_experts)
|
| 1275 |
+
|
| 1276 |
+
if attention_mask is None:
|
| 1277 |
+
# Compute the percentage of tokens routed to each experts
|
| 1278 |
+
tokens_per_expert = torch.mean(expert_mask.float(), dim=0)
|
| 1279 |
+
|
| 1280 |
+
# Compute the average probability of routing to these experts
|
| 1281 |
+
router_prob_per_expert = torch.mean(routing_weights, dim=0)
|
| 1282 |
+
else:
|
| 1283 |
+
batch_size, sequence_length = attention_mask.shape
|
| 1284 |
+
num_hidden_layers = concatenated_gate_logits.shape[0] // (batch_size * sequence_length)
|
| 1285 |
+
|
| 1286 |
+
# Compute the mask that masks all padding tokens as 0 with the same shape of expert_mask
|
| 1287 |
+
expert_attention_mask = (
|
| 1288 |
+
attention_mask[None, :, :, None, None]
|
| 1289 |
+
.expand((num_hidden_layers, batch_size, sequence_length, top_k, num_experts))
|
| 1290 |
+
.reshape(-1, top_k, num_experts)
|
| 1291 |
+
.to(compute_device)
|
| 1292 |
+
)
|
| 1293 |
+
|
| 1294 |
+
# Compute the percentage of tokens routed to each experts
|
| 1295 |
+
tokens_per_expert = torch.sum(expert_mask.float() * expert_attention_mask, dim=0) / torch.sum(
|
| 1296 |
+
expert_attention_mask, dim=0
|
| 1297 |
+
)
|
| 1298 |
+
|
| 1299 |
+
# Compute the mask that masks all padding tokens as 0 with the same shape of tokens_per_expert
|
| 1300 |
+
router_per_expert_attention_mask = (
|
| 1301 |
+
attention_mask[None, :, :, None]
|
| 1302 |
+
.expand((num_hidden_layers, batch_size, sequence_length, num_experts))
|
| 1303 |
+
.reshape(-1, num_experts)
|
| 1304 |
+
.to(compute_device)
|
| 1305 |
+
)
|
| 1306 |
+
|
| 1307 |
+
# Compute the average probability of routing to these experts
|
| 1308 |
+
router_prob_per_expert = torch.sum(routing_weights * router_per_expert_attention_mask, dim=0) / torch.sum(
|
| 1309 |
+
router_per_expert_attention_mask, dim=0
|
| 1310 |
+
)
|
| 1311 |
+
|
| 1312 |
+
overall_loss = torch.sum(tokens_per_expert * router_prob_per_expert.unsqueeze(0))
|
| 1313 |
+
return overall_loss * num_experts
|
| 1314 |
+
|
| 1315 |
+
|
| 1316 |
+
|
| 1317 |
+
# ==========================================================================
|
| 1318 |
+
# Pretrained base and full models
|
| 1319 |
+
# ==========================================================================
|
| 1320 |
+
|
| 1321 |
+
@auto_docstring
|
| 1322 |
+
class AgnesPreTrainedModel(PreTrainedModel):
|
| 1323 |
+
config: AgnesConfig
|
| 1324 |
+
base_model_prefix = "model"
|
| 1325 |
+
supports_gradient_checkpointing = True
|
| 1326 |
+
_no_split_modules = ["AgnesDecoderLayer"]
|
| 1327 |
+
_skip_keys_device_placement = ["past_key_values"]
|
| 1328 |
+
# Agnes runs the eager attention path only; the optimized backends are all
|
| 1329 |
+
# unavailable for a concrete structural reason:
|
| 1330 |
+
#
|
| 1331 |
+
# * FlashAttention 2 / 3 / 4 cap the supported head dim at 256, but Agnes
|
| 1332 |
+
# uses `head_dim=512`, so those kernels raise
|
| 1333 |
+
# `FlashAttention forward only supports head dimension at most 256`.
|
| 1334 |
+
# * SDPA: the fused torch SDPA kernel has no slot for the per-head learned
|
| 1335 |
+
# attention-sink logit, which is part of Agnes's softmax.
|
| 1336 |
+
# * FlexAttention: the compressor concatenates a *variable* number of
|
| 1337 |
+
# entries onto the KV axis inside the block, after the model-level mask
|
| 1338 |
+
# was already built, so the KV length no longer matches the BlockMask's
|
| 1339 |
+
# `kv_len`. BlockMask can't be resized at runtime, and the compressor
|
| 1340 |
+
# already carries its own causal bookkeeping, so wiring that into a
|
| 1341 |
+
# `mask_mod` is not worthwhile.
|
| 1342 |
+
_supports_flash_attn = False
|
| 1343 |
+
_supports_sdpa = False
|
| 1344 |
+
_supports_flex_attn = False
|
| 1345 |
+
# The compressor's rolling-window buffer / compressed-entries / overlap state
|
| 1346 |
+
# lives on the per-layer cache (:class:`AgnesHCACache` /
|
| 1347 |
+
# :class:`AgnesCSACache`) and isn't compatible with :class:`StaticCache`
|
| 1348 |
+
# — that path would hand the compressor a :class:`StaticSlidingWindowLayer`
|
| 1349 |
+
# with no `store_compression_weights` method. Disabling fullgraph compile
|
| 1350 |
+
# keeps generation tests on the dynamic cache build that does dispatch to
|
| 1351 |
+
# Agnes's own cache layers.
|
| 1352 |
+
_can_compile_fullgraph = False
|
| 1353 |
+
_supports_attention_backend = True
|
| 1354 |
+
_can_record_outputs = {
|
| 1355 |
+
"router_logits": OutputRecorder(AgnesTopKRouter, index=0),
|
| 1356 |
+
"hidden_states": AgnesDecoderLayer,
|
| 1357 |
+
"attentions": AgnesAttention,
|
| 1358 |
+
}
|
| 1359 |
+
config_class = AgnesConfig
|
| 1360 |
+
_keep_in_fp32_modules_strict = [
|
| 1361 |
+
"attn_hc",
|
| 1362 |
+
"ffn_hc",
|
| 1363 |
+
"hc_head",
|
| 1364 |
+
"sinks",
|
| 1365 |
+
"position_bias",
|
| 1366 |
+
"e_score_correction_bias",
|
| 1367 |
+
"q_a_norm",
|
| 1368 |
+
"kv_norm",
|
| 1369 |
+
"input_layernorm",
|
| 1370 |
+
"post_attention_layernorm",
|
| 1371 |
+
"norm",
|
| 1372 |
+
]
|
| 1373 |
+
# Agnes-Flash checkpoints mix FP8 and BF16 in the attention compressor /
|
| 1374 |
+
# indexer branch: these projections ship in BF16 with no companion `scale_inv`.
|
| 1375 |
+
# Listed here (non-strict) so the FP8 quantizer's `get_modules_to_not_convert`
|
| 1376 |
+
# auto-skips them; non-strict has no dtype effect at BF16, so they stay BF16.
|
| 1377 |
+
_keep_in_fp32_modules = [
|
| 1378 |
+
"self_attn.compressor.kv_proj",
|
| 1379 |
+
"self_attn.compressor.gate_proj",
|
| 1380 |
+
"self_attn.compressor.indexer.kv_proj",
|
| 1381 |
+
"self_attn.compressor.indexer.gate_proj",
|
| 1382 |
+
"self_attn.compressor.indexer.scorer.weights_proj",
|
| 1383 |
+
]
|
| 1384 |
+
_keys_to_ignore_on_load_unexpected = [r"(^|\.)mtp\..*"]
|
| 1385 |
+
# ``_is_stateful`` opts out of generation modes that need to roll the cache
|
| 1386 |
+
# back across drafts (assisted generation, prompt lookup, contrastive search).
|
| 1387 |
+
# The compressor's running-window state isn't rewindable, so `generate`
|
| 1388 |
+
# raises a clear error early instead of failing deep in the compressor with
|
| 1389 |
+
# a missing-method `AttributeError`.
|
| 1390 |
+
_is_stateful = True
|
| 1391 |
+
|
| 1392 |
+
@torch.no_grad()
|
| 1393 |
+
def _init_weights(self, module):
|
| 1394 |
+
super()._init_weights(module)
|
| 1395 |
+
std = self.config.initializer_range
|
| 1396 |
+
if isinstance(module, (AgnesTopKRouter, AgnesHashRouter)):
|
| 1397 |
+
init.normal_(module.weight, mean=0.0, std=std)
|
| 1398 |
+
if isinstance(module, AgnesTopKRouter):
|
| 1399 |
+
init.zeros_(module.e_score_correction_bias) # buffer
|
| 1400 |
+
if isinstance(module, AgnesHashRouter):
|
| 1401 |
+
init.zeros_(module.tid2eid) # buffer; real values come from the checkpoint
|
| 1402 |
+
elif isinstance(module, AgnesExperts):
|
| 1403 |
+
init.normal_(module.gate_up_proj, mean=0.0, std=std)
|
| 1404 |
+
init.normal_(module.down_proj, mean=0.0, std=std)
|
| 1405 |
+
elif isinstance(module, AgnesAttention):
|
| 1406 |
+
init.zeros_(module.sinks)
|
| 1407 |
+
elif isinstance(module, AgnesHyperConnection):
|
| 1408 |
+
init.normal_(module.fn, mean=0.0, std=std)
|
| 1409 |
+
init.zeros_(module.base)
|
| 1410 |
+
init.ones_(module.scale)
|
| 1411 |
+
elif isinstance(module, AgnesHyperHead):
|
| 1412 |
+
init.normal_(module.hc_fn, mean=0.0, std=std)
|
| 1413 |
+
init.zeros_(module.hc_base)
|
| 1414 |
+
init.ones_(module.hc_scale)
|
| 1415 |
+
elif isinstance(module, (AgnesHCACompressor, AgnesCSACompressor, AgnesIndexer)):
|
| 1416 |
+
init.zeros_(module.position_bias)
|
| 1417 |
+
elif isinstance(module, AgnesRotaryEmbedding):
|
| 1418 |
+
for layer_type in module.layer_types:
|
| 1419 |
+
rope_init_fn = module.compute_default_rope_parameters
|
| 1420 |
+
if module.rope_type[layer_type] != "default":
|
| 1421 |
+
rope_init_fn = ROPE_INIT_FUNCTIONS[module.rope_type[layer_type]]
|
| 1422 |
+
curr_inv_freq, _ = rope_init_fn(module.config, layer_type=layer_type)
|
| 1423 |
+
init.copy_(getattr(module, f"{layer_type}_inv_freq"), curr_inv_freq)
|
| 1424 |
+
init.copy_(getattr(module, f"{layer_type}_original_inv_freq"), curr_inv_freq)
|
| 1425 |
+
|
| 1426 |
+
|
| 1427 |
+
@auto_docstring
|
| 1428 |
+
class AgnesModel(AgnesPreTrainedModel):
|
| 1429 |
+
def __init__(self, config: AgnesConfig):
|
| 1430 |
+
super().__init__(config)
|
| 1431 |
+
self.padding_idx = config.pad_token_id
|
| 1432 |
+
self.vocab_size = config.vocab_size
|
| 1433 |
+
|
| 1434 |
+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
|
| 1435 |
+
self.layers = nn.ModuleList(
|
| 1436 |
+
[AgnesDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
|
| 1437 |
+
)
|
| 1438 |
+
self.norm = AgnesRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 1439 |
+
self.rotary_emb = AgnesRotaryEmbedding(config)
|
| 1440 |
+
self.gradient_checkpointing = False
|
| 1441 |
+
self.hc_head = AgnesHyperHead(config)
|
| 1442 |
+
|
| 1443 |
+
# Initialize weights and apply final processing
|
| 1444 |
+
self.post_init()
|
| 1445 |
+
|
| 1446 |
+
@merge_with_config_defaults
|
| 1447 |
+
@capture_outputs
|
| 1448 |
+
@auto_docstring
|
| 1449 |
+
def forward(
|
| 1450 |
+
self,
|
| 1451 |
+
input_ids: torch.LongTensor | None = None,
|
| 1452 |
+
attention_mask: torch.Tensor | None = None,
|
| 1453 |
+
position_ids: torch.LongTensor | None = None,
|
| 1454 |
+
past_key_values: Cache | None = None,
|
| 1455 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 1456 |
+
use_cache: bool | None = None,
|
| 1457 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 1458 |
+
) -> MoeModelOutputWithPast:
|
| 1459 |
+
if (input_ids is None) ^ (inputs_embeds is not None):
|
| 1460 |
+
raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
|
| 1461 |
+
# what we hand back is the caller's own cache (or None); a cache we spin
|
| 1462 |
+
# up internally is used for this pass only, never returned.
|
| 1463 |
+
return_cache = past_key_values if use_cache else None
|
| 1464 |
+
if past_key_values is None:
|
| 1465 |
+
past_key_values = DynamicCache(config=self.config)
|
| 1466 |
+
if inputs_embeds is None:
|
| 1467 |
+
inputs_embeds = self.embed_tokens(input_ids)
|
| 1468 |
+
if position_ids is None:
|
| 1469 |
+
start = past_key_values.get_seq_length()
|
| 1470 |
+
position_ids = (torch.arange(inputs_embeds.shape[1], device=inputs_embeds.device) + start).unsqueeze(0)
|
| 1471 |
+
|
| 1472 |
+
# generate() can pass an already-built per-layer-type mask dict; every
|
| 1473 |
+
# Agnes layer shares one sliding-window mask, so reuse it if present.
|
| 1474 |
+
if isinstance(attention_mask, dict):
|
| 1475 |
+
causal_mask = next(iter(attention_mask.values()))
|
| 1476 |
+
else:
|
| 1477 |
+
causal_mask = create_sliding_window_causal_mask(
|
| 1478 |
+
config=self.config,
|
| 1479 |
+
inputs_embeds=inputs_embeds,
|
| 1480 |
+
attention_mask=attention_mask,
|
| 1481 |
+
past_key_values=past_key_values,
|
| 1482 |
+
position_ids=position_ids,
|
| 1483 |
+
)
|
| 1484 |
+
|
| 1485 |
+
# fan the embeddings into hc_mult parallel residual streams and precompute
|
| 1486 |
+
# both rope flavours once for the whole stack.
|
| 1487 |
+
hidden_states = inputs_embeds.unsqueeze(2).expand(-1, -1, self.config.hc_mult, -1).contiguous()
|
| 1488 |
+
position_embeddings = {
|
| 1489 |
+
"main": self.rotary_emb(inputs_embeds, position_ids=position_ids, layer_type="main"),
|
| 1490 |
+
"compress": self.rotary_emb(inputs_embeds, position_ids=position_ids, layer_type="compress"),
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
for layer in self.layers:
|
| 1494 |
+
hidden_states = layer(
|
| 1495 |
+
hidden_states,
|
| 1496 |
+
position_embeddings=position_embeddings,
|
| 1497 |
+
position_ids=position_ids,
|
| 1498 |
+
attention_mask=causal_mask,
|
| 1499 |
+
input_ids=input_ids,
|
| 1500 |
+
past_key_values=past_key_values,
|
| 1501 |
+
**kwargs,
|
| 1502 |
+
)
|
| 1503 |
+
|
| 1504 |
+
hidden_states = self.norm(self.hc_head(hidden_states))
|
| 1505 |
+
return MoeModelOutputWithPast(last_hidden_state=hidden_states, past_key_values=return_cache)
|
| 1506 |
+
|
| 1507 |
+
|
| 1508 |
+
@auto_docstring
|
| 1509 |
+
class AgnesForCausalLM(AgnesPreTrainedModel, GenerationMixin):
|
| 1510 |
+
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
|
| 1511 |
+
_tp_plan = {"lm_head": "colwise_gather_output"}
|
| 1512 |
+
_pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
|
| 1513 |
+
|
| 1514 |
+
def __init__(self, config):
|
| 1515 |
+
super().__init__(config)
|
| 1516 |
+
self.model = AgnesModel(config)
|
| 1517 |
+
self.vocab_size = config.vocab_size
|
| 1518 |
+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
|
| 1519 |
+
self.router_aux_loss_coef = config.router_aux_loss_coef
|
| 1520 |
+
self.num_experts = config.num_local_experts
|
| 1521 |
+
self.num_experts_per_tok = config.num_experts_per_tok
|
| 1522 |
+
|
| 1523 |
+
# Initialize weights and apply final processing
|
| 1524 |
+
self.post_init()
|
| 1525 |
+
|
| 1526 |
+
@can_return_tuple
|
| 1527 |
+
@auto_docstring
|
| 1528 |
+
def forward(
|
| 1529 |
+
self,
|
| 1530 |
+
input_ids: torch.LongTensor | None = None,
|
| 1531 |
+
attention_mask: torch.Tensor | None = None,
|
| 1532 |
+
position_ids: torch.LongTensor | None = None,
|
| 1533 |
+
past_key_values: Cache | None = None,
|
| 1534 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 1535 |
+
labels: torch.LongTensor | None = None,
|
| 1536 |
+
use_cache: bool | None = None,
|
| 1537 |
+
output_router_logits: bool | None = None,
|
| 1538 |
+
logits_to_keep: int | torch.Tensor = 0,
|
| 1539 |
+
**kwargs: Unpack[TransformersKwargs],
|
| 1540 |
+
) -> MoeCausalLMOutputWithPast:
|
| 1541 |
+
r"""
|
| 1542 |
+
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
|
| 1543 |
+
Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
|
| 1544 |
+
config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
|
| 1545 |
+
(masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
|
| 1546 |
+
|
| 1547 |
+
Example:
|
| 1548 |
+
|
| 1549 |
+
```python
|
| 1550 |
+
>>> from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 1551 |
+
|
| 1552 |
+
>>> model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, trust_remote_code=True)
|
| 1553 |
+
>>> tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
|
| 1554 |
+
|
| 1555 |
+
>>> inputs = tokenizer("The capital of France is", return_tensors="pt")
|
| 1556 |
+
>>> generate_ids = model.generate(inputs.input_ids, max_length=30)
|
| 1557 |
+
>>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True)[0]
|
| 1558 |
+
```"""
|
| 1559 |
+
|
| 1560 |
+
output_router_logits = (
|
| 1561 |
+
output_router_logits if output_router_logits is not None else self.config.output_router_logits
|
| 1562 |
+
)
|
| 1563 |
+
|
| 1564 |
+
# decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn)
|
| 1565 |
+
outputs: MoeModelOutputWithPast = self.model(
|
| 1566 |
+
input_ids=input_ids,
|
| 1567 |
+
attention_mask=attention_mask,
|
| 1568 |
+
position_ids=position_ids,
|
| 1569 |
+
past_key_values=past_key_values,
|
| 1570 |
+
inputs_embeds=inputs_embeds,
|
| 1571 |
+
use_cache=use_cache,
|
| 1572 |
+
output_router_logits=output_router_logits,
|
| 1573 |
+
**kwargs,
|
| 1574 |
+
)
|
| 1575 |
+
|
| 1576 |
+
hidden_states = outputs.last_hidden_state
|
| 1577 |
+
# Only compute necessary logits, and do not upcast them to float if we are not computing the loss
|
| 1578 |
+
slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
|
| 1579 |
+
logits = self.lm_head(hidden_states[:, slice_indices, :])
|
| 1580 |
+
|
| 1581 |
+
loss = None
|
| 1582 |
+
if labels is not None:
|
| 1583 |
+
loss = self.loss_function(logits, labels, self.vocab_size, **kwargs)
|
| 1584 |
+
|
| 1585 |
+
aux_loss = None
|
| 1586 |
+
if output_router_logits:
|
| 1587 |
+
aux_loss = load_balancing_loss_func(
|
| 1588 |
+
outputs.router_logits,
|
| 1589 |
+
self.num_experts,
|
| 1590 |
+
self.num_experts_per_tok,
|
| 1591 |
+
attention_mask,
|
| 1592 |
+
)
|
| 1593 |
+
if labels is not None:
|
| 1594 |
+
loss += self.router_aux_loss_coef * aux_loss.to(loss.device) # make sure to reside in the same device
|
| 1595 |
+
|
| 1596 |
+
return MoeCausalLMOutputWithPast(
|
| 1597 |
+
loss=loss,
|
| 1598 |
+
aux_loss=aux_loss,
|
| 1599 |
+
logits=logits,
|
| 1600 |
+
past_key_values=outputs.past_key_values,
|
| 1601 |
+
hidden_states=outputs.hidden_states,
|
| 1602 |
+
attentions=outputs.attentions,
|
| 1603 |
+
router_logits=outputs.router_logits,
|
| 1604 |
+
)
|
| 1605 |
+
|
| 1606 |
+
|
| 1607 |
+
__all__ = ["AgnesPreTrainedModel", "AgnesModel", "AgnesForCausalLM"]
|
| 1608 |
+
|
| 1609 |
+
|
| 1610 |
+
|
| 1611 |
+
# ==========================================================================
|
| 1612 |
+
# Checkpoint weight conversion
|
| 1613 |
+
# ==========================================================================
|
| 1614 |
+
|
| 1615 |
+
# Agnes checkpoints ship native-format tensor names (`embed.weight`,
|
| 1616 |
+
# `layers.N.attn.*`, `layers.N.ffn.*`, flat `hc_*` hyper-connection params,
|
| 1617 |
+
# per-expert `w1`/`w2`/`w3` weights in the native gate/down/up notation). The mapping below
|
| 1618 |
+
# renames them onto this implementation's module tree and merges the
|
| 1619 |
+
# per-expert weights into the fused `gate_up_proj` / `down_proj` tensors.
|
| 1620 |
+
# Registered for `model_type="agnes"`, it is applied automatically by
|
| 1621 |
+
# `from_pretrained` and reversed by `save_pretrained`.
|
| 1622 |
+
_AGNES_CHECKPOINT_CONVERSION_MAPPING = [
|
| 1623 |
+
WeightRenaming(source_patterns=r"^embed\.weight$", target_patterns="embed_tokens.weight"),
|
| 1624 |
+
WeightRenaming(source_patterns=r"^head\.weight$", target_patterns="lm_head.weight"),
|
| 1625 |
+
WeightRenaming(source_patterns=r"^norm\.weight$", target_patterns="norm.weight"),
|
| 1626 |
+
WeightRenaming(source_patterns=r"^hc_head_fn$", target_patterns="hc_head.hc_fn"),
|
| 1627 |
+
WeightRenaming(source_patterns=r"^hc_head_base$", target_patterns="hc_head.hc_base"),
|
| 1628 |
+
WeightRenaming(source_patterns=r"^hc_head_scale$", target_patterns="hc_head.hc_scale"),
|
| 1629 |
+
WeightRenaming(
|
| 1630 |
+
source_patterns=r"^layers\.(\d+)\.attn_norm\.",
|
| 1631 |
+
target_patterns=r"layers.\1.input_layernorm.",
|
| 1632 |
+
),
|
| 1633 |
+
WeightRenaming(
|
| 1634 |
+
source_patterns=r"^layers\.(\d+)\.ffn_norm\.",
|
| 1635 |
+
target_patterns=r"layers.\1.post_attention_layernorm.",
|
| 1636 |
+
),
|
| 1637 |
+
WeightRenaming(source_patterns=r"^layers\.(\d+)\.hc_attn_fn$", target_patterns=r"layers.\1.attn_hc.fn"),
|
| 1638 |
+
WeightRenaming(
|
| 1639 |
+
source_patterns=r"^layers\.(\d+)\.hc_attn_base$", target_patterns=r"layers.\1.attn_hc.base"
|
| 1640 |
+
),
|
| 1641 |
+
WeightRenaming(
|
| 1642 |
+
source_patterns=r"^layers\.(\d+)\.hc_attn_scale$", target_patterns=r"layers.\1.attn_hc.scale"
|
| 1643 |
+
),
|
| 1644 |
+
WeightRenaming(source_patterns=r"^layers\.(\d+)\.hc_ffn_fn$", target_patterns=r"layers.\1.ffn_hc.fn"),
|
| 1645 |
+
WeightRenaming(source_patterns=r"^layers\.(\d+)\.hc_ffn_base$", target_patterns=r"layers.\1.ffn_hc.base"),
|
| 1646 |
+
WeightRenaming(
|
| 1647 |
+
source_patterns=r"^layers\.(\d+)\.hc_ffn_scale$", target_patterns=r"layers.\1.ffn_hc.scale"
|
| 1648 |
+
),
|
| 1649 |
+
WeightRenaming(
|
| 1650 |
+
source_patterns=r"^layers\.(\d+)\.attn\.",
|
| 1651 |
+
target_patterns=r"layers.\1.self_attn.",
|
| 1652 |
+
),
|
| 1653 |
+
WeightRenaming(
|
| 1654 |
+
source_patterns=r"^layers\.(\d+)\.ffn\.",
|
| 1655 |
+
target_patterns=r"layers.\1.mlp.",
|
| 1656 |
+
),
|
| 1657 |
+
WeightRenaming(
|
| 1658 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.attn_sink$",
|
| 1659 |
+
target_patterns=r"layers.\1.self_attn.sinks",
|
| 1660 |
+
),
|
| 1661 |
+
WeightRenaming(
|
| 1662 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.indexer\.compressor\.norm\.",
|
| 1663 |
+
target_patterns=r"layers.\1.self_attn.compressor.indexer.kv_norm.",
|
| 1664 |
+
),
|
| 1665 |
+
WeightRenaming(
|
| 1666 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.indexer\.compressor\.ape$",
|
| 1667 |
+
target_patterns=r"layers.\1.self_attn.compressor.indexer.position_bias",
|
| 1668 |
+
),
|
| 1669 |
+
WeightRenaming(
|
| 1670 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.indexer\.compressor\.",
|
| 1671 |
+
target_patterns=r"layers.\1.self_attn.compressor.indexer.",
|
| 1672 |
+
),
|
| 1673 |
+
WeightRenaming(
|
| 1674 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.indexer\.",
|
| 1675 |
+
target_patterns=r"layers.\1.self_attn.compressor.indexer.",
|
| 1676 |
+
),
|
| 1677 |
+
WeightRenaming(
|
| 1678 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.compressor\.indexer\.weights_proj\.",
|
| 1679 |
+
target_patterns=r"layers.\1.self_attn.compressor.indexer.scorer.weights_proj.",
|
| 1680 |
+
),
|
| 1681 |
+
WeightRenaming(
|
| 1682 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.compressor\.norm\.",
|
| 1683 |
+
target_patterns=r"layers.\1.self_attn.compressor.kv_norm.",
|
| 1684 |
+
),
|
| 1685 |
+
WeightRenaming(
|
| 1686 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.compressor\.ape$",
|
| 1687 |
+
target_patterns=r"layers.\1.self_attn.compressor.position_bias",
|
| 1688 |
+
),
|
| 1689 |
+
WeightRenaming(
|
| 1690 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wq_a\.",
|
| 1691 |
+
target_patterns=r"layers.\1.self_attn.\2.q_a_proj.",
|
| 1692 |
+
),
|
| 1693 |
+
WeightRenaming(
|
| 1694 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wq_b\.",
|
| 1695 |
+
target_patterns=r"layers.\1.self_attn.\2.q_b_proj.",
|
| 1696 |
+
),
|
| 1697 |
+
WeightRenaming(
|
| 1698 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wkv\.",
|
| 1699 |
+
target_patterns=r"layers.\1.self_attn.\2.kv_proj.",
|
| 1700 |
+
),
|
| 1701 |
+
WeightRenaming(
|
| 1702 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wgate\.",
|
| 1703 |
+
target_patterns=r"layers.\1.self_attn.\2.gate_proj.",
|
| 1704 |
+
),
|
| 1705 |
+
WeightRenaming(
|
| 1706 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wo_a\.",
|
| 1707 |
+
target_patterns=r"layers.\1.self_attn.\2.o_a_proj.",
|
| 1708 |
+
),
|
| 1709 |
+
WeightRenaming(
|
| 1710 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.(.*?)\.wo_b\.",
|
| 1711 |
+
target_patterns=r"layers.\1.self_attn.\2.o_b_proj.",
|
| 1712 |
+
),
|
| 1713 |
+
WeightRenaming(
|
| 1714 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.wq_a\.",
|
| 1715 |
+
target_patterns=r"layers.\1.self_attn.q_a_proj.",
|
| 1716 |
+
),
|
| 1717 |
+
WeightRenaming(
|
| 1718 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.wq_b\.",
|
| 1719 |
+
target_patterns=r"layers.\1.self_attn.q_b_proj.",
|
| 1720 |
+
),
|
| 1721 |
+
WeightRenaming(
|
| 1722 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.wkv\.",
|
| 1723 |
+
target_patterns=r"layers.\1.self_attn.kv_proj.",
|
| 1724 |
+
),
|
| 1725 |
+
WeightRenaming(
|
| 1726 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.wo_a\.",
|
| 1727 |
+
target_patterns=r"layers.\1.self_attn.o_a_proj.",
|
| 1728 |
+
),
|
| 1729 |
+
WeightRenaming(
|
| 1730 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.wo_b\.",
|
| 1731 |
+
target_patterns=r"layers.\1.self_attn.o_b_proj.",
|
| 1732 |
+
),
|
| 1733 |
+
WeightRenaming(
|
| 1734 |
+
source_patterns=r"^layers\.(\d+)\.self_attn\.q_norm\.",
|
| 1735 |
+
target_patterns=r"layers.\1.self_attn.q_a_norm.",
|
| 1736 |
+
),
|
| 1737 |
+
WeightRenaming(
|
| 1738 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.gate\.bias$",
|
| 1739 |
+
target_patterns=r"layers.\1.mlp.gate.e_score_correction_bias",
|
| 1740 |
+
),
|
| 1741 |
+
WeightRenaming(
|
| 1742 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.shared_experts\.w1\.",
|
| 1743 |
+
target_patterns=r"layers.\1.mlp.shared_experts.gate_proj.",
|
| 1744 |
+
),
|
| 1745 |
+
WeightRenaming(
|
| 1746 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.shared_experts\.w2\.",
|
| 1747 |
+
target_patterns=r"layers.\1.mlp.shared_experts.down_proj.",
|
| 1748 |
+
),
|
| 1749 |
+
WeightRenaming(
|
| 1750 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.shared_experts\.w3\.",
|
| 1751 |
+
target_patterns=r"layers.\1.mlp.shared_experts.up_proj.",
|
| 1752 |
+
),
|
| 1753 |
+
WeightRenaming(
|
| 1754 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.parallel_ffn\.w1\.",
|
| 1755 |
+
target_patterns=r"layers.\1.mlp.parallel_ffn.gate_proj.",
|
| 1756 |
+
),
|
| 1757 |
+
WeightRenaming(
|
| 1758 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.parallel_ffn\.w2\.",
|
| 1759 |
+
target_patterns=r"layers.\1.mlp.parallel_ffn.down_proj.",
|
| 1760 |
+
),
|
| 1761 |
+
WeightRenaming(
|
| 1762 |
+
source_patterns=r"^layers\.(\d+)\.mlp\.parallel_ffn\.w3\.",
|
| 1763 |
+
target_patterns=r"layers.\1.mlp.parallel_ffn.up_proj.",
|
| 1764 |
+
),
|
| 1765 |
+
WeightConverter(
|
| 1766 |
+
source_patterns=[
|
| 1767 |
+
"mlp.experts.*.w1.weight",
|
| 1768 |
+
"mlp.experts.*.w3.weight",
|
| 1769 |
+
],
|
| 1770 |
+
target_patterns="mlp.experts.gate_up_proj",
|
| 1771 |
+
operations=[MergeModulelist(dim=0), Concatenate(dim=1)],
|
| 1772 |
+
),
|
| 1773 |
+
WeightConverter(
|
| 1774 |
+
source_patterns="mlp.experts.*.w2.weight",
|
| 1775 |
+
target_patterns="mlp.experts.down_proj",
|
| 1776 |
+
operations=[MergeModulelist(dim=0)],
|
| 1777 |
+
),
|
| 1778 |
+
]
|
| 1779 |
+
|
| 1780 |
+
|
| 1781 |
+
try:
|
| 1782 |
+
register_checkpoint_conversion_mapping("agnes", _AGNES_CHECKPOINT_CONVERSION_MAPPING)
|
| 1783 |
+
except ValueError:
|
| 1784 |
+
pass # already registered (module imported more than once)
|
serve.sh
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Agnes PFFN-FP8 serving entrypoint, pinned to lmsysorg/sglang:v0.5.16.
|
| 3 |
+
#
|
| 4 |
+
# Usage:
|
| 5 |
+
# docker run --gpus all --shm-size 64g -p 30001:30002 \
|
| 6 |
+
# -v /path/to/this/model:/model \
|
| 7 |
+
# lmsysorg/sglang:v0.5.16 bash /model/serve.sh
|
| 8 |
+
#
|
| 9 |
+
# Extra sglang flags may be appended after serve.sh, e.g.:
|
| 10 |
+
# ... bash /model/serve.sh --tp 4
|
| 11 |
+
set -e
|
| 12 |
+
MODEL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 13 |
+
SGL_PKG=/sgl-workspace/sglang/python/sglang
|
| 14 |
+
|
| 15 |
+
# Overlay the Agnes support files onto the stock sglang package
|
| 16 |
+
# (inside this container only; the model directory is never modified).
|
| 17 |
+
cp -r "$MODEL_DIR/sglang_patch/srt" "$MODEL_DIR/sglang_patch/kernels" "$SGL_PKG/"
|
| 18 |
+
|
| 19 |
+
exec sglang serve --model-path "$MODEL_DIR" --trust-remote-code --tp 8 \
|
| 20 |
+
--context-length 1048576 --mem-fraction-static 0.90 \
|
| 21 |
+
--host 0.0.0.0 --port 30002 "$@"
|
sglang_patch/kernels/ops/layernorm/mhc_head.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fused triton kernel for the DSV4 hc_head LM-head mixer.
|
| 2 |
+
|
| 3 |
+
Reference torch implementation (deepseek_v4.py AgnesModel.hc_head):
|
| 4 |
+
|
| 5 |
+
shape, dtype = x.size(), x.dtype
|
| 6 |
+
x = x.flatten(1).float()
|
| 7 |
+
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + norm_eps)
|
| 8 |
+
mixes = F.linear(x, hc_fn) * rsqrt
|
| 9 |
+
pre = torch.sigmoid(mixes * hc_scale + hc_base) + hc_eps
|
| 10 |
+
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=1)
|
| 11 |
+
return y.to(dtype)
|
| 12 |
+
|
| 13 |
+
Shapes (DSV4-Pro, hc_mult=4, hidden_size=7168 typical):
|
| 14 |
+
x : (T, hc_mult, hidden_size) bf16
|
| 15 |
+
hc_fn : (hc_mult, hc_mult * hidden_size) fp32
|
| 16 |
+
scale : (1,) fp32
|
| 17 |
+
base : (hc_mult,) fp32
|
| 18 |
+
out y : (T, hidden_size) bf16
|
| 19 |
+
|
| 20 |
+
This is a one-shot LM-head op (fires once per forward on the last PP rank), so
|
| 21 |
+
we use a 1-CTA-per-token design that does two passes over x without split-K.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import triton
|
| 28 |
+
import triton.language as tl
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@triton.jit
|
| 32 |
+
def _hc_head_kernel(
|
| 33 |
+
x_ptr,
|
| 34 |
+
fn_ptr,
|
| 35 |
+
scale_ptr,
|
| 36 |
+
base_ptr,
|
| 37 |
+
y_ptr,
|
| 38 |
+
hidden_size: tl.constexpr,
|
| 39 |
+
HC_MULT: tl.constexpr,
|
| 40 |
+
K_TOTAL: tl.constexpr,
|
| 41 |
+
BLOCK_K: tl.constexpr,
|
| 42 |
+
BLOCK_D: tl.constexpr,
|
| 43 |
+
norm_eps: tl.constexpr,
|
| 44 |
+
hc_eps: tl.constexpr,
|
| 45 |
+
):
|
| 46 |
+
pid = tl.program_id(0).to(tl.int64)
|
| 47 |
+
|
| 48 |
+
# ---------- Pass 1: sum_sq over flattened K dim, plus hc_mult inner products ----------
|
| 49 |
+
sumsq = tl.zeros((), dtype=tl.float32)
|
| 50 |
+
mix = tl.zeros((HC_MULT,), dtype=tl.float32)
|
| 51 |
+
|
| 52 |
+
x_row = x_ptr + pid * K_TOTAL
|
| 53 |
+
m_idx = tl.arange(0, HC_MULT)
|
| 54 |
+
|
| 55 |
+
for k_off in tl.range(0, K_TOTAL, BLOCK_K):
|
| 56 |
+
k_offs = k_off + tl.arange(0, BLOCK_K)
|
| 57 |
+
k_mask = k_offs < K_TOTAL
|
| 58 |
+
x_tile = tl.load(x_row + k_offs, mask=k_mask, other=0.0).to(tl.float32)
|
| 59 |
+
|
| 60 |
+
sumsq += tl.sum(x_tile * x_tile, axis=0)
|
| 61 |
+
|
| 62 |
+
fn_offs = m_idx[:, None] * K_TOTAL + k_offs[None, :]
|
| 63 |
+
fn_mask = (m_idx[:, None] < HC_MULT) & k_mask[None, :]
|
| 64 |
+
fn_tile = tl.load(fn_ptr + fn_offs, mask=fn_mask, other=0.0)
|
| 65 |
+
mix += tl.sum(fn_tile * x_tile[None, :], axis=1)
|
| 66 |
+
|
| 67 |
+
rsqrt = tl.rsqrt(sumsq / K_TOTAL + norm_eps)
|
| 68 |
+
scale_v = tl.load(scale_ptr).to(tl.float32)
|
| 69 |
+
base_v = tl.load(base_ptr + m_idx).to(tl.float32)
|
| 70 |
+
|
| 71 |
+
# pre[m] = sigmoid(mix[m] * rsqrt * scale + base[m]) + hc_eps
|
| 72 |
+
pre = tl.sigmoid(mix * rsqrt * scale_v + base_v) + hc_eps
|
| 73 |
+
|
| 74 |
+
# ---------- Pass 2: y[d] = sum_m pre[m] * x[m, d] for d in range(hidden_size) ----------
|
| 75 |
+
y_row = y_ptr + pid * hidden_size
|
| 76 |
+
|
| 77 |
+
for d_off in tl.range(0, hidden_size, BLOCK_D):
|
| 78 |
+
d_offs = d_off + tl.arange(0, BLOCK_D)
|
| 79 |
+
d_mask = d_offs < hidden_size
|
| 80 |
+
|
| 81 |
+
x_offs = m_idx[:, None] * hidden_size + d_offs[None, :]
|
| 82 |
+
x_mask = (m_idx[:, None] < HC_MULT) & d_mask[None, :]
|
| 83 |
+
x_block = tl.load(x_row + x_offs, mask=x_mask, other=0.0).to(tl.float32)
|
| 84 |
+
|
| 85 |
+
y_block = tl.sum(pre[:, None] * x_block, axis=0)
|
| 86 |
+
|
| 87 |
+
tl.store(y_row + d_offs, y_block.to(y_ptr.dtype.element_ty), mask=d_mask)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def fused_hc_head(
|
| 91 |
+
x: torch.Tensor,
|
| 92 |
+
hc_fn: torch.Tensor,
|
| 93 |
+
hc_scale: torch.Tensor,
|
| 94 |
+
hc_base: torch.Tensor,
|
| 95 |
+
norm_eps: float,
|
| 96 |
+
hc_eps: float,
|
| 97 |
+
) -> torch.Tensor:
|
| 98 |
+
"""Fused (RMSNorm + Linear + Sigmoid-gate + weighted-sum) for the DSV4 hc_head.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
x : (T, hc_mult, hidden_size) bf16/fp16, must be contiguous
|
| 102 |
+
hc_fn : (hc_mult, hc_mult * hidden_size) fp32, contiguous
|
| 103 |
+
hc_scale : (1,) fp32 scalar
|
| 104 |
+
hc_base : (hc_mult,) fp32
|
| 105 |
+
norm_eps : RMS epsilon
|
| 106 |
+
hc_eps : additive epsilon after sigmoid
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
y : (T, hidden_size) same dtype as x
|
| 110 |
+
"""
|
| 111 |
+
assert x.is_contiguous(), "x must be contiguous"
|
| 112 |
+
assert hc_fn.is_contiguous(), "hc_fn must be contiguous"
|
| 113 |
+
assert hc_scale.dtype == torch.float32 and hc_base.dtype == torch.float32
|
| 114 |
+
assert hc_fn.dtype == torch.float32
|
| 115 |
+
assert x.dim() == 3, f"x must be 3D (T, hc_mult, hidden_size), got {x.shape}"
|
| 116 |
+
|
| 117 |
+
T, hc_mult, hidden_size = x.shape
|
| 118 |
+
assert hc_fn.shape == (hc_mult, hc_mult * hidden_size), (
|
| 119 |
+
f"hc_fn shape {hc_fn.shape} does not match (hc_mult={hc_mult}, "
|
| 120 |
+
f"hc_mult*hidden_size={hc_mult * hidden_size})"
|
| 121 |
+
)
|
| 122 |
+
assert hc_base.shape == (hc_mult,)
|
| 123 |
+
assert hc_scale.numel() == 1
|
| 124 |
+
|
| 125 |
+
y = torch.empty((T, hidden_size), dtype=x.dtype, device=x.device)
|
| 126 |
+
|
| 127 |
+
if T == 0:
|
| 128 |
+
return y
|
| 129 |
+
|
| 130 |
+
BLOCK_K = 512
|
| 131 |
+
BLOCK_D = 512
|
| 132 |
+
|
| 133 |
+
hc_mult_pow2 = max(1, triton.next_power_of_2(hc_mult))
|
| 134 |
+
|
| 135 |
+
grid = (T,)
|
| 136 |
+
_hc_head_kernel[grid](
|
| 137 |
+
x,
|
| 138 |
+
hc_fn,
|
| 139 |
+
hc_scale,
|
| 140 |
+
hc_base,
|
| 141 |
+
y,
|
| 142 |
+
hidden_size=hidden_size,
|
| 143 |
+
HC_MULT=hc_mult_pow2,
|
| 144 |
+
K_TOTAL=hc_mult * hidden_size,
|
| 145 |
+
BLOCK_K=BLOCK_K,
|
| 146 |
+
BLOCK_D=BLOCK_D,
|
| 147 |
+
norm_eps=norm_eps,
|
| 148 |
+
hc_eps=hc_eps,
|
| 149 |
+
num_warps=4,
|
| 150 |
+
)
|
| 151 |
+
return y
|
sglang_patch/srt/arg_groups/overrides.py
ADDED
|
@@ -0,0 +1,2230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023-2026 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
"""Declarative model-override registry.
|
| 15 |
+
|
| 16 |
+
Model-identity adjustments to the server configuration are DECLARED here and
|
| 17 |
+
materialized onto ``server_args`` at the end of ``__post_init__`` (gate
|
| 18 |
+
order, last writer wins) — model code never mutates ``ServerArgs`` fields
|
| 19 |
+
imperatively.
|
| 20 |
+
|
| 21 |
+
Two declaration forms, keyed on ``hf_config.architectures[0]``:
|
| 22 |
+
|
| 23 |
+
- ``MODEL_OVERRIDES``: pure-constant cases — ``arch -> {field: value}``.
|
| 24 |
+
- ``@register_model_override(arch)``: derived cases — a callable
|
| 25 |
+
``fn(server_args, hf_config) -> dict`` that faithfully carries today's
|
| 26 |
+
conditional logic. ``server_args`` is pristine and must be treated
|
| 27 |
+
read-only: the callable returns declarations, it never writes.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import dataclasses
|
| 33 |
+
import logging
|
| 34 |
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
| 35 |
+
|
| 36 |
+
from sglang.srt.arg_groups.arg_utils import resolvable_fields
|
| 37 |
+
from sglang.srt.environ import envs
|
| 38 |
+
from sglang.srt.model_executor.cuda_graph_config import Backend
|
| 39 |
+
from sglang.srt.utils.common import (
|
| 40 |
+
cpu_has_amx_support,
|
| 41 |
+
get_device_capability,
|
| 42 |
+
get_device_sm,
|
| 43 |
+
get_nvidia_driver_version,
|
| 44 |
+
get_quantization_config,
|
| 45 |
+
is_blackwell_supported,
|
| 46 |
+
is_cpu,
|
| 47 |
+
is_cuda,
|
| 48 |
+
is_flashinfer_available,
|
| 49 |
+
is_gfx95_supported,
|
| 50 |
+
is_hip,
|
| 51 |
+
is_musa,
|
| 52 |
+
is_npu,
|
| 53 |
+
is_sm90_supported,
|
| 54 |
+
is_sm100_supported,
|
| 55 |
+
is_sm120_supported,
|
| 56 |
+
is_triton_kernels_available,
|
| 57 |
+
is_xpu,
|
| 58 |
+
xpu_has_xmx_support,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
logger = logging.getLogger(__name__)
|
| 62 |
+
|
| 63 |
+
# Constant per-architecture overrides (populated by the migration sweeps).
|
| 64 |
+
MODEL_OVERRIDES: Dict[str, Dict[str, Any]] = {
|
| 65 |
+
# These models run in bfloat16 regardless of the requested dtype
|
| 66 |
+
# (faithful port of the legacy unconditional arch branch).
|
| 67 |
+
"MistralLarge3ForCausalLM": {"dtype": "bfloat16"},
|
| 68 |
+
"PixtralForConditionalGeneration": {"dtype": "bfloat16"},
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Derived per-architecture override providers, in registration order.
|
| 72 |
+
_MODEL_OVERRIDE_FNS: Dict[str, List[Callable[..., dict]]] = {}
|
| 73 |
+
|
| 74 |
+
# Predicate-keyed providers, in registration order — for legacy branches
|
| 75 |
+
# matched by substring/predicate on the architecture string rather than an
|
| 76 |
+
# exact name (e.g. '"Step3p5ForCausalLM" in model_arch').
|
| 77 |
+
_PREDICATE_OVERRIDE_FNS: List[Tuple[Callable[[str], bool], Callable[..., dict]]] = []
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def register_model_override(architecture: str):
|
| 81 |
+
"""Register a derived-override provider for ``architecture``.
|
| 82 |
+
|
| 83 |
+
The decorated callable receives ``(server_args, hf_config)``, must not
|
| 84 |
+
mutate either, and returns a ``{field: resolved_value}`` dict (possibly
|
| 85 |
+
empty when nothing applies). Providers needing derived model data beyond
|
| 86 |
+
the HF config go through ``server_args.get_model_config()`` (cached,
|
| 87 |
+
read-only) — never anything mutating.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
def decorator(fn: Callable[..., dict]) -> Callable[..., dict]:
|
| 91 |
+
_MODEL_OVERRIDE_FNS.setdefault(architecture, []).append(fn)
|
| 92 |
+
return fn
|
| 93 |
+
|
| 94 |
+
return decorator
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def register_model_override_predicate(predicate: Callable[[str], bool]):
|
| 98 |
+
"""Register a derived-override provider keyed by an architecture
|
| 99 |
+
predicate. Same callable contract as ``register_model_override``."""
|
| 100 |
+
|
| 101 |
+
def decorator(fn: Callable[..., dict]) -> Callable[..., dict]:
|
| 102 |
+
_PREDICATE_OVERRIDE_FNS.append((predicate, fn))
|
| 103 |
+
return fn
|
| 104 |
+
|
| 105 |
+
return decorator
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _invoke_provider(
|
| 109 |
+
fn: Callable[..., dict], server_args: Any, hf_config: Any
|
| 110 |
+
) -> Dict[str, Any]:
|
| 111 |
+
declared = fn(server_args, hf_config)
|
| 112 |
+
if not isinstance(declared, dict):
|
| 113 |
+
raise TypeError(
|
| 114 |
+
f"model override provider {fn.__qualname__} must return a dict, "
|
| 115 |
+
f"got {type(declared).__name__}"
|
| 116 |
+
)
|
| 117 |
+
return declared
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class ResolvedView:
|
| 121 |
+
"""Read-only view of the resolving configuration handed to post-process
|
| 122 |
+
passes: the accumulated declarations overlaid on the pristine
|
| 123 |
+
``server_args`` (residual imperative writes of non-resolved fields show
|
| 124 |
+
through the fallthrough) — exactly the state the legacy handler at the
|
| 125 |
+
same slot observed. Writes are rejected: passes return declarations.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
__slots__ = ("_server_args", "_overlay")
|
| 129 |
+
|
| 130 |
+
def __init__(self, server_args: Any, overlay: Optional[Dict[str, Any]] = None):
|
| 131 |
+
object.__setattr__(self, "_server_args", server_args)
|
| 132 |
+
object.__setattr__(self, "_overlay", overlay or {})
|
| 133 |
+
|
| 134 |
+
def __getattr__(self, name: str) -> Any:
|
| 135 |
+
overlay = object.__getattribute__(self, "_overlay")
|
| 136 |
+
if name in overlay:
|
| 137 |
+
return overlay[name]
|
| 138 |
+
return getattr(object.__getattribute__(self, "_server_args"), name)
|
| 139 |
+
|
| 140 |
+
def __setattr__(self, name: str, value: Any) -> None:
|
| 141 |
+
raise AttributeError(
|
| 142 |
+
"ResolvedView is read-only; post-process passes return declarations"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# Ordered post-process passes (the normalization stage). List order is the
|
| 147 |
+
# end-state execution order and mirrors today's handler call sequence in
|
| 148 |
+
# __post_init__; during the transition each pass is invoked from its legacy
|
| 149 |
+
# slot via run_post_process_pass, so ordering is preserved byte-for-byte.
|
| 150 |
+
POST_PROCESS_PASSES: List[Callable[..., dict]] = []
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def register_post_process(fn: Callable[..., dict]) -> Callable[..., dict]:
|
| 154 |
+
"""Register a post-process pass: ``fn(view) -> {field: resolved_value}``.
|
| 155 |
+
|
| 156 |
+
The pass reads a :class:`ResolvedView` (post-model-override state) and
|
| 157 |
+
must not mutate anything; validations may live in a pass (read + raise).
|
| 158 |
+
"""
|
| 159 |
+
POST_PROCESS_PASSES.append(fn)
|
| 160 |
+
return fn
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _declaration_overlay(server_args: Any) -> Dict[str, Any]:
|
| 164 |
+
"""Accumulated declared values: declarations never mutate
|
| 165 |
+
``server_args``, so mid-resolution readers overlay them from the
|
| 166 |
+
declaration stash (last writer wins, like the gate)."""
|
| 167 |
+
overlay: Dict[str, Any] = {}
|
| 168 |
+
for _source, declared in getattr(server_args, "_resolved_overrides", None) or ():
|
| 169 |
+
overlay.update(declared)
|
| 170 |
+
return overlay
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def run_post_process_pass(server_args: Any, fn: Callable[..., dict]) -> None:
|
| 174 |
+
"""Invoke one pass at its legacy handler slot.
|
| 175 |
+
|
| 176 |
+
Evaluates the pass on the resolving state (a read-only view with the
|
| 177 |
+
accumulated declarations overlaid from the stash) and appends its
|
| 178 |
+
declaration to the stash. During ``__post_init__`` the fields stay
|
| 179 |
+
untouched — ``materialize_declarations`` applies the whole stash once at
|
| 180 |
+
the end of resolution; a pass invoked after materialization (a post-init
|
| 181 |
+
slot) writes through immediately.
|
| 182 |
+
"""
|
| 183 |
+
declared = fn(ResolvedView(server_args, overlay=_declaration_overlay(server_args)))
|
| 184 |
+
if not isinstance(declared, dict):
|
| 185 |
+
raise TypeError(
|
| 186 |
+
f"post-process pass {fn.__qualname__} must return a dict, "
|
| 187 |
+
f"got {type(declared).__name__}"
|
| 188 |
+
)
|
| 189 |
+
if declared:
|
| 190 |
+
entry = (fn.__qualname__, dict(declared))
|
| 191 |
+
stash = getattr(server_args, "_resolved_overrides", None)
|
| 192 |
+
if stash is None:
|
| 193 |
+
# Handlers hosting pass slots may be invoked directly on fixtures
|
| 194 |
+
# that never ran the monolith dispatch (which owns the stash);
|
| 195 |
+
# create it lazily. Real publishes always pass through the
|
| 196 |
+
# dispatch first — the dispatch ASSIGNS the stash, so pass slots
|
| 197 |
+
# must sit at or after it in __post_init__ order.
|
| 198 |
+
stash = server_args._resolved_overrides = []
|
| 199 |
+
stash.append(entry)
|
| 200 |
+
validate_declarations(server_args, [entry])
|
| 201 |
+
if getattr(server_args, "_declarations_materialized", False):
|
| 202 |
+
_apply_fields(server_args, declared)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def _apply_fields(server_args: Any, fields: Dict[str, Any]) -> None:
|
| 206 |
+
"""Write fields on behalf of the pipeline (bypasses the strict bare-
|
| 207 |
+
assignment guard that protects post-resolution mutation)."""
|
| 208 |
+
object.__setattr__(server_args, "_in_override", True)
|
| 209 |
+
try:
|
| 210 |
+
for field, value in fields.items():
|
| 211 |
+
setattr(server_args, field, value)
|
| 212 |
+
finally:
|
| 213 |
+
object.__setattr__(server_args, "_in_override", False)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def materialize_declarations(server_args: Any) -> None:
|
| 217 |
+
"""Apply the accumulated declarations onto ``server_args`` once, at the
|
| 218 |
+
end of ``__post_init__`` (gate order: last writer wins). After this the
|
| 219 |
+
fields carry the resolved configuration — every post-init reader, in any
|
| 220 |
+
process, reads them directly; ``resolved_view`` remains an internal
|
| 221 |
+
helper for mid-resolution code only."""
|
| 222 |
+
for _source, declared in getattr(server_args, "_resolved_overrides", None) or ():
|
| 223 |
+
for field, value in declared.items():
|
| 224 |
+
setattr(server_args, field, value)
|
| 225 |
+
server_args._declarations_materialized = True
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def resolved_view(server_args: Any) -> ResolvedView:
|
| 229 |
+
"""Read-only view of the resolving configuration for mid-resolution code
|
| 230 |
+
that is not a pass (``__post_init__`` handlers and hooks). Internal to
|
| 231 |
+
the resolution pipeline: after ``materialize_declarations`` runs, the
|
| 232 |
+
fields themselves carry the resolved values — read them directly."""
|
| 233 |
+
return ResolvedView(server_args, overlay=_declaration_overlay(server_args))
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def attention_backends_of(cfg: Any) -> tuple:
|
| 237 |
+
"""(prefill, decode) attention backends of a config-shaped object (a
|
| 238 |
+
ResolvedView mid-resolution, or pristine server_args at dispatch time):
|
| 239 |
+
split fields fall back to the base backend."""
|
| 240 |
+
prefill = (
|
| 241 |
+
cfg.prefill_attention_backend
|
| 242 |
+
if cfg.prefill_attention_backend
|
| 243 |
+
else cfg.attention_backend
|
| 244 |
+
)
|
| 245 |
+
decode = (
|
| 246 |
+
cfg.decode_attention_backend
|
| 247 |
+
if cfg.decode_attention_backend
|
| 248 |
+
else cfg.attention_backend
|
| 249 |
+
)
|
| 250 |
+
return prefill, decode
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def mamba_extra_buffer_of(cfg: Any) -> bool:
|
| 254 |
+
"""Mid-resolution equivalent of runtime_context.mamba_extra_buffer_enabled:
|
| 255 |
+
reads the (possibly overlaid) strategy from a config-shaped object."""
|
| 256 |
+
return cfg.disable_radix_cache is False and cfg.mamba_radix_cache_strategy in (
|
| 257 |
+
"extra_buffer",
|
| 258 |
+
"extra_buffer_lazy",
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def declare_load_time_override(source: str, declared: Dict[str, Any]) -> None:
|
| 263 |
+
"""Declare a load-time resolved field (model-file config overrides,
|
| 264 |
+
weight-resolved dtypes) on the published ``server_args``: resolution has
|
| 265 |
+
already materialized, so the declaration writes through, joining the
|
| 266 |
+
declaration stash for provenance and republish consistency."""
|
| 267 |
+
from sglang.srt.runtime_context import get_context
|
| 268 |
+
|
| 269 |
+
server_args = get_context().server_args
|
| 270 |
+
validate_declarations(server_args, [(source, dict(declared))])
|
| 271 |
+
override = getattr(server_args, "override", None)
|
| 272 |
+
if override is not None:
|
| 273 |
+
override(source, **declared)
|
| 274 |
+
else:
|
| 275 |
+
# Config-shaped fixtures without the mutation entry point.
|
| 276 |
+
_apply_fields(server_args, declared)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def collect_model_override_declarations(
|
| 280 |
+
architecture: str, server_args: Any, hf_config: Any
|
| 281 |
+
) -> List[Tuple[str, Dict[str, Any]]]:
|
| 282 |
+
"""Collect ``(source, declaration)`` pairs for one architecture.
|
| 283 |
+
|
| 284 |
+
Application order (last writer wins downstream in the gate): the constant
|
| 285 |
+
``MODEL_OVERRIDES`` entry first, then exact-keyed callables in
|
| 286 |
+
registration order, then matching predicate-keyed callables in
|
| 287 |
+
registration order. Empty declarations are dropped.
|
| 288 |
+
"""
|
| 289 |
+
declarations: List[Tuple[str, Dict[str, Any]]] = []
|
| 290 |
+
const = MODEL_OVERRIDES.get(architecture)
|
| 291 |
+
if const:
|
| 292 |
+
declarations.append((f"MODEL_OVERRIDES[{architecture!r}]", dict(const)))
|
| 293 |
+
for fn in _MODEL_OVERRIDE_FNS.get(architecture, ()):
|
| 294 |
+
declared = _invoke_provider(fn, server_args, hf_config)
|
| 295 |
+
if declared:
|
| 296 |
+
declarations.append((fn.__qualname__, dict(declared)))
|
| 297 |
+
for predicate, fn in _PREDICATE_OVERRIDE_FNS:
|
| 298 |
+
if predicate(architecture):
|
| 299 |
+
declared = _invoke_provider(fn, server_args, hf_config)
|
| 300 |
+
if declared:
|
| 301 |
+
declarations.append((fn.__qualname__, dict(declared)))
|
| 302 |
+
return declarations
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
# ---------------------------------------------------------------------------
|
| 306 |
+
# Derived per-family declarations (faithful ports of legacy arch branches).
|
| 307 |
+
# Callables read the PRISTINE server_args, never write; logging is kept
|
| 308 |
+
# verbatim from the legacy branch for operator-visible fidelity.
|
| 309 |
+
# ---------------------------------------------------------------------------
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _register_for(*architectures: str):
|
| 313 |
+
"""Register one provider for several architectures (family lists)."""
|
| 314 |
+
|
| 315 |
+
def decorator(fn: Callable[..., dict]) -> Callable[..., dict]:
|
| 316 |
+
for architecture in architectures:
|
| 317 |
+
register_model_override(architecture)(fn)
|
| 318 |
+
return fn
|
| 319 |
+
|
| 320 |
+
return decorator
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
@_register_for(
|
| 324 |
+
"DeepseekV3ForCausalLM",
|
| 325 |
+
"DeepseekV32ForCausalLM",
|
| 326 |
+
"KimiK25ForConditionalGeneration",
|
| 327 |
+
"MistralLarge3ForCausalLM",
|
| 328 |
+
"PixtralForConditionalGeneration",
|
| 329 |
+
"GlmMoeDsaForCausalLM",
|
| 330 |
+
"LongcatFlashForCausalLM",
|
| 331 |
+
"LongcatFlashForCausalLMNextN",
|
| 332 |
+
)
|
| 333 |
+
def _deepseek_family_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 334 |
+
"""Order-safe declarations of the DeepSeek/DSA branch. The CP parallel
|
| 335 |
+
writes (enable_dp_attention/ep_size/moe_a2a_backend have post-monolith
|
| 336 |
+
writers), the kv-cache/split-backend defaults, the quant/moe block (read
|
| 337 |
+
before it by _set_default_dsa_kv_cache_dtype) and the env writes stay in
|
| 338 |
+
the branch."""
|
| 339 |
+
from sglang.srt.configs.model_config import is_deepseek_dsa
|
| 340 |
+
|
| 341 |
+
overrides: Dict[str, Any] = {}
|
| 342 |
+
if is_deepseek_dsa(hf_config): # DeepSeek 3.2/GLM 5
|
| 343 |
+
# Set attention backend for DeepSeek
|
| 344 |
+
if server_args.is_attention_backend_not_set():
|
| 345 |
+
overrides["attention_backend"] = "dsa"
|
| 346 |
+
logger.info("Use dsa attention backend for DeepSeek with DSA.")
|
| 347 |
+
if not is_npu() and not is_xpu(): # CUDA or ROCm GPU
|
| 348 |
+
if server_args.enable_prefill_cp:
|
| 349 |
+
logger.warning(
|
| 350 |
+
"Context parallel feature is still under experiment. It has only been verified on Hopper platform."
|
| 351 |
+
)
|
| 352 |
+
overrides["enable_dp_attention"] = True
|
| 353 |
+
overrides["moe_dense_tp_size"] = 1
|
| 354 |
+
if server_args.cp_strategy == "zigzag":
|
| 355 |
+
overrides["moe_a2a_backend"] = "deepep"
|
| 356 |
+
overrides["ep_size"] = server_args.tp_size
|
| 357 |
+
logger.warning(
|
| 358 |
+
"zigzag DSA CP requires moe_dense_tp_size=1, "
|
| 359 |
+
"moe_a2a_backend=deepep, ep_size=tp_size, batch_size=1."
|
| 360 |
+
)
|
| 361 |
+
else:
|
| 362 |
+
assert (
|
| 363 |
+
server_args.dp_size == 1
|
| 364 |
+
), "interleave DSA CP does not support DP attention."
|
| 365 |
+
assert (
|
| 366 |
+
server_args.tp_size <= 8
|
| 367 |
+
), "Context parallel only supports single machine (tp_size <= 8). Cross-machine CP has precision issues."
|
| 368 |
+
# Note(kpham-sgl): Keep attn_tp_size == 1 under DSA CP.
|
| 369 |
+
# DSACPLayerCommunicator does not all-reduce attention-TP
|
| 370 |
+
# partial o_proj outputs before replicated dense FFNs.
|
| 371 |
+
attn_cp_size = server_args.tp_size // server_args.dp_size
|
| 372 |
+
overrides["attn_cp_size"] = attn_cp_size
|
| 373 |
+
logger.warning(
|
| 374 |
+
"Enabled DSA context parallel: "
|
| 375 |
+
f"strategy={server_args.cp_strategy}, dp_size={server_args.dp_size}, "
|
| 376 |
+
f"moe_dense_tp_size={overrides['moe_dense_tp_size']}, "
|
| 377 |
+
f"ep_size={overrides.get('ep_size', server_args.ep_size)}, tp_size={server_args.tp_size}, "
|
| 378 |
+
f"attn_cp_size={attn_cp_size}, "
|
| 379 |
+
f"kv_cache_dtype={server_args.kv_cache_dtype}, "
|
| 380 |
+
f"moe_a2a_backend={overrides.get('moe_a2a_backend', server_args.moe_a2a_backend)}, "
|
| 381 |
+
f"cuda_graph_config[prefill].backend=disabled"
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Deferred import to avoid a circular import at module-load
|
| 385 |
+
# time (dsa.utils imports the runtime-context accessors).
|
| 386 |
+
from sglang.srt.layers.attention.dsa.utils import (
|
| 387 |
+
aiter_can_use_preshuffle_paged_mqa,
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
if is_hip() and not aiter_can_use_preshuffle_paged_mqa():
|
| 391 |
+
# Legacy ROCm DSA path: aiter's gluon paged-MQA kernel is
|
| 392 |
+
# unavailable (Triton<3.5 and AITER_ENABLE_AOT_GLUON_PA_MQA_LOGITS
|
| 393 |
+
# not set, or SGLANG_DSA_HIP_DISABLE_PRESHUFFLE=1 / SGLANG_USE_AITER=0).
|
| 394 |
+
overrides["page_size"] = 1
|
| 395 |
+
logger.warning(
|
| 396 |
+
"Setting page size to 1 for DeepSeek DSA on ROCm "
|
| 397 |
+
"(aiter preshuffle paged-MQA path unavailable: "
|
| 398 |
+
"needs Triton>=3.5.0 or AITER_ENABLE_AOT_GLUON_PA_MQA_LOGITS=1)."
|
| 399 |
+
)
|
| 400 |
+
else:
|
| 401 |
+
overrides["page_size"] = 64
|
| 402 |
+
logger.warning("Setting page size to 64 for DeepSeek DSA.")
|
| 403 |
+
else:
|
| 404 |
+
# DeepSeek V3/R1/V3.1
|
| 405 |
+
if is_sm100_supported():
|
| 406 |
+
if (
|
| 407 |
+
server_args.attention_backend is None
|
| 408 |
+
and server_args.prefill_attention_backend is None
|
| 409 |
+
and server_args.decode_attention_backend is None
|
| 410 |
+
):
|
| 411 |
+
overrides["attention_backend"] = "trtllm_mla"
|
| 412 |
+
logger.info(
|
| 413 |
+
"Use trtllm_mla as attention backend on sm100 for DeepseekV3ForCausalLM"
|
| 414 |
+
)
|
| 415 |
+
# MLA prefill CP auto-config. Mirrors the NSA CP block above
|
| 416 |
+
# (minus the in-seq/round-robin mode split, which MLA CP does not support)
|
| 417 |
+
if server_args.enable_prefill_cp and server_args.use_mla_backend():
|
| 418 |
+
logger.warning(
|
| 419 |
+
"MLA prefill context parallel is still experimental. "
|
| 420 |
+
"Verified on Hopper with the fa3 backend."
|
| 421 |
+
)
|
| 422 |
+
overrides["enable_dp_attention"] = True
|
| 423 |
+
# TODO(kpham-sgl) Supports moe_dense_tp_size != 1.
|
| 424 |
+
overrides["moe_dense_tp_size"] = 1
|
| 425 |
+
overrides["moe_a2a_backend"] = "deepep"
|
| 426 |
+
overrides["ep_size"] = server_args.tp_size
|
| 427 |
+
logger.warning(
|
| 428 |
+
"For MLA CP, we have the following restrictions: moe_dense_tp_size == 1, moe_a2a_backend == deepep, ep_size == tp_size, batch_size == 1"
|
| 429 |
+
)
|
| 430 |
+
# FIXME(kpham-sgl): Keep attn_tp_size == 1 under MLA CP.
|
| 431 |
+
# DSACPLayerCommunicator does not all-reduce attention-TP
|
| 432 |
+
# partial o_proj outputs before replicated dense FFNs.
|
| 433 |
+
attn_cp_size = server_args.tp_size // server_args.dp_size
|
| 434 |
+
overrides["attn_cp_size"] = attn_cp_size
|
| 435 |
+
logger.warning(
|
| 436 |
+
f"Enable Context Parallel opt for MLA, "
|
| 437 |
+
f"Setting dp_size == {server_args.dp_size} and "
|
| 438 |
+
f"attn_cp_size == {attn_cp_size}, "
|
| 439 |
+
f"moe_dense_tp_size == {overrides['moe_dense_tp_size']}, "
|
| 440 |
+
f"ep_size == {overrides['ep_size']}, "
|
| 441 |
+
f"tp_size == {server_args.tp_size}, "
|
| 442 |
+
f"moe_a2a_backend {overrides['moe_a2a_backend']}, "
|
| 443 |
+
f"cuda_graph_config[prefill].backend=disabled"
|
| 444 |
+
)
|
| 445 |
+
return overrides
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
# Keep in sync with MIMO_V2_MODEL_ARCHS (server_args.py / configs/hf_config.py).
|
| 449 |
+
@_register_for("MiMoV2ForCausalLM", "MiMoV2FlashForCausalLM")
|
| 450 |
+
def _mimo_v2_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 451 |
+
if server_args.speculative_algorithm == "EAGLE":
|
| 452 |
+
logger.info("Enable multi-layer EAGLE speculative decoding for MiMoV2 model.")
|
| 453 |
+
return {"enable_multi_layer_eagle": True}
|
| 454 |
+
return {}
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
@_register_for("MiniMaxM2ForCausalLM")
|
| 458 |
+
def _minimax_m2_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 459 |
+
logger.info(
|
| 460 |
+
"Enable TF32 matmul for MiniMaxM2ForCausalLM model to improve gate gemm performance."
|
| 461 |
+
)
|
| 462 |
+
return {"enable_tf32_matmul": True}
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
@_register_for("MiniMaxM3SparseForCausalLM", "MiniMaxM3SparseForConditionalGeneration")
|
| 466 |
+
def _minimax_m3_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 467 |
+
|
| 468 |
+
overrides: Dict[str, Any] = {}
|
| 469 |
+
|
| 470 |
+
quant_method = get_quantization_config(hf_config)
|
| 471 |
+
quant_resolved = server_args.quantization
|
| 472 |
+
if (
|
| 473 |
+
quant_resolved is None
|
| 474 |
+
and not server_args._quantization_explicitly_unset
|
| 475 |
+
and quant_method is not None
|
| 476 |
+
):
|
| 477 |
+
overrides["quantization"] = quant_method
|
| 478 |
+
quant_resolved = quant_method
|
| 479 |
+
|
| 480 |
+
if is_hip():
|
| 481 |
+
if server_args.is_attention_backend_not_set():
|
| 482 |
+
overrides["attention_backend"] = "triton"
|
| 483 |
+
if server_args.moe_runner_backend == "auto" and quant_resolved == "mxfp8":
|
| 484 |
+
overrides["moe_runner_backend"] = "triton"
|
| 485 |
+
if not envs.USE_ROCM_AITER_ROPE_BACKEND.is_set():
|
| 486 |
+
envs.USE_ROCM_AITER_ROPE_BACKEND.set("0")
|
| 487 |
+
aiter_fusion_resolved = server_args.enable_aiter_allreduce_fusion
|
| 488 |
+
if (
|
| 489 |
+
server_args.ep_size > 1
|
| 490 |
+
and server_args.moe_a2a_backend == "none"
|
| 491 |
+
and aiter_fusion_resolved
|
| 492 |
+
):
|
| 493 |
+
logger.warning(
|
| 494 |
+
"Disable --enable-aiter-allreduce-fusion for MiniMax-M3 "
|
| 495 |
+
"standard EP on ROCm because the deferred fused all-reduce "
|
| 496 |
+
"corrupts sparse MoE partial outputs."
|
| 497 |
+
)
|
| 498 |
+
overrides["enable_aiter_allreduce_fusion"] = False
|
| 499 |
+
aiter_fusion_resolved = False
|
| 500 |
+
if not aiter_fusion_resolved:
|
| 501 |
+
overrides["disable_custom_all_reduce"] = True
|
| 502 |
+
elif is_sm100_supported():
|
| 503 |
+
if server_args.is_attention_backend_not_set():
|
| 504 |
+
overrides["attention_backend"] = "fa4"
|
| 505 |
+
page_resolved = server_args.page_size
|
| 506 |
+
if (
|
| 507 |
+
page_resolved is None
|
| 508 |
+
and overrides.get("attention_backend", server_args.attention_backend)
|
| 509 |
+
== "fa4"
|
| 510 |
+
):
|
| 511 |
+
overrides["page_size"] = 128
|
| 512 |
+
page_resolved = 128
|
| 513 |
+
if server_args.moe_runner_backend == "auto" and quant_resolved == "mxfp8":
|
| 514 |
+
overrides["moe_runner_backend"] = "deep_gemm"
|
| 515 |
+
logger.info(
|
| 516 |
+
"MiniMax-M3 on SM100: attention_backend="
|
| 517 |
+
f"{overrides.get('attention_backend', server_args.attention_backend)}, page_size={page_resolved}, "
|
| 518 |
+
f"moe_runner_backend={overrides.get('moe_runner_backend', server_args.moe_runner_backend)}."
|
| 519 |
+
)
|
| 520 |
+
elif is_sm90_supported():
|
| 521 |
+
if server_args.is_attention_backend_not_set():
|
| 522 |
+
overrides["attention_backend"] = "fa3"
|
| 523 |
+
page_resolved = server_args.page_size
|
| 524 |
+
if (
|
| 525 |
+
page_resolved is None
|
| 526 |
+
and overrides.get("attention_backend", server_args.attention_backend)
|
| 527 |
+
== "fa3"
|
| 528 |
+
):
|
| 529 |
+
overrides["page_size"] = 128
|
| 530 |
+
page_resolved = 128
|
| 531 |
+
logger.info(
|
| 532 |
+
"MiniMax-M3 on Hopper: attention_backend="
|
| 533 |
+
f"{overrides.get('attention_backend', server_args.attention_backend)}, page_size={page_resolved} "
|
| 534 |
+
"(MSA is SM100-only; sparse attention runs on the Triton path)."
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
moe_runner_resolved = overrides.get(
|
| 538 |
+
"moe_runner_backend", server_args.moe_runner_backend
|
| 539 |
+
)
|
| 540 |
+
if quant_resolved is None and moe_runner_resolved in ("auto", "deep_gemm"):
|
| 541 |
+
if moe_runner_resolved == "deep_gemm":
|
| 542 |
+
logger.warning(
|
| 543 |
+
"MiniMax-M3: the deep_gemm MoE runner produces corrupted output "
|
| 544 |
+
"on bf16 full weights; overriding --moe-runner-backend to 'triton'."
|
| 545 |
+
)
|
| 546 |
+
overrides["moe_runner_backend"] = "triton"
|
| 547 |
+
|
| 548 |
+
return overrides
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
@_register_for(
|
| 552 |
+
"Gemma2ForCausalLM",
|
| 553 |
+
"Gemma3ForCausalLM",
|
| 554 |
+
"Gemma3ForConditionalGeneration",
|
| 555 |
+
"Gemma3nForCausalLM",
|
| 556 |
+
"Gemma3nForConditionalGeneration",
|
| 557 |
+
)
|
| 558 |
+
def _gemma2_gemma3_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 559 |
+
# FIXME: https://github.com/sgl-project/sglang/pull/7367 is not compatible with gemma2 model.
|
| 560 |
+
# It failed at this test: https://github.com/sgl-project/sglang/actions/runs/16255155597/job/45890331952#step:4:736
|
| 561 |
+
logger.warning(
|
| 562 |
+
f"Disable hybrid SWA memory for {hf_config.architectures[0]} as it is not yet supported."
|
| 563 |
+
)
|
| 564 |
+
return {"disable_hybrid_swa_memory": True}
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
@_register_for("Exaone4ForCausalLM", "ExaoneMoEForCausalLM")
|
| 568 |
+
def _exaone_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 569 |
+
if hf_config.sliding_window_pattern is not None:
|
| 570 |
+
logger.warning(
|
| 571 |
+
f"Disabling hybrid SWA memory for {hf_config.architectures[0]} as it is not yet supported."
|
| 572 |
+
)
|
| 573 |
+
return {"disable_hybrid_swa_memory": True}
|
| 574 |
+
return {}
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
@_register_for("GptOssForCausalLM")
|
| 578 |
+
def _gpt_oss_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 579 |
+
overrides: Dict[str, Any] = {}
|
| 580 |
+
# Set attention backend for GPT-OSS
|
| 581 |
+
if server_args.is_attention_backend_not_set():
|
| 582 |
+
if is_sm100_supported():
|
| 583 |
+
overrides["attention_backend"] = "trtllm_mha"
|
| 584 |
+
elif is_sm90_supported():
|
| 585 |
+
overrides["attention_backend"] = "fa3"
|
| 586 |
+
elif is_cpu() and cpu_has_amx_support():
|
| 587 |
+
overrides["attention_backend"] = "intel_amx"
|
| 588 |
+
elif is_xpu():
|
| 589 |
+
overrides["attention_backend"] = "intel_xpu"
|
| 590 |
+
elif is_hip():
|
| 591 |
+
overrides["attention_backend"] = "aiter"
|
| 592 |
+
else:
|
| 593 |
+
overrides["attention_backend"] = "triton"
|
| 594 |
+
if is_xpu():
|
| 595 |
+
# Check for bf16 dtype on Intel XPU. Reads the pristine dtype request,
|
| 596 |
+
# which equals the legacy mid-branch read: dtype had no earlier writer
|
| 597 |
+
# for this arch.
|
| 598 |
+
if server_args.dtype == "auto":
|
| 599 |
+
logger.warning(
|
| 600 |
+
"GptOssForCausalLM on Intel XPU currently supports bfloat16 dtype only"
|
| 601 |
+
)
|
| 602 |
+
elif server_args.dtype not in ["bfloat16"]:
|
| 603 |
+
raise NotImplementedError(
|
| 604 |
+
f"GptOssForCausalLM on Intel XPU only supports bfloat16 dtype, "
|
| 605 |
+
f"but got '{server_args.dtype}'. Please use --dtype bfloat16 or remove --dtype to use auto."
|
| 606 |
+
)
|
| 607 |
+
quantization_config = getattr(hf_config, "quantization_config", None)
|
| 608 |
+
is_mxfp4_quant_format = (
|
| 609 |
+
quantization_config is not None
|
| 610 |
+
and quantization_config.get("quant_method") == "mxfp4"
|
| 611 |
+
)
|
| 612 |
+
if is_mxfp4_quant_format:
|
| 613 |
+
# use bf16 for mxfp4 triton kernels
|
| 614 |
+
overrides["dtype"] = "bfloat16"
|
| 615 |
+
if server_args.moe_runner_backend == "auto":
|
| 616 |
+
|
| 617 |
+
if is_sm100_supported() and is_mxfp4_quant_format:
|
| 618 |
+
overrides["moe_runner_backend"] = "flashinfer_mxfp4"
|
| 619 |
+
logger.warning(
|
| 620 |
+
"Detected SM100 and MXFP4 quantization format for GPT-OSS model, enabling FlashInfer MXFP4 MOE kernel."
|
| 621 |
+
)
|
| 622 |
+
elif is_sm120_supported() and is_mxfp4_quant_format:
|
| 623 |
+
# trtllm-gen only supports SM100
|
| 624 |
+
overrides["moe_runner_backend"] = "marlin"
|
| 625 |
+
logger.warning(
|
| 626 |
+
"Detected SM120 and MXFP4 quantization format for GPT-OSS model, enabling Marlin MOE kernel."
|
| 627 |
+
)
|
| 628 |
+
elif (is_hip() and envs.SGLANG_USE_AITER.get()) and is_mxfp4_quant_format:
|
| 629 |
+
overrides["moe_runner_backend"] = "auto"
|
| 630 |
+
logger.warning(
|
| 631 |
+
"Detected ROCm and MXFP4 quantization format for GPT-OSS model, enabling aiter MXFP4 MOE kernel."
|
| 632 |
+
)
|
| 633 |
+
## The AITER MXFP4 fused-MoE path for GPT-OSS expects the
|
| 634 |
+
## SEPARATED gate/up tile layout (matches the
|
| 635 |
+
## `gptoss_fp4_tuned_fmoe.csv` flydsl entries and the
|
| 636 |
+
## Mxfp4MoEMethod weight shuffle). Other AITER MXFP4
|
| 637 |
+
## callers default to INTERLEAVE; opt this path out
|
| 638 |
+
## unless the user explicitly overrode it.
|
| 639 |
+
# envs.SGLANG_USE_AITER_MOE_GU_ITLV.set(False)
|
| 640 |
+
elif is_hip() and envs.SGLANG_USE_AITER.get():
|
| 641 |
+
# For GPT-OSS bf16 on ROCm with aiter, use triton backend
|
| 642 |
+
# because aiter CK kernel doesn't support all GEMM dimensions
|
| 643 |
+
overrides["moe_runner_backend"] = "triton"
|
| 644 |
+
logger.warning(
|
| 645 |
+
"Detected ROCm with SGLANG_USE_AITER for GPT-OSS bf16 model, using triton MOE kernel."
|
| 646 |
+
)
|
| 647 |
+
elif is_musa() and envs.SGLANG_DEEPEP_BF16_DISPATCH.get():
|
| 648 |
+
overrides["moe_runner_backend"] = "deep_gemm"
|
| 649 |
+
logger.warning(
|
| 650 |
+
"Detected MUSA with SGLANG_DEEPEP_BF16_DISPATCH for bf16 model, using deep_gemm kernel."
|
| 651 |
+
)
|
| 652 |
+
elif (
|
| 653 |
+
server_args.ep_size == 1
|
| 654 |
+
and is_triton_kernels_available()
|
| 655 |
+
and server_args.quantization is None
|
| 656 |
+
and not (is_cpu() and cpu_has_amx_support())
|
| 657 |
+
):
|
| 658 |
+
# The triton_kernels package segfaults on Blackwell (B200)
|
| 659 |
+
# with NVIDIA driver >= 595. Fall back to triton backend.
|
| 660 |
+
if is_blackwell_supported() and get_nvidia_driver_version() >= (595,):
|
| 661 |
+
overrides["moe_runner_backend"] = "triton"
|
| 662 |
+
logger.warning(
|
| 663 |
+
"Detected GPT-OSS model on Blackwell with driver >= 595, "
|
| 664 |
+
"using triton MOE kernel to avoid triton_kernels SIGSEGV."
|
| 665 |
+
)
|
| 666 |
+
else:
|
| 667 |
+
overrides["moe_runner_backend"] = "triton_kernel"
|
| 668 |
+
logger.warning(
|
| 669 |
+
"Detected GPT-OSS model, enabling triton_kernels MOE kernel."
|
| 670 |
+
)
|
| 671 |
+
return overrides
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
# Keep in sync with LLAMA4_MODEL_ARCHS (server_args.py).
|
| 675 |
+
@_register_for("Llama4ForConditionalGeneration", "Llama4ForCausalLM")
|
| 676 |
+
def _llama4_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 677 |
+
if server_args.device == "cpu":
|
| 678 |
+
return {}
|
| 679 |
+
overrides: Dict[str, Any] = {}
|
| 680 |
+
# Auto-select attention backend for Llama4 if not specified
|
| 681 |
+
if server_args.attention_backend is None:
|
| 682 |
+
if is_sm100_supported():
|
| 683 |
+
backend, platform = "trtllm_mha", "sm100"
|
| 684 |
+
elif is_sm90_supported():
|
| 685 |
+
backend, platform = "fa3", "sm90"
|
| 686 |
+
elif is_hip():
|
| 687 |
+
backend, platform = "aiter", "hip"
|
| 688 |
+
elif server_args.device == "xpu":
|
| 689 |
+
backend, platform = "intel_xpu", "xpu"
|
| 690 |
+
else:
|
| 691 |
+
backend, platform = "triton", "other platforms"
|
| 692 |
+
logger.warning(
|
| 693 |
+
f"Use {backend} as attention backend on {platform} for Llama4 model"
|
| 694 |
+
)
|
| 695 |
+
overrides["attention_backend"] = backend
|
| 696 |
+
if is_sm100_supported() and server_args.moe_runner_backend == "auto":
|
| 697 |
+
if server_args.quantization in {"fp8", "modelopt_fp8"}:
|
| 698 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 699 |
+
logger.info(
|
| 700 |
+
"Use flashinfer_trtllm as MoE runner backend on SM100 for Llama4"
|
| 701 |
+
)
|
| 702 |
+
return overrides
|
| 703 |
+
|
| 704 |
+
|
| 705 |
+
@_register_for(
|
| 706 |
+
"Gemma4ForConditionalGeneration",
|
| 707 |
+
"Gemma4ForCausalLM",
|
| 708 |
+
"Gemma4UnifiedForConditionalGeneration",
|
| 709 |
+
)
|
| 710 |
+
def _gemma4_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 711 |
+
overrides: Dict[str, Any] = {}
|
| 712 |
+
default_attention_backend = "trtllm_mha" if is_sm100_supported() else "triton"
|
| 713 |
+
if server_args.is_attention_backend_not_set():
|
| 714 |
+
logger.info(
|
| 715 |
+
f"Use {default_attention_backend} as default attention backend for Gemma4"
|
| 716 |
+
)
|
| 717 |
+
overrides["attention_backend"] = default_attention_backend
|
| 718 |
+
# If only one split backend is set, keep the other side on a
|
| 719 |
+
# Gemma4-compatible fallback instead of letting generic backend selection
|
| 720 |
+
# choose an unsupported backend later.
|
| 721 |
+
elif server_args.attention_backend is None:
|
| 722 |
+
overrides["attention_backend"] = default_attention_backend
|
| 723 |
+
if is_sm100_supported() and server_args.moe_runner_backend == "auto":
|
| 724 |
+
if server_args.get_model_config().quantization == "modelopt_fp4":
|
| 725 |
+
overrides["quantization"] = "modelopt_fp4"
|
| 726 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 727 |
+
logger.info(
|
| 728 |
+
"Use flashinfer_trtllm as MoE runner backend on "
|
| 729 |
+
"SM100 for Gemma-4 (modelopt_fp4)"
|
| 730 |
+
)
|
| 731 |
+
return overrides
|
| 732 |
+
|
| 733 |
+
|
| 734 |
+
@_register_for("MossVLForConditionalGeneration")
|
| 735 |
+
def _moss_vl_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 736 |
+
overrides: Dict[str, Any] = {}
|
| 737 |
+
if server_args.is_attention_backend_not_set():
|
| 738 |
+
overrides["prefill_attention_backend"] = "flashinfer"
|
| 739 |
+
logger.info("Use flashinfer as default prefill attention backend for Moss-VL")
|
| 740 |
+
prefill_backend = (
|
| 741 |
+
overrides.get("prefill_attention_backend")
|
| 742 |
+
or server_args.get_attention_backends()[0]
|
| 743 |
+
)
|
| 744 |
+
assert prefill_backend == "flashinfer", (
|
| 745 |
+
"MossVLForConditionalGeneration requires flashinfer prefill "
|
| 746 |
+
"attention backend for cross-attention custom mask support."
|
| 747 |
+
)
|
| 748 |
+
return overrides
|
| 749 |
+
|
| 750 |
+
|
| 751 |
+
@_register_for("MiniCPMV4_6ForConditionalGeneration")
|
| 752 |
+
def _minicpm_v4_6_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 753 |
+
if is_sm100_supported() and server_args.attention_backend is None:
|
| 754 |
+
return {"attention_backend": "triton"}
|
| 755 |
+
return {}
|
| 756 |
+
|
| 757 |
+
|
| 758 |
+
@_register_for(
|
| 759 |
+
"FalconH1ForCausalLM", "JetNemotronForCausalLM", "JetVLMForConditionalGeneration"
|
| 760 |
+
)
|
| 761 |
+
def _falcon_h1_jet_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 762 |
+
if is_sm100_supported() and server_args.attention_backend is None:
|
| 763 |
+
return {"attention_backend": "triton"}
|
| 764 |
+
return {}
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
@_register_for("GraniteMoeHybridForCausalLM")
|
| 768 |
+
def _granite_moe_hybrid_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 769 |
+
has_mamba = any(
|
| 770 |
+
layer_type == "mamba" for layer_type in getattr(hf_config, "layer_types", [])
|
| 771 |
+
)
|
| 772 |
+
if has_mamba and is_sm100_supported() and server_args.attention_backend is None:
|
| 773 |
+
return {"attention_backend": "flashinfer"}
|
| 774 |
+
return {}
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
@_register_for("Lfm2ForCausalLM")
|
| 778 |
+
def _lfm2_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 779 |
+
if is_sm100_supported() and server_args.attention_backend is None:
|
| 780 |
+
return {"attention_backend": "flashinfer"}
|
| 781 |
+
return {}
|
| 782 |
+
|
| 783 |
+
|
| 784 |
+
@_register_for("AgnesForCausalLM")
|
| 785 |
+
def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 786 |
+
"""DeepSeek V4 attention/page/window/MoE-runner defaults (from
|
| 787 |
+
arg_groups/deepseek_v4_hook.py). The kv-cache dtype and NPU split-backend
|
| 788 |
+
writes, the max_running_requests fill and the validations stay in the
|
| 789 |
+
hook at its legacy slot."""
|
| 790 |
+
from sglang.srt.server_args import ServerArgs
|
| 791 |
+
|
| 792 |
+
model_arch = hf_config.architectures[0]
|
| 793 |
+
overrides: Dict[str, Any] = {"attention_backend": "dsv4"}
|
| 794 |
+
|
| 795 |
+
page_size = 256
|
| 796 |
+
if server_args.device == "npu":
|
| 797 |
+
# NPU keeps the device-aware "dsv4" backend (the registry routes it to
|
| 798 |
+
# the Ascend V4 subclass); only the pool geometry / dtype differ.
|
| 799 |
+
# set_default_server_args() pins all three backends to "ascend" for
|
| 800 |
+
# generic NPU models; override that here so V4 stays consistently on
|
| 801 |
+
# dsv4.
|
| 802 |
+
page_size = 128
|
| 803 |
+
overrides["prefill_attention_backend"] = "dsv4"
|
| 804 |
+
overrides["decode_attention_backend"] = "dsv4"
|
| 805 |
+
overrides["page_size"] = page_size
|
| 806 |
+
logger.info(
|
| 807 |
+
f"Use dsv4 attention backend for {model_arch}, setting page_size to {page_size}."
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
if server_args.swa_full_tokens_ratio == ServerArgs.swa_full_tokens_ratio:
|
| 811 |
+
overrides["swa_full_tokens_ratio"] = 0.1
|
| 812 |
+
logger.info(f"Setting swa_full_tokens_ratio to 0.1 for {model_arch}.")
|
| 813 |
+
|
| 814 |
+
# nvidia/DeepSeek-V4-Pro-NVFP4 uses flashinfer_trtllm_routed MoE runner backend.
|
| 815 |
+
if (
|
| 816 |
+
server_args.moe_runner_backend == "auto"
|
| 817 |
+
and server_args.get_model_config().nvfp4_moe_meta is not None
|
| 818 |
+
):
|
| 819 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm_routed"
|
| 820 |
+
logger.info(
|
| 821 |
+
"Use flashinfer_trtllm_routed as MoE runner backend for "
|
| 822 |
+
f"{model_arch} hybrid FP8+NVFP4 checkpoint."
|
| 823 |
+
)
|
| 824 |
+
return overrides
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
@_register_for(
|
| 828 |
+
"InklingForConditionalGeneration",
|
| 829 |
+
"InklingForConditionalGenerationMTP",
|
| 830 |
+
)
|
| 831 |
+
def _inkling_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 832 |
+
"""Inkling architecture defaults: SWA / mamba KV-pool ratios tuned for the
|
| 833 |
+
hybrid-SWA layout, the extra-buffer mamba strategy, and the unified radix
|
| 834 |
+
tree (which Inkling requires — models/inkling.py asserts it). The full-graph
|
| 835 |
+
prefill default is set separately (inline, before cuda-graph resolution) —
|
| 836 |
+
see ServerArgs.__post_init__ / _apply_inkling_prefill_cuda_graph_default. The
|
| 837 |
+
server-arg defaults each yield to an explicit user value (compared against
|
| 838 |
+
the ServerArgs class default); the prefill declaration is materialized
|
| 839 |
+
before _parse_cuda_graph_config folds cuda_graph_backend_prefill into
|
| 840 |
+
prefill.backend, and an explicit --cuda-graph-backend-prefill /
|
| 841 |
+
--disable-prefill-cuda-graph still wins. The unified-radix env write follows
|
| 842 |
+
the MiniMax-M3 handler precedent (env is not a resolvable server-arg)."""
|
| 843 |
+
from sglang.srt.server_args import ServerArgs
|
| 844 |
+
|
| 845 |
+
overrides: Dict[str, Any] = {}
|
| 846 |
+
# NOTE: the full-graph prefill default is NOT set here. cuda-graph config is
|
| 847 |
+
# resolved in __post_init__ before declarations are materialized, so a
|
| 848 |
+
# cuda_graph_backend_prefill declared here lands too late (the breakable
|
| 849 |
+
# default would already have been auto-disabled for this multimodal arch).
|
| 850 |
+
# It is set inline before _handle_cuda_graph_config instead.
|
| 851 |
+
if server_args.swa_full_tokens_ratio == ServerArgs.swa_full_tokens_ratio:
|
| 852 |
+
overrides["swa_full_tokens_ratio"] = 0.1
|
| 853 |
+
if server_args.mamba_full_memory_ratio == ServerArgs.mamba_full_memory_ratio:
|
| 854 |
+
overrides["mamba_full_memory_ratio"] = 0.1
|
| 855 |
+
# Inkling requires the extra-buffer mamba strategy (inkling.py asserts
|
| 856 |
+
# enable_mamba_extra_buffer()); the generic "auto" resolution does not cover
|
| 857 |
+
# Inkling, so pin it here. Yields to an explicit --mamba-scheduler-strategy.
|
| 858 |
+
if server_args.mamba_radix_cache_strategy == ServerArgs.mamba_radix_cache_strategy:
|
| 859 |
+
overrides["mamba_radix_cache_strategy"] = "extra_buffer"
|
| 860 |
+
# Inkling attention runs only on the fa4 (Blackwell) or triton backends --
|
| 861 |
+
# models/inkling_common/attn.py asserts attention_backend in {fa4, triton}.
|
| 862 |
+
# The generic resolver would otherwise pick trtllm_mha (SM100) / fa3
|
| 863 |
+
# (Hopper), so a bare launch fails on the first attention forward. Pin a
|
| 864 |
+
# supported default when the user left every attention-backend flag unset
|
| 865 |
+
# (mirrors the MiniMax-M3 SM100 fa4-default above); an explicit
|
| 866 |
+
# --attention-backend / --prefill/decode-attention-backend still wins.
|
| 867 |
+
if server_args.is_attention_backend_not_set():
|
| 868 |
+
inkling_attn_backend = "fa4" if is_sm100_supported() else "triton"
|
| 869 |
+
overrides["attention_backend"] = inkling_attn_backend
|
| 870 |
+
logger.info(
|
| 871 |
+
f"Use {inkling_attn_backend} as the attention backend for Inkling "
|
| 872 |
+
"(requires fa4 or triton)."
|
| 873 |
+
)
|
| 874 |
+
envs.SGLANG_ENABLE_UNIFIED_RADIX_TREE.set(True)
|
| 875 |
+
return overrides
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
@_register_for("NemotronHForCausalLM", "NemotronHPuzzleForCausalLM")
|
| 879 |
+
def _nemotron_h_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 880 |
+
"""NemotronH quantization / MoE runner / attention backend defaults
|
| 881 |
+
(absorbed from the retired arg_groups/nemotron_h_hook.py; the mamba radix
|
| 882 |
+
cache handling and the triton-backend assert stay in the arch branch)."""
|
| 883 |
+
model_arch = hf_config.architectures[0]
|
| 884 |
+
model_config = server_args.get_model_config()
|
| 885 |
+
overrides: Dict[str, Any] = {}
|
| 886 |
+
|
| 887 |
+
is_modelopt = model_config.quantization in [
|
| 888 |
+
"modelopt",
|
| 889 |
+
"modelopt_fp8",
|
| 890 |
+
"modelopt_fp4",
|
| 891 |
+
"modelopt_mixed",
|
| 892 |
+
]
|
| 893 |
+
quantization = server_args.quantization
|
| 894 |
+
if is_modelopt:
|
| 895 |
+
assert model_config.hf_config.mlp_hidden_act == "relu2"
|
| 896 |
+
if model_config.quantization == "modelopt":
|
| 897 |
+
quant_algo = model_config.hf_config.quantization_config["quant_algo"]
|
| 898 |
+
if quant_algo == "MIXED_PRECISION":
|
| 899 |
+
quantization = "modelopt_mixed"
|
| 900 |
+
else:
|
| 901 |
+
quantization = (
|
| 902 |
+
"modelopt_fp4" if quant_algo == "NVFP4" else "modelopt_fp8"
|
| 903 |
+
)
|
| 904 |
+
else:
|
| 905 |
+
quantization = model_config.quantization
|
| 906 |
+
overrides["quantization"] = quantization
|
| 907 |
+
|
| 908 |
+
if (is_modelopt or model_config.quantization is None) and (
|
| 909 |
+
server_args.moe_runner_backend == "auto"
|
| 910 |
+
):
|
| 911 |
+
if is_sm100_supported() and server_args.moe_a2a_backend == "none":
|
| 912 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 913 |
+
logger.info(
|
| 914 |
+
f"Use flashinfer_trtllm as MoE runner backend on sm100 for {model_arch}"
|
| 915 |
+
)
|
| 916 |
+
elif (
|
| 917 |
+
(
|
| 918 |
+
model_config.quantization in ("modelopt_fp4", "modelopt_mixed")
|
| 919 |
+
or quantization == "modelopt_fp4"
|
| 920 |
+
)
|
| 921 |
+
and is_cuda()
|
| 922 |
+
and (8, 0) <= get_device_capability() < (10, 0)
|
| 923 |
+
):
|
| 924 |
+
overrides["moe_runner_backend"] = "marlin"
|
| 925 |
+
logger.info(
|
| 926 |
+
"Use marlin as MoE runner backend on SM80-SM90 for "
|
| 927 |
+
f"{model_arch} {model_config.quantization}"
|
| 928 |
+
)
|
| 929 |
+
else:
|
| 930 |
+
overrides["moe_runner_backend"] = "flashinfer_cutlass"
|
| 931 |
+
|
| 932 |
+
if is_sm100_supported() and server_args.attention_backend is None:
|
| 933 |
+
overrides["attention_backend"] = "flashinfer"
|
| 934 |
+
return overrides
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
@_register_for(
|
| 938 |
+
"Qwen3NextForCausalLM",
|
| 939 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 940 |
+
"InternS2PreviewForConditionalGeneration",
|
| 941 |
+
"Qwen3_5ForConditionalGeneration",
|
| 942 |
+
)
|
| 943 |
+
def _qwen3_5_hybrid_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 944 |
+
if not is_sm100_supported() or server_args.attention_backend is not None:
|
| 945 |
+
return {}
|
| 946 |
+
sm100_default_attn_backend = "triton"
|
| 947 |
+
# trtllm_mha requires speculative_eagle_topk == 1 and page_size > 1.
|
| 948 |
+
# _get_default_attn_backend handles the eagle_topk check.
|
| 949 |
+
# There is only one case where page_size=1 is required,
|
| 950 |
+
# which is when radix cache is enabled and both extra_buffer
|
| 951 |
+
# and spec decoding are disabled.
|
| 952 |
+
default_attn_backend = server_args._get_default_attn_backend(
|
| 953 |
+
use_mla_backend=server_args.use_mla_backend(),
|
| 954 |
+
model_config=server_args.get_model_config(),
|
| 955 |
+
)
|
| 956 |
+
# The mamba radix-cache pass runs before this dispatch: read the
|
| 957 |
+
# declared strategy through the view (the legacy branch observed the
|
| 958 |
+
# already-written field here).
|
| 959 |
+
if default_attn_backend == "trtllm_mha" and not (
|
| 960 |
+
not mamba_extra_buffer_of(resolved_view(server_args))
|
| 961 |
+
and not server_args.disable_radix_cache
|
| 962 |
+
and server_args.speculative_algorithm is None
|
| 963 |
+
):
|
| 964 |
+
sm100_default_attn_backend = "trtllm_mha"
|
| 965 |
+
return {
|
| 966 |
+
"attention_backend": sm100_default_attn_backend,
|
| 967 |
+
"page_size": 64 if sm100_default_attn_backend == "trtllm_mha" else 1,
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
|
| 971 |
+
@_register_for("Qwen3VLForConditionalGeneration")
|
| 972 |
+
def _qwen3vl_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 973 |
+
|
| 974 |
+
if (
|
| 975 |
+
is_hip()
|
| 976 |
+
and envs.SGLANG_USE_AITER_UNIFIED_ATTN.get()
|
| 977 |
+
and server_args.page_size is None
|
| 978 |
+
):
|
| 979 |
+
logger.info(
|
| 980 |
+
"Setting page_size=16 for aiter unified attention on Qwen3VLForConditionalGeneration."
|
| 981 |
+
)
|
| 982 |
+
return {"page_size": 16}
|
| 983 |
+
return {}
|
| 984 |
+
|
| 985 |
+
|
| 986 |
+
@_register_for(
|
| 987 |
+
"Qwen3MoeForCausalLM",
|
| 988 |
+
"Qwen3VLMoeForConditionalGeneration",
|
| 989 |
+
"Qwen3NextForCausalLM",
|
| 990 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 991 |
+
"InternS2PreviewForConditionalGeneration",
|
| 992 |
+
"Qwen3_5ForConditionalGeneration",
|
| 993 |
+
)
|
| 994 |
+
def _qwen3_moe_family_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 995 |
+
overrides: Dict[str, Any] = {}
|
| 996 |
+
if is_sm100_supported():
|
| 997 |
+
quant_method = get_quantization_config(hf_config)
|
| 998 |
+
quantization = server_args.quantization
|
| 999 |
+
if (
|
| 1000 |
+
quantization is None
|
| 1001 |
+
and not server_args._quantization_explicitly_unset
|
| 1002 |
+
and quant_method is not None
|
| 1003 |
+
):
|
| 1004 |
+
overrides["quantization"] = quant_method
|
| 1005 |
+
quantization = quant_method
|
| 1006 |
+
if (
|
| 1007 |
+
(quantization in ("fp8", "modelopt_fp4") or quantization is None)
|
| 1008 |
+
and server_args.moe_a2a_backend == "none"
|
| 1009 |
+
and server_args.moe_runner_backend == "auto"
|
| 1010 |
+
):
|
| 1011 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 1012 |
+
logger.info(
|
| 1013 |
+
"Use flashinfer_trtllm as MoE runner backend on sm100 for "
|
| 1014 |
+
f"{hf_config.architectures[0]}"
|
| 1015 |
+
)
|
| 1016 |
+
return overrides
|
| 1017 |
+
|
| 1018 |
+
|
| 1019 |
+
@_register_for("Glm4MoeForCausalLM")
|
| 1020 |
+
def _glm4_moe_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 1021 |
+
overrides: Dict[str, Any] = {}
|
| 1022 |
+
if is_sm100_supported():
|
| 1023 |
+
quantization_config = getattr(hf_config, "quantization_config", None)
|
| 1024 |
+
quant_method = (
|
| 1025 |
+
quantization_config.get("quant_method")
|
| 1026 |
+
if quantization_config is not None
|
| 1027 |
+
else None
|
| 1028 |
+
)
|
| 1029 |
+
quantization = server_args.quantization
|
| 1030 |
+
if (
|
| 1031 |
+
quantization is None
|
| 1032 |
+
and not server_args._quantization_explicitly_unset
|
| 1033 |
+
and quant_method is not None
|
| 1034 |
+
):
|
| 1035 |
+
overrides["quantization"] = quant_method
|
| 1036 |
+
quantization = quant_method
|
| 1037 |
+
if (
|
| 1038 |
+
quantization in {"modelopt_fp4", None}
|
| 1039 |
+
and server_args.moe_a2a_backend == "none"
|
| 1040 |
+
and server_args.moe_runner_backend == "auto"
|
| 1041 |
+
):
|
| 1042 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 1043 |
+
logger.info(
|
| 1044 |
+
"Use flashinfer_trtllm as MoE runner backend on sm100 for Glm4MoeForCausalLM"
|
| 1045 |
+
)
|
| 1046 |
+
logger.info(
|
| 1047 |
+
"Enable TF32 matmul for Glm4MoeForCausalLM model to improve gate gemm performance."
|
| 1048 |
+
)
|
| 1049 |
+
overrides["enable_tf32_matmul"] = True
|
| 1050 |
+
return overrides
|
| 1051 |
+
|
| 1052 |
+
|
| 1053 |
+
@_register_for("Olmo2ForCausalLM")
|
| 1054 |
+
def _olmo2_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 1055 |
+
overrides: Dict[str, Any] = {}
|
| 1056 |
+
# FIXME: https://github.com/sgl-project/sglang/pull/7367 is not compatible with Olmo3 model.
|
| 1057 |
+
logger.warning(
|
| 1058 |
+
f"Disabling hybrid SWA memory for {hf_config.architectures[0]} as it is not yet supported."
|
| 1059 |
+
)
|
| 1060 |
+
overrides["disable_hybrid_swa_memory"] = True
|
| 1061 |
+
if server_args.attention_backend is None:
|
| 1062 |
+
if is_cuda() and is_sm100_supported():
|
| 1063 |
+
overrides["attention_backend"] = "trtllm_mha"
|
| 1064 |
+
elif is_cuda() and get_device_sm() >= 80:
|
| 1065 |
+
overrides["attention_backend"] = "fa3"
|
| 1066 |
+
else:
|
| 1067 |
+
overrides["attention_backend"] = "triton"
|
| 1068 |
+
return overrides
|
| 1069 |
+
|
| 1070 |
+
|
| 1071 |
+
@register_model_override_predicate(
|
| 1072 |
+
lambda arch: "Step3p5ForCausalLM" in arch
|
| 1073 |
+
or "Step3p7ForConditionalGeneration" in arch
|
| 1074 |
+
)
|
| 1075 |
+
def _step3p_overrides(server_args: Any, hf_config: Any) -> dict:
|
| 1076 |
+
overrides: Dict[str, Any] = {}
|
| 1077 |
+
if server_args.is_attention_backend_not_set():
|
| 1078 |
+
if is_blackwell_supported():
|
| 1079 |
+
logger.info("Auto-select fa4 attention backend for Step3p7 on Blackwell.")
|
| 1080 |
+
overrides["attention_backend"] = "fa4"
|
| 1081 |
+
elif is_sm90_supported():
|
| 1082 |
+
logger.info("Auto-select fa3 attention backend for Step3p7 on Hopper.")
|
| 1083 |
+
overrides["attention_backend"] = "fa3"
|
| 1084 |
+
if server_args.speculative_algorithm == "EAGLE":
|
| 1085 |
+
logger.info(
|
| 1086 |
+
"Enable multi-layer EAGLE speculative decoding for Step3p5ForCausalLM model."
|
| 1087 |
+
)
|
| 1088 |
+
overrides["enable_multi_layer_eagle"] = True
|
| 1089 |
+
if server_args.enable_hierarchical_cache:
|
| 1090 |
+
logger.warning(
|
| 1091 |
+
"Reset swa_full_tokens_ratio to 1.0 for Step3p5ForCausalLM model with hierarchical cache"
|
| 1092 |
+
)
|
| 1093 |
+
overrides["swa_full_tokens_ratio"] = 1.0
|
| 1094 |
+
logger.warning(
|
| 1095 |
+
"Disable hybrid SWA memory for Step3p5ForCausalLM model with hierarchical cache"
|
| 1096 |
+
)
|
| 1097 |
+
overrides["disable_hybrid_swa_memory"] = True
|
| 1098 |
+
return overrides
|
| 1099 |
+
|
| 1100 |
+
|
| 1101 |
+
# ---------------------------------------------------------------------------
|
| 1102 |
+
# Post-process passes (normalization stage), in end-state execution order.
|
| 1103 |
+
# Faithful ports of the legacy __post_init__ handlers; each is invoked from
|
| 1104 |
+
# its legacy slot via run_post_process_pass during the transition.
|
| 1105 |
+
# ---------------------------------------------------------------------------
|
| 1106 |
+
|
| 1107 |
+
|
| 1108 |
+
# Architectures whose monolith branch routes through the mamba radix cache
|
| 1109 |
+
# handling (hybrid linear-attention models). Keep in sync with the branch
|
| 1110 |
+
# guards in _handle_model_specific_adjustments.
|
| 1111 |
+
_MAMBA_RADIX_CACHE_ARCHS = frozenset(
|
| 1112 |
+
{
|
| 1113 |
+
"KimiLinearForCausalLM",
|
| 1114 |
+
"BailingMoeV2_5ForCausalLM",
|
| 1115 |
+
"Qwen3NextForCausalLM",
|
| 1116 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 1117 |
+
"InternS2PreviewForConditionalGeneration",
|
| 1118 |
+
"Qwen3_5ForConditionalGeneration",
|
| 1119 |
+
"MiniCPMV4_6ForConditionalGeneration",
|
| 1120 |
+
"NemotronHForCausalLM",
|
| 1121 |
+
"NemotronHPuzzleForCausalLM",
|
| 1122 |
+
"FalconH1ForCausalLM",
|
| 1123 |
+
"JetNemotronForCausalLM",
|
| 1124 |
+
"JetVLMForConditionalGeneration",
|
| 1125 |
+
"Lfm2ForCausalLM",
|
| 1126 |
+
"ZayaForCausalLM",
|
| 1127 |
+
}
|
| 1128 |
+
)
|
| 1129 |
+
|
| 1130 |
+
# Architectures that support the extra_buffer mamba radix cache strategy.
|
| 1131 |
+
# Single source of truth: ServerArgs._support_mamba_cache_extra_buffer
|
| 1132 |
+
# delegates here.
|
| 1133 |
+
_MAMBA_EXTRA_BUFFER_ARCHS = frozenset(
|
| 1134 |
+
{
|
| 1135 |
+
"KimiLinearForCausalLM",
|
| 1136 |
+
"Qwen3_5ForConditionalGeneration",
|
| 1137 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 1138 |
+
"Qwen3NextForCausalLM",
|
| 1139 |
+
"InternS2PreviewForConditionalGeneration",
|
| 1140 |
+
"MiniCPMV4_6ForConditionalGeneration",
|
| 1141 |
+
"BailingMoeV2_5ForCausalLM",
|
| 1142 |
+
"FalconH1ForCausalLM",
|
| 1143 |
+
"GraniteMoeHybridForCausalLM",
|
| 1144 |
+
"NemotronHForCausalLM",
|
| 1145 |
+
"NemotronHPuzzleForCausalLM",
|
| 1146 |
+
}
|
| 1147 |
+
)
|
| 1148 |
+
|
| 1149 |
+
|
| 1150 |
+
def supports_mamba_cache_extra_buffer(view: Any, model_arch: str) -> bool:
|
| 1151 |
+
"""Whether ``model_arch`` supports the extra_buffer strategy on the
|
| 1152 |
+
configured linear-attention backend (pure read)."""
|
| 1153 |
+
if model_arch in _MAMBA_EXTRA_BUFFER_ARCHS:
|
| 1154 |
+
return view.linear_attn_backend == "triton"
|
| 1155 |
+
return False
|
| 1156 |
+
|
| 1157 |
+
|
| 1158 |
+
@register_post_process
|
| 1159 |
+
def _mamba_radix_cache_resolution(view: Any) -> dict:
|
| 1160 |
+
"""Resolve the hybrid-mamba radix cache fields (pure).
|
| 1161 |
+
|
| 1162 |
+
Slot pass: invoked at each legacy ``_handle_mamba_radix_cache`` slot —
|
| 1163 |
+
the hybrid-spec call at the head of the monolith and the per-arch branch
|
| 1164 |
+
calls — where it reads the mid-resolution ``page_size`` /
|
| 1165 |
+
``disable_overlap_schedule`` exactly as the legacy helper did. The arch
|
| 1166 |
+
guard replicates the union of the legacy call-site guards so the pass is
|
| 1167 |
+
self-sufficient in the end-state pass list.
|
| 1168 |
+
"""
|
| 1169 |
+
from sglang.srt.configs.linear_attn_model_registry import (
|
| 1170 |
+
get_linear_attn_spec_by_arch,
|
| 1171 |
+
)
|
| 1172 |
+
|
| 1173 |
+
hf_config = view.get_model_config().hf_config
|
| 1174 |
+
model_arch = hf_config.architectures[0]
|
| 1175 |
+
|
| 1176 |
+
in_branch = model_arch in _MAMBA_RADIX_CACHE_ARCHS
|
| 1177 |
+
if model_arch == "GraniteMoeHybridForCausalLM":
|
| 1178 |
+
in_branch = any(
|
| 1179 |
+
layer_type == "mamba"
|
| 1180 |
+
for layer_type in getattr(hf_config, "layer_types", [])
|
| 1181 |
+
)
|
| 1182 |
+
spec = get_linear_attn_spec_by_arch(model_arch)
|
| 1183 |
+
if not ((spec is not None and spec.uses_mamba_radix_cache) or in_branch):
|
| 1184 |
+
return {}
|
| 1185 |
+
|
| 1186 |
+
if view.disable_radix_cache:
|
| 1187 |
+
return {}
|
| 1188 |
+
|
| 1189 |
+
declared: Dict[str, Any] = {"uses_mamba_radix_cache": True}
|
| 1190 |
+
if view.mamba_radix_cache_strategy == "auto":
|
| 1191 |
+
wants_overlap = not view.disable_overlap_schedule
|
| 1192 |
+
wants_paging = view.page_size is not None and view.page_size > 1
|
| 1193 |
+
if (wants_overlap or wants_paging) and supports_mamba_cache_extra_buffer(
|
| 1194 |
+
view, model_arch
|
| 1195 |
+
):
|
| 1196 |
+
declared["mamba_radix_cache_strategy"] = "extra_buffer"
|
| 1197 |
+
else:
|
| 1198 |
+
declared["mamba_radix_cache_strategy"] = "no_buffer"
|
| 1199 |
+
declared["disable_overlap_schedule"] = True
|
| 1200 |
+
return declared
|
| 1201 |
+
|
| 1202 |
+
|
| 1203 |
+
@register_post_process
|
| 1204 |
+
def _dsa_kv_cache_dtype_default(view: Any) -> dict:
|
| 1205 |
+
"""Slot pass in the DSA arm, ordered before the split-backend
|
| 1206 |
+
resolution: default the kv-cache dtype from the device capability
|
| 1207 |
+
(Blackwell FP8, Hopper bf16) and normalize the bf16 alias. Reads the
|
| 1208 |
+
PRISTINE dsa split backends (their resolution runs after this pass)."""
|
| 1209 |
+
from sglang.srt.configs.model_config import is_deepseek_dsa
|
| 1210 |
+
|
| 1211 |
+
hf_config = view.get_model_config().hf_config
|
| 1212 |
+
if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS:
|
| 1213 |
+
return {}
|
| 1214 |
+
if not is_deepseek_dsa(hf_config):
|
| 1215 |
+
return {}
|
| 1216 |
+
if is_npu() or is_xpu():
|
| 1217 |
+
return {}
|
| 1218 |
+
|
| 1219 |
+
import torch
|
| 1220 |
+
|
| 1221 |
+
major, _ = torch.cuda.get_device_capability()
|
| 1222 |
+
|
| 1223 |
+
# If user specified a backend but didn't explicitly set kv_cache_dtype,
|
| 1224 |
+
# suggest them to be explicit about kv_cache_dtype to avoid surprises
|
| 1225 |
+
if (
|
| 1226 |
+
view.dsa_prefill_backend is not None or view.dsa_decode_backend is not None
|
| 1227 |
+
) and view.kv_cache_dtype == "auto":
|
| 1228 |
+
logger.warning(
|
| 1229 |
+
"When specifying --dsa-prefill-backend or --dsa-decode-backend, "
|
| 1230 |
+
"you should also explicitly set --kv-cache-dtype (e.g., 'fp8_e4m3' or 'bfloat16'). "
|
| 1231 |
+
"DeepSeek V3.2 defaults to FP8 KV cache which may not be compatible with all backends."
|
| 1232 |
+
)
|
| 1233 |
+
|
| 1234 |
+
kv_cache_dtype = view.kv_cache_dtype
|
| 1235 |
+
if kv_cache_dtype == "auto":
|
| 1236 |
+
kv_cache_dtype = "fp8_e4m3" if major >= 10 else "bfloat16"
|
| 1237 |
+
logger.warning(
|
| 1238 |
+
f"Setting KV cache dtype to {kv_cache_dtype} for DeepSeek DSA on SM{major} device."
|
| 1239 |
+
)
|
| 1240 |
+
if kv_cache_dtype == "bf16":
|
| 1241 |
+
kv_cache_dtype = "bfloat16"
|
| 1242 |
+
assert kv_cache_dtype in [
|
| 1243 |
+
"bfloat16",
|
| 1244 |
+
"fp8_e4m3",
|
| 1245 |
+
], "DeepSeek DSA only supports bf16/bfloat16 or fp8_e4m3 kv_cache_dtype"
|
| 1246 |
+
if kv_cache_dtype != view.kv_cache_dtype:
|
| 1247 |
+
return {"kv_cache_dtype": kv_cache_dtype}
|
| 1248 |
+
return {}
|
| 1249 |
+
|
| 1250 |
+
|
| 1251 |
+
@register_post_process
|
| 1252 |
+
def _dsa_split_backend_resolution(view: Any) -> dict:
|
| 1253 |
+
"""Slot pass in the DSA arm: default the DSA prefill/decode split
|
| 1254 |
+
backends from the mid-resolution kv-cache dtype and the device
|
| 1255 |
+
capability. The hisparse arm takes precedence under --enable-hisparse."""
|
| 1256 |
+
from sglang.srt.configs.model_config import is_deepseek_dsa
|
| 1257 |
+
|
| 1258 |
+
hf_config = view.get_model_config().hf_config
|
| 1259 |
+
if hf_config.architectures[0] not in _DEEPSEEK_FAMILY_ARCHS:
|
| 1260 |
+
return {}
|
| 1261 |
+
if not is_deepseek_dsa(hf_config):
|
| 1262 |
+
return {}
|
| 1263 |
+
if is_npu() or is_xpu():
|
| 1264 |
+
return {}
|
| 1265 |
+
|
| 1266 |
+
import torch
|
| 1267 |
+
|
| 1268 |
+
major, _ = torch.cuda.get_device_capability()
|
| 1269 |
+
kv_cache_dtype = view.kv_cache_dtype
|
| 1270 |
+
user_set_prefill = view.dsa_prefill_backend is not None
|
| 1271 |
+
user_set_decode = view.dsa_decode_backend is not None
|
| 1272 |
+
declared: Dict[str, Any] = {}
|
| 1273 |
+
|
| 1274 |
+
if view.enable_hisparse:
|
| 1275 |
+
from sglang.srt.arg_groups.hisparse_hook import _hisparse_default_backend
|
| 1276 |
+
|
| 1277 |
+
backend = _hisparse_default_backend(kv_cache_dtype)
|
| 1278 |
+
if not user_set_prefill:
|
| 1279 |
+
declared["dsa_prefill_backend"] = backend
|
| 1280 |
+
if not user_set_decode:
|
| 1281 |
+
declared["dsa_decode_backend"] = backend
|
| 1282 |
+
prefill = declared.get("dsa_prefill_backend", view.dsa_prefill_backend)
|
| 1283 |
+
decode = declared.get("dsa_decode_backend", view.dsa_decode_backend)
|
| 1284 |
+
logger.warning(
|
| 1285 |
+
f"HiSparse enabled ({kv_cache_dtype}): using DSA backends "
|
| 1286 |
+
f"prefill={prefill}, decode={decode}."
|
| 1287 |
+
)
|
| 1288 |
+
return declared
|
| 1289 |
+
|
| 1290 |
+
if not user_set_prefill and not user_set_decode and is_hip():
|
| 1291 |
+
declared["dsa_prefill_backend"] = "tilelang"
|
| 1292 |
+
declared["dsa_decode_backend"] = "tilelang"
|
| 1293 |
+
elif kv_cache_dtype == "fp8_e4m3":
|
| 1294 |
+
# Blackwell FP8 defaults to trtllm; Hopper FP8 to flashmla_kv.
|
| 1295 |
+
default = "trtllm" if major >= 10 else "flashmla_kv"
|
| 1296 |
+
if not user_set_prefill:
|
| 1297 |
+
declared["dsa_prefill_backend"] = default
|
| 1298 |
+
if not user_set_decode:
|
| 1299 |
+
declared["dsa_decode_backend"] = default
|
| 1300 |
+
else:
|
| 1301 |
+
# Set prefill/decode backends based on hardware architecture.
|
| 1302 |
+
if not user_set_prefill:
|
| 1303 |
+
declared["dsa_prefill_backend"] = "flashmla_sparse"
|
| 1304 |
+
if not user_set_decode:
|
| 1305 |
+
declared["dsa_decode_backend"] = "trtllm" if major >= 10 else "fa3"
|
| 1306 |
+
|
| 1307 |
+
prefill = declared.get("dsa_prefill_backend", view.dsa_prefill_backend)
|
| 1308 |
+
decode = declared.get("dsa_decode_backend", view.dsa_decode_backend)
|
| 1309 |
+
logger.warning(
|
| 1310 |
+
f"Set DSA backends for {kv_cache_dtype} KV Cache: "
|
| 1311 |
+
f"prefill={prefill}, decode={decode}."
|
| 1312 |
+
)
|
| 1313 |
+
return declared
|
| 1314 |
+
|
| 1315 |
+
|
| 1316 |
+
# Keep in sync with the DeepSeek family list on _deepseek_family_overrides.
|
| 1317 |
+
_DEEPSEEK_FAMILY_ARCHS = frozenset(
|
| 1318 |
+
{
|
| 1319 |
+
"DeepseekV3ForCausalLM",
|
| 1320 |
+
"DeepseekV32ForCausalLM",
|
| 1321 |
+
"KimiK25ForConditionalGeneration",
|
| 1322 |
+
"MistralLarge3ForCausalLM",
|
| 1323 |
+
"PixtralForConditionalGeneration",
|
| 1324 |
+
"GlmMoeDsaForCausalLM",
|
| 1325 |
+
"LongcatFlashForCausalLM",
|
| 1326 |
+
"LongcatFlashForCausalLMNextN",
|
| 1327 |
+
}
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
|
| 1331 |
+
@register_post_process
|
| 1332 |
+
def _deepseek_moe_quant_resolution(view: Any) -> dict:
|
| 1333 |
+
"""Slot pass invoked from inside the DeepSeek arch branch ("Set moe
|
| 1334 |
+
backend for DeepSeek"), NOT a dispatch-time declaration: the DSA
|
| 1335 |
+
kv-cache-dtype default earlier in the branch must read the PRISTINE
|
| 1336 |
+
quantization, so this resolution has to stay at its legacy slot."""
|
| 1337 |
+
hf_config = view.get_model_config().hf_config
|
| 1338 |
+
model_arch = hf_config.architectures[0]
|
| 1339 |
+
if model_arch not in _DEEPSEEK_FAMILY_ARCHS:
|
| 1340 |
+
return {}
|
| 1341 |
+
overrides: Dict[str, Any] = {}
|
| 1342 |
+
if is_sm100_supported():
|
| 1343 |
+
quant_method = get_quantization_config(hf_config)
|
| 1344 |
+
quant_cfg = getattr(hf_config, "quantization_config", None) or {}
|
| 1345 |
+
config_groups = quant_cfg.get("config_groups", {})
|
| 1346 |
+
group0 = config_groups.get("group_0", {})
|
| 1347 |
+
weights_cfg = group0.get("weights", {})
|
| 1348 |
+
# this also apply to kimi k2.5
|
| 1349 |
+
# since it follow the compressed tensor int4 recipe
|
| 1350 |
+
# but not kimi k2 instruct or 0905 instruct.
|
| 1351 |
+
is_kimi_k2_k25_thinking_int4 = (
|
| 1352 |
+
quant_method == "compressed-tensors"
|
| 1353 |
+
and weights_cfg.get("num_bits") == 4
|
| 1354 |
+
and weights_cfg.get("group_size") == 32
|
| 1355 |
+
and weights_cfg.get("strategy") == "group"
|
| 1356 |
+
and weights_cfg.get("type") == "int"
|
| 1357 |
+
)
|
| 1358 |
+
quantization = view.quantization
|
| 1359 |
+
if quantization is None and not view._quantization_explicitly_unset:
|
| 1360 |
+
# DeepSeek V3/R1 uses native FP8 MoE experts without
|
| 1361 |
+
# declaring it in quantization_config. However, other
|
| 1362 |
+
# models that share the same architecture class (e.g.
|
| 1363 |
+
# Moonlight-16B-A3B) are purely BF16. Check the actual
|
| 1364 |
+
# safetensors header instead of assuming FP8 by arch name.
|
| 1365 |
+
if quant_method is None and model_arch in ["DeepseekV3ForCausalLM"]:
|
| 1366 |
+
from sglang.srt.utils.common import has_fp8_weights_in_checkpoint
|
| 1367 |
+
|
| 1368 |
+
if has_fp8_weights_in_checkpoint(view.model_path):
|
| 1369 |
+
overrides["quantization"] = quantization = "fp8"
|
| 1370 |
+
logger.info(
|
| 1371 |
+
"Detected FP8 expert weights in checkpoint, "
|
| 1372 |
+
"default to fp8 for DeepSeek on sm100"
|
| 1373 |
+
)
|
| 1374 |
+
else:
|
| 1375 |
+
logger.info(
|
| 1376 |
+
"No FP8 expert weights found in checkpoint, "
|
| 1377 |
+
"keeping bf16 for DeepSeek-arch model on sm100"
|
| 1378 |
+
)
|
| 1379 |
+
else:
|
| 1380 |
+
overrides["quantization"] = quantization = quant_method
|
| 1381 |
+
if (
|
| 1382 |
+
view.moe_a2a_backend == "none"
|
| 1383 |
+
and view.moe_runner_backend == "auto"
|
| 1384 |
+
and (
|
| 1385 |
+
quantization
|
| 1386 |
+
in ["fp8", "modelopt_fp8", "modelopt_fp4", "modelopt_mixed"]
|
| 1387 |
+
or is_kimi_k2_k25_thinking_int4
|
| 1388 |
+
or quantization is None
|
| 1389 |
+
)
|
| 1390 |
+
):
|
| 1391 |
+
overrides["moe_runner_backend"] = "flashinfer_trtllm"
|
| 1392 |
+
if is_kimi_k2_k25_thinking_int4:
|
| 1393 |
+
logger.info(
|
| 1394 |
+
"Use flashinfer_trtllm as MoE runner backend on Blackwell for Kimi K2 / K2.5 thinking int4"
|
| 1395 |
+
)
|
| 1396 |
+
else:
|
| 1397 |
+
logger.info(
|
| 1398 |
+
"Use flashinfer_trtllm as MoE runner backend on sm100 for DeepseekV3ForCausalLM"
|
| 1399 |
+
)
|
| 1400 |
+
if (
|
| 1401 |
+
model_arch in ["LongcatFlashForCausalLM", "LongcatFlashForCausalLMNextN"]
|
| 1402 |
+
and view.fp8_gemm_runner_backend == "auto"
|
| 1403 |
+
and quantization in ["fp8", "modelopt_fp8"]
|
| 1404 |
+
and quant_cfg.get("scale_fmt", None) != "ue8m0"
|
| 1405 |
+
):
|
| 1406 |
+
overrides["fp8_gemm_runner_backend"] = "flashinfer_trtllm"
|
| 1407 |
+
logger.info(
|
| 1408 |
+
"Use flashinfer_trtllm as FP8 GEMM backend on Blackwell for LongCat FP8 "
|
| 1409 |
+
"checkpoint with non-ue8m0 scales"
|
| 1410 |
+
)
|
| 1411 |
+
return overrides
|
| 1412 |
+
|
| 1413 |
+
|
| 1414 |
+
@register_post_process
|
| 1415 |
+
def _deepseek_spec_moe_resolution(view: Any) -> dict:
|
| 1416 |
+
"""Slot pass at the DeepSeek branch's HIP arm: draft (nextn) spec-MoE
|
| 1417 |
+
backends for the DeepSeek fp4 checkpoint. Reads the mid-resolution
|
| 1418 |
+
quantization (after _deepseek_moe_quant_resolution) and the pre-a2a
|
| 1419 |
+
ep_size, exactly like the legacy in-branch writes."""
|
| 1420 |
+
|
| 1421 |
+
hf_config = view.get_model_config().hf_config
|
| 1422 |
+
model_arch = hf_config.architectures[0]
|
| 1423 |
+
if model_arch not in _DEEPSEEK_FAMILY_ARCHS:
|
| 1424 |
+
return {}
|
| 1425 |
+
if not is_hip():
|
| 1426 |
+
return {}
|
| 1427 |
+
if not (
|
| 1428 |
+
view.quantization == "modelopt_fp4"
|
| 1429 |
+
and view.speculative_algorithm == "EAGLE"
|
| 1430 |
+
and (
|
| 1431 |
+
view.speculative_moe_runner_backend is None
|
| 1432 |
+
or view.speculative_moe_a2a_backend is None
|
| 1433 |
+
)
|
| 1434 |
+
):
|
| 1435 |
+
return {}
|
| 1436 |
+
if envs.SGLANG_NVFP4_CKPT_FP8_NEXTN_MOE.get():
|
| 1437 |
+
logger.info(
|
| 1438 |
+
"Use deep_gemm moe runner and deepep a2a backend for bf16 nextn layer in deepseek fp4 checkpoint."
|
| 1439 |
+
)
|
| 1440 |
+
# Validate usage of ep
|
| 1441 |
+
if view.ep_size == 1:
|
| 1442 |
+
raise ValueError(
|
| 1443 |
+
"Invalid configuration: 'deep_gemm' speculative MoE runner backend with "
|
| 1444 |
+
"'deepep' a2a backend requires expert parallelism (ep_size > 1). "
|
| 1445 |
+
f"Current ep_size is {view.ep_size}. "
|
| 1446 |
+
"Please set --ep-size > 1 (e.g., --ep-size 8) to use this configuration, "
|
| 1447 |
+
"or change --speculative-moe-a2a-backend to 'none' if expert parallelism is not available."
|
| 1448 |
+
)
|
| 1449 |
+
return {
|
| 1450 |
+
"speculative_moe_runner_backend": "deep_gemm",
|
| 1451 |
+
"speculative_moe_a2a_backend": "deepep",
|
| 1452 |
+
}
|
| 1453 |
+
logger.info(
|
| 1454 |
+
"Use triton fused moe by default for bf16 nextn layer in deepseek fp4 checkpoint."
|
| 1455 |
+
)
|
| 1456 |
+
return {
|
| 1457 |
+
"speculative_moe_runner_backend": "triton",
|
| 1458 |
+
"speculative_moe_a2a_backend": "none",
|
| 1459 |
+
}
|
| 1460 |
+
|
| 1461 |
+
|
| 1462 |
+
@register_post_process
|
| 1463 |
+
def _deepseek_v4_kv_cache_dtype(view: Any) -> dict:
|
| 1464 |
+
"""Slot pass in the DeepSeek V4 hook: default the kv-cache dtype to FP8
|
| 1465 |
+
(bfloat16 on NPU, where the pool geometry differs) and validate the
|
| 1466 |
+
result. The NPU split-backend writes stay in the hook."""
|
| 1467 |
+
hf_config = view.get_model_config().hf_config
|
| 1468 |
+
model_arch = hf_config.architectures[0]
|
| 1469 |
+
if model_arch != "AgnesForCausalLM":
|
| 1470 |
+
return {}
|
| 1471 |
+
|
| 1472 |
+
kv_cache_dtype = view.kv_cache_dtype
|
| 1473 |
+
if kv_cache_dtype == "auto":
|
| 1474 |
+
kv_cache_dtype = "fp8_e4m3"
|
| 1475 |
+
logger.warning(f"Setting KV cache dtype to {kv_cache_dtype} for {model_arch}.")
|
| 1476 |
+
if view.device == "npu":
|
| 1477 |
+
kv_cache_dtype = "bfloat16"
|
| 1478 |
+
assert kv_cache_dtype in [
|
| 1479 |
+
"fp8_e4m3",
|
| 1480 |
+
"bfloat16",
|
| 1481 |
+
], f"{kv_cache_dtype} is not supported for {model_arch}"
|
| 1482 |
+
if kv_cache_dtype != view.kv_cache_dtype:
|
| 1483 |
+
return {"kv_cache_dtype": kv_cache_dtype}
|
| 1484 |
+
return {}
|
| 1485 |
+
|
| 1486 |
+
|
| 1487 |
+
@register_post_process
|
| 1488 |
+
def _deepseek_v4_sm120_moe(view: Any) -> dict:
|
| 1489 |
+
"""Default DeepSeek V4 MXFP4 experts to FlashInfer CUTLASS on SM120."""
|
| 1490 |
+
hf_config = view.get_model_config().hf_config
|
| 1491 |
+
if hf_config.architectures[0] != "AgnesForCausalLM":
|
| 1492 |
+
return {}
|
| 1493 |
+
if is_sm120_supported() and view.moe_runner_backend == "auto":
|
| 1494 |
+
logger.info(
|
| 1495 |
+
"Use flashinfer_mxfp4 as MoE runner backend on SM120 for DeepseekV4"
|
| 1496 |
+
)
|
| 1497 |
+
return {"moe_runner_backend": "flashinfer_mxfp4"}
|
| 1498 |
+
return {}
|
| 1499 |
+
|
| 1500 |
+
|
| 1501 |
+
@register_post_process
|
| 1502 |
+
def _sparse_head_overlap_disable(view: Any) -> dict:
|
| 1503 |
+
|
| 1504 |
+
if envs.SGLANG_EMBEDDINGS_SPARSE_HEAD.is_set():
|
| 1505 |
+
logger.warning(
|
| 1506 |
+
"Overlap scheduler is disabled when using sparse head for embedding model."
|
| 1507 |
+
)
|
| 1508 |
+
return {"disable_overlap_schedule": True}
|
| 1509 |
+
return {}
|
| 1510 |
+
|
| 1511 |
+
|
| 1512 |
+
# Architectures with explicit FlashInfer AllReduce Fusion support. Keep in
|
| 1513 |
+
# sync with the model-side fusion implementations.
|
| 1514 |
+
_FLASHINFER_ALLREDUCE_FUSION_ARCHS = frozenset(
|
| 1515 |
+
{
|
| 1516 |
+
"DeepseekV3ForCausalLM",
|
| 1517 |
+
"DeepseekV32ForCausalLM",
|
| 1518 |
+
"GptOssForCausalLM",
|
| 1519 |
+
"GlmMoeDsaForCausalLM",
|
| 1520 |
+
"Glm4MoeForCausalLM",
|
| 1521 |
+
"Glm4MoeLiteForCausalLM",
|
| 1522 |
+
"MistralLarge3ForCausalLM",
|
| 1523 |
+
"Qwen3MoeForCausalLM",
|
| 1524 |
+
"Qwen3VLMoeForConditionalGeneration",
|
| 1525 |
+
"Qwen3NextForCausalLM",
|
| 1526 |
+
"KimiK25ForConditionalGeneration",
|
| 1527 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 1528 |
+
"InternS2PreviewForConditionalGeneration",
|
| 1529 |
+
"Qwen3_5ForConditionalGeneration",
|
| 1530 |
+
"NemotronHForCausalLM",
|
| 1531 |
+
"NemotronHPuzzleForCausalLM",
|
| 1532 |
+
}
|
| 1533 |
+
)
|
| 1534 |
+
|
| 1535 |
+
|
| 1536 |
+
@register_post_process
|
| 1537 |
+
def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
|
| 1538 |
+
"""Slot pass at the monolith tail: auto-enable FlashInfer AllReduce
|
| 1539 |
+
Fusion on SM90/SM100 for models with explicit support. auto resolves to
|
| 1540 |
+
mnnvl on Blackwell (single- and multi-node) and trtllm on SM90
|
| 1541 |
+
single-node systems. Reads the mid-resolution enable_dp_attention /
|
| 1542 |
+
moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly
|
| 1543 |
+
like the legacy tail block."""
|
| 1544 |
+
model_arch = view.get_model_config().hf_config.architectures[0]
|
| 1545 |
+
if (
|
| 1546 |
+
view.flashinfer_allreduce_fusion_backend is None
|
| 1547 |
+
and model_arch in _FLASHINFER_ALLREDUCE_FUSION_ARCHS
|
| 1548 |
+
and (is_sm90_supported() or is_sm100_supported())
|
| 1549 |
+
and view.tp_size > 1
|
| 1550 |
+
and not view.enable_dp_attention
|
| 1551 |
+
and (view.nnodes == 1 or is_sm100_supported())
|
| 1552 |
+
and view.moe_a2a_backend == "none"
|
| 1553 |
+
):
|
| 1554 |
+
logger.info(
|
| 1555 |
+
f"Auto-enabling FlashInfer AllReduce Fusion on SM90/SM10X for {model_arch}"
|
| 1556 |
+
)
|
| 1557 |
+
return {"flashinfer_allreduce_fusion_backend": "auto"}
|
| 1558 |
+
return {}
|
| 1559 |
+
|
| 1560 |
+
|
| 1561 |
+
@register_post_process
|
| 1562 |
+
def _enforce_disable_allreduce_fusion(view: Any) -> dict:
|
| 1563 |
+
"""Slot pass right after the auto-enable: the user's enforce-disable
|
| 1564 |
+
switch wins over every model-specific adjustment."""
|
| 1565 |
+
if view.enforce_disable_flashinfer_allreduce_fusion:
|
| 1566 |
+
logger.info(
|
| 1567 |
+
"FlashInfer allreduce fusion is forcibly disabled "
|
| 1568 |
+
"via --enforce-disable-flashinfer-allreduce-fusion."
|
| 1569 |
+
)
|
| 1570 |
+
return {"flashinfer_allreduce_fusion_backend": None}
|
| 1571 |
+
return {}
|
| 1572 |
+
|
| 1573 |
+
|
| 1574 |
+
@register_post_process
|
| 1575 |
+
def _sampling_backend_default(view: Any) -> dict:
|
| 1576 |
+
if view.sampling_backend is None:
|
| 1577 |
+
return {
|
| 1578 |
+
"sampling_backend": (
|
| 1579 |
+
"flashinfer" if is_flashinfer_available() else "pytorch"
|
| 1580 |
+
)
|
| 1581 |
+
}
|
| 1582 |
+
return {}
|
| 1583 |
+
|
| 1584 |
+
|
| 1585 |
+
@register_post_process
|
| 1586 |
+
def _deterministic_sampling_backend(view: Any) -> dict:
|
| 1587 |
+
if view.enable_deterministic_inference and view.sampling_backend != "ascend":
|
| 1588 |
+
logger.warning(
|
| 1589 |
+
"Sampling backend is set to pytorch for deterministic inference."
|
| 1590 |
+
)
|
| 1591 |
+
return {"sampling_backend": "pytorch"}
|
| 1592 |
+
return {}
|
| 1593 |
+
|
| 1594 |
+
|
| 1595 |
+
def _deterministic_is_deepseek_model(view: Any) -> bool:
|
| 1596 |
+
"""Faithful copy of the deterministic handler's arch probe (pure read;
|
| 1597 |
+
the handler keeps its own copy for the later deepseek validation)."""
|
| 1598 |
+
from sglang.srt.connector import ConnectorType
|
| 1599 |
+
from sglang.srt.utils.common import parse_connector_type
|
| 1600 |
+
|
| 1601 |
+
if parse_connector_type(view.model_path) == ConnectorType.INSTANCE:
|
| 1602 |
+
return False
|
| 1603 |
+
try:
|
| 1604 |
+
hf_config = view.get_model_config().hf_config
|
| 1605 |
+
return hf_config.architectures[0] in [
|
| 1606 |
+
"DeepseekV2ForCausalLM",
|
| 1607 |
+
"DeepseekV3ForCausalLM",
|
| 1608 |
+
"DeepseekV32ForCausalLM",
|
| 1609 |
+
"MistralLarge3ForCausalLM",
|
| 1610 |
+
"PixtralForConditionalGeneration",
|
| 1611 |
+
"GlmMoeDsaForCausalLM",
|
| 1612 |
+
]
|
| 1613 |
+
except Exception:
|
| 1614 |
+
return False
|
| 1615 |
+
|
| 1616 |
+
|
| 1617 |
+
@register_post_process
|
| 1618 |
+
def _deterministic_allreduce_fusion_disable(view: Any) -> dict:
|
| 1619 |
+
if (
|
| 1620 |
+
view.enable_deterministic_inference
|
| 1621 |
+
and view.flashinfer_allreduce_fusion_backend is not None
|
| 1622 |
+
):
|
| 1623 |
+
logger.warning(
|
| 1624 |
+
"Disable --flashinfer-allreduce-fusion-backend because deterministic inference is enabled."
|
| 1625 |
+
)
|
| 1626 |
+
return {"flashinfer_allreduce_fusion_backend": None}
|
| 1627 |
+
return {}
|
| 1628 |
+
|
| 1629 |
+
|
| 1630 |
+
@register_post_process
|
| 1631 |
+
def _deterministic_attention_backend(view: Any) -> dict:
|
| 1632 |
+
if not view.enable_deterministic_inference:
|
| 1633 |
+
return {}
|
| 1634 |
+
from sglang.srt.server_args import DETERMINISTIC_ATTENTION_BACKEND_CHOICES
|
| 1635 |
+
|
| 1636 |
+
if view.attention_backend is None:
|
| 1637 |
+
# User didn't specify attention backend, fallback based on GPU architecture
|
| 1638 |
+
if is_sm100_supported() or is_sm120_supported():
|
| 1639 |
+
# Blackwell and newer architectures
|
| 1640 |
+
if _deterministic_is_deepseek_model(view):
|
| 1641 |
+
# fallback to triton for DeepSeek models because flashinfer
|
| 1642 |
+
# doesn't support deterministic inference for DeepSeek models yet
|
| 1643 |
+
backend = "triton"
|
| 1644 |
+
else:
|
| 1645 |
+
# fallback to flashinfer on Blackwell for non-DeepSeek models
|
| 1646 |
+
backend = "flashinfer"
|
| 1647 |
+
else:
|
| 1648 |
+
# Hopper (SM90) and older architectures
|
| 1649 |
+
backend = "fa3"
|
| 1650 |
+
logger.warning(
|
| 1651 |
+
f"Attention backend not specified. Falling back to '{backend}' for deterministic inference. "
|
| 1652 |
+
f"You can explicitly set --attention-backend to one of {DETERMINISTIC_ATTENTION_BACKEND_CHOICES}."
|
| 1653 |
+
)
|
| 1654 |
+
return {"attention_backend": backend}
|
| 1655 |
+
elif view.attention_backend not in DETERMINISTIC_ATTENTION_BACKEND_CHOICES:
|
| 1656 |
+
# User explicitly specified an incompatible attention backend
|
| 1657 |
+
raise ValueError(
|
| 1658 |
+
f"Currently only {DETERMINISTIC_ATTENTION_BACKEND_CHOICES} attention backends are supported for deterministic inference, "
|
| 1659 |
+
f"but you explicitly specified '{view.attention_backend}'."
|
| 1660 |
+
)
|
| 1661 |
+
return {}
|
| 1662 |
+
|
| 1663 |
+
|
| 1664 |
+
@register_post_process
|
| 1665 |
+
def _attention_backend_default(view: Any) -> dict:
|
| 1666 |
+
if view.prefill_attention_backend is not None and (
|
| 1667 |
+
view.prefill_attention_backend == view.decode_attention_backend
|
| 1668 |
+
): # override the default attention backend
|
| 1669 |
+
return {"attention_backend": view.prefill_attention_backend}
|
| 1670 |
+
if view.attention_backend is None:
|
| 1671 |
+
backend = view._get_default_attn_backend(
|
| 1672 |
+
view.use_mla_backend(), view.get_model_config()
|
| 1673 |
+
)
|
| 1674 |
+
logger.info(
|
| 1675 |
+
f"Attention backend not specified. Use {backend} backend by default."
|
| 1676 |
+
)
|
| 1677 |
+
return {"attention_backend": backend}
|
| 1678 |
+
return {}
|
| 1679 |
+
|
| 1680 |
+
|
| 1681 |
+
@register_post_process
|
| 1682 |
+
def _mla_backend_page_constraints(view: Any) -> dict:
|
| 1683 |
+
"""Page-size constraints of the MLA/TRTLLM backend family (the raises and
|
| 1684 |
+
the cutedsl prefill fallback stay in the handler; only the page snaps are
|
| 1685 |
+
declared). The snaps chain on a local value exactly as the legacy blocks
|
| 1686 |
+
chained on self.page_size."""
|
| 1687 |
+
page_size = view.page_size
|
| 1688 |
+
if (
|
| 1689 |
+
view.attention_backend == "flashmla"
|
| 1690 |
+
or view.decode_attention_backend == "flashmla"
|
| 1691 |
+
):
|
| 1692 |
+
logger.warning(
|
| 1693 |
+
"FlashMLA only supports a page_size of 64, change page_size to 64."
|
| 1694 |
+
)
|
| 1695 |
+
page_size = 64
|
| 1696 |
+
if (
|
| 1697 |
+
view.attention_backend == "cutlass_mla"
|
| 1698 |
+
or view.decode_attention_backend == "cutlass_mla"
|
| 1699 |
+
):
|
| 1700 |
+
logger.warning(
|
| 1701 |
+
"Cutlass MLA only supports a page_size of 128, change page_size to 128."
|
| 1702 |
+
)
|
| 1703 |
+
page_size = 128
|
| 1704 |
+
if (
|
| 1705 |
+
view.attention_backend == "trtllm_mla"
|
| 1706 |
+
or view.decode_attention_backend == "trtllm_mla"
|
| 1707 |
+
):
|
| 1708 |
+
if page_size not in [32, 64]:
|
| 1709 |
+
logger.warning(
|
| 1710 |
+
f"TensorRT-LLM MLA only supports page_size of 32 or 64, changing page_size from {page_size} to 64."
|
| 1711 |
+
)
|
| 1712 |
+
page_size = 64
|
| 1713 |
+
if (
|
| 1714 |
+
view.attention_backend == "tokenspeed_mla"
|
| 1715 |
+
or view.decode_attention_backend == "tokenspeed_mla"
|
| 1716 |
+
):
|
| 1717 |
+
if page_size not in [32, 64]:
|
| 1718 |
+
logger.warning(
|
| 1719 |
+
f"tokenspeed_mla only supports page_size of 32 or 64, changing page_size from {page_size} to 64."
|
| 1720 |
+
)
|
| 1721 |
+
page_size = 64
|
| 1722 |
+
if (
|
| 1723 |
+
view.attention_backend == "cutedsl_mla"
|
| 1724 |
+
or view.decode_attention_backend == "cutedsl_mla"
|
| 1725 |
+
or view.prefill_attention_backend == "cutedsl_mla"
|
| 1726 |
+
):
|
| 1727 |
+
if page_size not in [32, 64]:
|
| 1728 |
+
logger.warning(
|
| 1729 |
+
f"CuteDSL MLA only supports page_size of 32 or 64, changing page_size from {page_size} to 64."
|
| 1730 |
+
)
|
| 1731 |
+
page_size = 64
|
| 1732 |
+
if (
|
| 1733 |
+
view.attention_backend == "trtllm_mha"
|
| 1734 |
+
or view.decode_attention_backend == "trtllm_mha"
|
| 1735 |
+
or view.prefill_attention_backend == "trtllm_mha"
|
| 1736 |
+
):
|
| 1737 |
+
if page_size not in [16, 32, 64]:
|
| 1738 |
+
logger.warning(
|
| 1739 |
+
f"TensorRT-LLM MHA only supports page_size of 16, 32 or 64, changing page_size from {page_size} to 64."
|
| 1740 |
+
)
|
| 1741 |
+
page_size = 64
|
| 1742 |
+
if page_size != view.page_size:
|
| 1743 |
+
return {"page_size": page_size}
|
| 1744 |
+
return {}
|
| 1745 |
+
|
| 1746 |
+
|
| 1747 |
+
@register_post_process
|
| 1748 |
+
def _mla_kv_cache_dtype_checks(view: Any) -> dict:
|
| 1749 |
+
"""Read-only validation pass in the attention-backend compatibility
|
| 1750 |
+
handler: the TRT-LLM and tokenspeed MLA backends constrain the resolved
|
| 1751 |
+
kv-cache dtype (declarations never reach the field, so the checks read
|
| 1752 |
+
the view)."""
|
| 1753 |
+
if (
|
| 1754 |
+
view.attention_backend == "trtllm_mla"
|
| 1755 |
+
or view.decode_attention_backend == "trtllm_mla"
|
| 1756 |
+
):
|
| 1757 |
+
if not is_blackwell_supported():
|
| 1758 |
+
raise ValueError(
|
| 1759 |
+
"TRTLLM MLA backend is only supported on Blackwell GPUs (SM100/SM12x). Please use a different backend."
|
| 1760 |
+
)
|
| 1761 |
+
if view.kv_cache_dtype not in ["fp8_e4m3", "fp4_e2m1", "bf16", "auto"]:
|
| 1762 |
+
raise ValueError(
|
| 1763 |
+
"TensorRT-LLM MLA backend only supports kv-cache-dtype of fp8_e4m3, fp4_e2m1, bf16, or auto."
|
| 1764 |
+
)
|
| 1765 |
+
if (
|
| 1766 |
+
view.attention_backend == "tokenspeed_mla"
|
| 1767 |
+
or view.decode_attention_backend == "tokenspeed_mla"
|
| 1768 |
+
):
|
| 1769 |
+
if not is_blackwell_supported():
|
| 1770 |
+
raise ValueError(
|
| 1771 |
+
"tokenspeed_mla backend is only supported on Blackwell GPUs (SM100/SM12x)."
|
| 1772 |
+
)
|
| 1773 |
+
if view.kv_cache_dtype not in ["fp8_e4m3"]:
|
| 1774 |
+
raise ValueError(
|
| 1775 |
+
"tokenspeed_mla backend requires kv-cache-dtype=fp8_e4m3, "
|
| 1776 |
+
f"got {view.kv_cache_dtype}."
|
| 1777 |
+
)
|
| 1778 |
+
return {}
|
| 1779 |
+
|
| 1780 |
+
|
| 1781 |
+
@register_post_process
|
| 1782 |
+
def _hisparse_validation(view: Any) -> dict:
|
| 1783 |
+
"""Read-only validation pass: --enable-hisparse constraints (model class,
|
| 1784 |
+
radix cache, kv dtype, DSA backends) read the resolved values through the
|
| 1785 |
+
view."""
|
| 1786 |
+
from sglang.srt.arg_groups.hisparse_hook import validate_hisparse
|
| 1787 |
+
|
| 1788 |
+
validate_hisparse(view)
|
| 1789 |
+
return {}
|
| 1790 |
+
|
| 1791 |
+
|
| 1792 |
+
@register_post_process
|
| 1793 |
+
def _cutedsl_prefill_backend_fill(view: Any) -> dict:
|
| 1794 |
+
"""Slot pass in the attention-backend compatibility handler: CuteDSL MLA
|
| 1795 |
+
is decode-only, so validate the combination and default the prefill side
|
| 1796 |
+
to trtllm_mla. The trtllm_mha check that follows at the legacy slot reads
|
| 1797 |
+
the resolved value through the view."""
|
| 1798 |
+
if not (
|
| 1799 |
+
view.attention_backend == "cutedsl_mla"
|
| 1800 |
+
or view.decode_attention_backend == "cutedsl_mla"
|
| 1801 |
+
or view.prefill_attention_backend == "cutedsl_mla"
|
| 1802 |
+
):
|
| 1803 |
+
return {}
|
| 1804 |
+
assert (
|
| 1805 |
+
view.prefill_attention_backend != "cutedsl_mla"
|
| 1806 |
+
), "CuteDSL MLA only supports decoding for now"
|
| 1807 |
+
if not is_sm100_supported():
|
| 1808 |
+
raise ValueError(
|
| 1809 |
+
"CuteDSL MLA backend is only supported on Blackwell GPUs (SM100). Please use a different backend."
|
| 1810 |
+
)
|
| 1811 |
+
if view.kv_cache_dtype not in [
|
| 1812 |
+
"fp8_e4m3",
|
| 1813 |
+
"bf16",
|
| 1814 |
+
"bfloat16",
|
| 1815 |
+
"auto",
|
| 1816 |
+
]:
|
| 1817 |
+
raise ValueError(
|
| 1818 |
+
"CuteDSL MLA backend only supports kv-cache-dtype of fp8_e4m3, bf16, or auto."
|
| 1819 |
+
)
|
| 1820 |
+
if view.prefill_attention_backend is None:
|
| 1821 |
+
return {"prefill_attention_backend": "trtllm_mla"}
|
| 1822 |
+
return {}
|
| 1823 |
+
|
| 1824 |
+
|
| 1825 |
+
@register_post_process
|
| 1826 |
+
def _attention_backend_fa3_fp8_fallback(view: Any) -> dict:
|
| 1827 |
+
if view.attention_backend == "fa3" and view.kv_cache_dtype == "fp8_e5m2":
|
| 1828 |
+
logger.warning(
|
| 1829 |
+
"FlashAttention3 only supports fp8_e4m3 if using FP8; "
|
| 1830 |
+
"Setting attention backend to triton."
|
| 1831 |
+
)
|
| 1832 |
+
return {"attention_backend": "triton"}
|
| 1833 |
+
return {}
|
| 1834 |
+
|
| 1835 |
+
|
| 1836 |
+
@register_post_process
|
| 1837 |
+
def _fa4_page_constraint(view: Any) -> dict:
|
| 1838 |
+
if (
|
| 1839 |
+
(
|
| 1840 |
+
view.attention_backend == "fa4"
|
| 1841 |
+
or view.decode_attention_backend == "fa4"
|
| 1842 |
+
or view.prefill_attention_backend == "fa4"
|
| 1843 |
+
)
|
| 1844 |
+
and not view.use_mla_backend()
|
| 1845 |
+
and is_sm100_supported()
|
| 1846 |
+
# EAGLE topk>1 spec runs the two-pass page-tree cascade, which the FA4
|
| 1847 |
+
# CUTLASS kernel aborts on at page_size>1. That path only works at
|
| 1848 |
+
# page_size==1, so skip the 128 auto-force for it and keep the default.
|
| 1849 |
+
and (view.speculative_eagle_topk or 0) <= 1
|
| 1850 |
+
):
|
| 1851 |
+
logger.warning(
|
| 1852 |
+
f"FA4 backend only supports page size 128 for non-MLA model architectures, changing page_size from {view.page_size} to 128."
|
| 1853 |
+
)
|
| 1854 |
+
return {"page_size": 128}
|
| 1855 |
+
return {}
|
| 1856 |
+
|
| 1857 |
+
|
| 1858 |
+
@register_post_process
|
| 1859 |
+
def _attention_backend_platform_fallbacks(view: Any) -> dict:
|
| 1860 |
+
if (
|
| 1861 |
+
view.attention_backend == "intel_amx"
|
| 1862 |
+
and view.device == "cpu"
|
| 1863 |
+
and not cpu_has_amx_support()
|
| 1864 |
+
):
|
| 1865 |
+
logger.warning(
|
| 1866 |
+
"The current platform does not support Intel AMX, will fallback to torch_native backend."
|
| 1867 |
+
)
|
| 1868 |
+
return {"attention_backend": "torch_native"}
|
| 1869 |
+
if (
|
| 1870 |
+
view.attention_backend == "intel_xpu"
|
| 1871 |
+
and view.device == "xpu"
|
| 1872 |
+
and not xpu_has_xmx_support()
|
| 1873 |
+
):
|
| 1874 |
+
logger.warning(
|
| 1875 |
+
"The current platform does not support Intel XMX, will fallback to triton backend."
|
| 1876 |
+
)
|
| 1877 |
+
return {"attention_backend": "triton"}
|
| 1878 |
+
return {}
|
| 1879 |
+
|
| 1880 |
+
|
| 1881 |
+
@register_post_process
|
| 1882 |
+
def _intel_xpu_page_constraint(view: Any) -> dict:
|
| 1883 |
+
_, decode_backend = attention_backends_of(view)
|
| 1884 |
+
if decode_backend == "intel_xpu":
|
| 1885 |
+
if view.use_mla_backend():
|
| 1886 |
+
supported_page_sizes = [16, 32, 64, 128]
|
| 1887 |
+
msg = "Intel XPU attention backend for MLA Decode"
|
| 1888 |
+
else:
|
| 1889 |
+
supported_page_sizes = [64, 128]
|
| 1890 |
+
msg = "Intel XPU attention backend"
|
| 1891 |
+
if view.page_size not in supported_page_sizes:
|
| 1892 |
+
logger.warning(
|
| 1893 |
+
f"{msg} only supports page_sizes of {supported_page_sizes}, changing page_size from {view.page_size} to 128."
|
| 1894 |
+
)
|
| 1895 |
+
return {"page_size": 128}
|
| 1896 |
+
return {}
|
| 1897 |
+
|
| 1898 |
+
|
| 1899 |
+
@register_post_process
|
| 1900 |
+
def _attention_backend_dual_chunk(view: Any) -> dict:
|
| 1901 |
+
if (
|
| 1902 |
+
getattr(view.get_model_config().hf_config, "dual_chunk_attention_config", None)
|
| 1903 |
+
is not None
|
| 1904 |
+
):
|
| 1905 |
+
if view.attention_backend is None:
|
| 1906 |
+
logger.info("Dual chunk attention is turned on by default.")
|
| 1907 |
+
return {"attention_backend": "dual_chunk_flash_attn"}
|
| 1908 |
+
elif view.attention_backend != "dual_chunk_flash_attn":
|
| 1909 |
+
raise ValueError(
|
| 1910 |
+
"Dual chunk attention is enabled, but attention backend is set to "
|
| 1911 |
+
f"{view.attention_backend}. Please set it to 'dual_chunk_flash_attn'."
|
| 1912 |
+
)
|
| 1913 |
+
return {}
|
| 1914 |
+
|
| 1915 |
+
|
| 1916 |
+
@register_post_process
|
| 1917 |
+
def _page_size_default(view: Any) -> dict:
|
| 1918 |
+
if view.page_size is not None:
|
| 1919 |
+
return {}
|
| 1920 |
+
|
| 1921 |
+
# SHUFFLE 5D vectorized KV layout (aiter backend + pa_decode_gluon)
|
| 1922 |
+
# is tuned for and prefers page_size=64 — making it the default
|
| 1923 |
+
# when the layout flag is set avoids users having to pass
|
| 1924 |
+
# --page-size 64 explicitly. The env var is only consumed by the
|
| 1925 |
+
# ROCm AITER backend, so the auto-bump is gated on HIP; on other
|
| 1926 |
+
# platforms the SHUFFLE 5D pool has no consumer kernels and the
|
| 1927 |
+
# env var is silently ignored (see MHATokenToKVPool).
|
| 1928 |
+
if is_hip() and envs.SGLANG_AITER_KV_CACHE_LAYOUT.get().lower() == "vectorized_5d":
|
| 1929 |
+
logger.info(
|
| 1930 |
+
"Setting page_size=64 as default for "
|
| 1931 |
+
"SGLANG_AITER_KV_CACHE_LAYOUT=vectorized_5d."
|
| 1932 |
+
)
|
| 1933 |
+
return {"page_size": 64}
|
| 1934 |
+
if not is_musa():
|
| 1935 |
+
return {"page_size": 1}
|
| 1936 |
+
return {"page_size": 64}
|
| 1937 |
+
|
| 1938 |
+
|
| 1939 |
+
@register_post_process
|
| 1940 |
+
def _data_parallelism_defaults(view: Any) -> dict:
|
| 1941 |
+
if view.dp_size == 1 and view.ep_join_mode != "scale":
|
| 1942 |
+
return {"enable_dp_attention": False, "enable_dp_lm_head": False}
|
| 1943 |
+
return {}
|
| 1944 |
+
|
| 1945 |
+
|
| 1946 |
+
@register_post_process
|
| 1947 |
+
def _dp_lm_head_validation(view: Any) -> dict:
|
| 1948 |
+
"""Read-only validation pass: dp-attention is a prerequisite for the
|
| 1949 |
+
dp LM head. Reads the mid-resolution values through the view."""
|
| 1950 |
+
if view.enable_dp_lm_head:
|
| 1951 |
+
assert (
|
| 1952 |
+
view.enable_dp_attention
|
| 1953 |
+
), "Please enable dp attention when setting enable_dp_lm_head. "
|
| 1954 |
+
return {}
|
| 1955 |
+
|
| 1956 |
+
|
| 1957 |
+
@register_post_process
|
| 1958 |
+
def _moe_runner_backend_quant_constraints(view: Any) -> dict:
|
| 1959 |
+
"""The quantization-driven moe_runner_backend resolutions at the head of
|
| 1960 |
+
_handle_moe_kernel_config. The backend-compatibility asserts and the
|
| 1961 |
+
disable_shared_experts_fusion writes (post-publish writers exist for that
|
| 1962 |
+
field) stay in the handler."""
|
| 1963 |
+
moe_runner_backend = view.moe_runner_backend
|
| 1964 |
+
if view.quantization == "nvfp4_online":
|
| 1965 |
+
if not is_sm100_supported():
|
| 1966 |
+
raise ValueError(
|
| 1967 |
+
"--quantization nvfp4_online is supported only on "
|
| 1968 |
+
"NVIDIA Blackwell SM100/SM103 GPUs."
|
| 1969 |
+
)
|
| 1970 |
+
if moe_runner_backend == "auto":
|
| 1971 |
+
moe_runner_backend = "flashinfer_trtllm"
|
| 1972 |
+
elif moe_runner_backend not in [
|
| 1973 |
+
"flashinfer_trtllm",
|
| 1974 |
+
"flashinfer_trtllm_routed",
|
| 1975 |
+
]:
|
| 1976 |
+
raise ValueError(
|
| 1977 |
+
"--quantization nvfp4_online supports only "
|
| 1978 |
+
"--moe-runner-backend flashinfer_trtllm or "
|
| 1979 |
+
"flashinfer_trtllm_routed."
|
| 1980 |
+
)
|
| 1981 |
+
if view.quantization == "mxfp8":
|
| 1982 |
+
from sglang.srt.server_args import MXFP8_MOE_RUNNER_BACKEND_CHOICES
|
| 1983 |
+
|
| 1984 |
+
is_gfx95_mxfp8 = is_hip() and is_gfx95_supported()
|
| 1985 |
+
allowed = list(MXFP8_MOE_RUNNER_BACKEND_CHOICES)
|
| 1986 |
+
if is_gfx95_mxfp8:
|
| 1987 |
+
allowed.append("triton")
|
| 1988 |
+
mxfp8_default = "triton" if is_gfx95_mxfp8 else "flashinfer_trtllm"
|
| 1989 |
+
if moe_runner_backend == "auto":
|
| 1990 |
+
moe_runner_backend = mxfp8_default
|
| 1991 |
+
elif moe_runner_backend not in allowed:
|
| 1992 |
+
logger.warning(
|
| 1993 |
+
"mxfp8 quantization supports only %s backends. " "Overriding %r.",
|
| 1994 |
+
", ".join(allowed),
|
| 1995 |
+
moe_runner_backend,
|
| 1996 |
+
)
|
| 1997 |
+
moe_runner_backend = mxfp8_default
|
| 1998 |
+
if (
|
| 1999 |
+
moe_runner_backend == "auto"
|
| 2000 |
+
and view.quantization == "modelopt_fp4"
|
| 2001 |
+
and is_sm120_supported()
|
| 2002 |
+
):
|
| 2003 |
+
moe_runner_backend = "flashinfer_cutlass"
|
| 2004 |
+
logger.info(
|
| 2005 |
+
"Use flashinfer_cutlass as MoE runner backend on SM120 for "
|
| 2006 |
+
"modelopt_fp4 (trtllm-gen MoE kernels are SM100-only)"
|
| 2007 |
+
)
|
| 2008 |
+
if moe_runner_backend != view.moe_runner_backend:
|
| 2009 |
+
return {"moe_runner_backend": moe_runner_backend}
|
| 2010 |
+
return {}
|
| 2011 |
+
|
| 2012 |
+
|
| 2013 |
+
@register_post_process
|
| 2014 |
+
def _moe_runner_fusion_disable(view: Any) -> dict:
|
| 2015 |
+
"""FlashInfer CuteDSL / TRT-LLM / TRT-LLM-routed MoE runners require the
|
| 2016 |
+
shared-experts fusion disabled; declared at the legacy write slots in
|
| 2017 |
+
_handle_moe_kernel_config (before the deprecated cutlass env override, so
|
| 2018 |
+
the runner value observed is the pre-override one)."""
|
| 2019 |
+
runner = view.moe_runner_backend
|
| 2020 |
+
if runner == "flashinfer_cutedsl":
|
| 2021 |
+
logger.warning(
|
| 2022 |
+
"FlashInfer CuteDSL MoE is enabled. --disable-shared-experts-fusion is automatically set."
|
| 2023 |
+
)
|
| 2024 |
+
return {"disable_shared_experts_fusion": True}
|
| 2025 |
+
if runner in ("flashinfer_trtllm", "experimental_sgl_trtllm"):
|
| 2026 |
+
logger.warning(
|
| 2027 |
+
"FlashInfer TRTLLM MoE is enabled. --disable-shared-experts-fusion is automatically set."
|
| 2028 |
+
)
|
| 2029 |
+
return {"disable_shared_experts_fusion": True}
|
| 2030 |
+
if runner == "flashinfer_trtllm_routed":
|
| 2031 |
+
logger.warning(
|
| 2032 |
+
"FlashInfer TRTLLM routed MoE is enabled. --disable-shared-experts-fusion is automatically set."
|
| 2033 |
+
)
|
| 2034 |
+
return {"disable_shared_experts_fusion": True}
|
| 2035 |
+
return {}
|
| 2036 |
+
|
| 2037 |
+
|
| 2038 |
+
def _a2a_fusion_adjustments(view: Any) -> dict:
|
| 2039 |
+
"""A2A-backend-driven shared-experts fusion adjustments, declared at the
|
| 2040 |
+
legacy write slots in _handle_a2a_moe: Waterfill requires the
|
| 2041 |
+
fusion enabled; FlashInfer A2A requires it disabled."""
|
| 2042 |
+
if view.moe_a2a_backend in ("deepep", "megamoe") and view.enable_waterfill:
|
| 2043 |
+
if view.disable_shared_experts_fusion:
|
| 2044 |
+
logger.warning(
|
| 2045 |
+
"disable_shared_experts_fusion is overridden to False because Waterfill requires shared expert fusion."
|
| 2046 |
+
)
|
| 2047 |
+
return {"disable_shared_experts_fusion": False}
|
| 2048 |
+
return {}
|
| 2049 |
+
if view.moe_a2a_backend == "flashinfer":
|
| 2050 |
+
logger.warning(
|
| 2051 |
+
"Flashinfer MoE A2A is enabled. --disable-shared-experts-fusion is automatically set."
|
| 2052 |
+
)
|
| 2053 |
+
return {"disable_shared_experts_fusion": True}
|
| 2054 |
+
return {}
|
| 2055 |
+
|
| 2056 |
+
|
| 2057 |
+
def _cutlass_moe_env_override(view: Any) -> dict:
|
| 2058 |
+
|
| 2059 |
+
if envs.SGLANG_CUTLASS_MOE.get():
|
| 2060 |
+
logger.warning(
|
| 2061 |
+
"SGLANG_CUTLASS_MOE is deprecated, use --moe-runner-backend=cutlass and/or --speculative-moe-runner-backend=cutlass instead"
|
| 2062 |
+
)
|
| 2063 |
+
assert view.quantization in [
|
| 2064 |
+
"fp8",
|
| 2065 |
+
"mxfp8",
|
| 2066 |
+
], "cutlass MoE is only supported with fp8/mxfp8 quantization"
|
| 2067 |
+
return {"moe_runner_backend": "cutlass"}
|
| 2068 |
+
return {}
|
| 2069 |
+
|
| 2070 |
+
|
| 2071 |
+
# Every A2A backend that forces expert parallelism to span the TP group.
|
| 2072 |
+
_A2A_EP_SPANNING_BACKENDS = frozenset(
|
| 2073 |
+
{"megamoe", "deepep", "mooncake", "nixl", "ascend_fuseep", "flashinfer", "mori"}
|
| 2074 |
+
)
|
| 2075 |
+
|
| 2076 |
+
|
| 2077 |
+
@register_post_process
|
| 2078 |
+
def _a2a_backend_overrides(view: Any) -> dict:
|
| 2079 |
+
|
| 2080 |
+
moe_a2a_backend = view.moe_a2a_backend
|
| 2081 |
+
if view.enable_waterfill and moe_a2a_backend not in ("deepep", "megamoe"):
|
| 2082 |
+
logger.warning(
|
| 2083 |
+
"moe_a2a_backend is overridden to 'deepep' because Waterfill "
|
| 2084 |
+
"requires the DeepEP or MegaMOE backend."
|
| 2085 |
+
)
|
| 2086 |
+
moe_a2a_backend = "deepep"
|
| 2087 |
+
if envs.SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE.get() and moe_a2a_backend != "megamoe":
|
| 2088 |
+
moe_a2a_backend = "megamoe"
|
| 2089 |
+
logger.info(
|
| 2090 |
+
"SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE is set, "
|
| 2091 |
+
"auto-configuring --moe-a2a-backend megamoe."
|
| 2092 |
+
)
|
| 2093 |
+
if moe_a2a_backend != view.moe_a2a_backend:
|
| 2094 |
+
return {"moe_a2a_backend": moe_a2a_backend}
|
| 2095 |
+
return {}
|
| 2096 |
+
|
| 2097 |
+
|
| 2098 |
+
@register_post_process
|
| 2099 |
+
def _a2a_ep_size(view: Any) -> dict:
|
| 2100 |
+
if view.moe_a2a_backend in _A2A_EP_SPANNING_BACKENDS:
|
| 2101 |
+
return {"ep_size": view.tp_size}
|
| 2102 |
+
return {}
|
| 2103 |
+
|
| 2104 |
+
|
| 2105 |
+
@register_post_process
|
| 2106 |
+
def _pipeline_parallel_overlap_disable(view: Any) -> dict:
|
| 2107 |
+
if view.pp_size > 1:
|
| 2108 |
+
logger.warning("Pipeline parallelism is incompatible with overlap schedule.")
|
| 2109 |
+
return {"disable_overlap_schedule": True}
|
| 2110 |
+
return {}
|
| 2111 |
+
|
| 2112 |
+
|
| 2113 |
+
@register_post_process
|
| 2114 |
+
def _speculative_moe_runner_default(view: Any) -> dict:
|
| 2115 |
+
"""Default the speculative (draft) MoE runner backend to the resolved
|
| 2116 |
+
target-model backend. Invoked at the head of the speculative-decoding
|
| 2117 |
+
hook, after the MoE kernel chain has resolved."""
|
| 2118 |
+
if view.speculative_moe_runner_backend is None:
|
| 2119 |
+
return {"speculative_moe_runner_backend": view.moe_runner_backend}
|
| 2120 |
+
return {}
|
| 2121 |
+
|
| 2122 |
+
|
| 2123 |
+
@register_post_process
|
| 2124 |
+
def _gguf_quantization(view: Any) -> dict:
|
| 2125 |
+
from sglang.srt.utils.hf_transformers_utils import check_gguf_file
|
| 2126 |
+
|
| 2127 |
+
if (view.load_format == "auto" or view.load_format == "gguf") and check_gguf_file(
|
| 2128 |
+
view.model_path
|
| 2129 |
+
):
|
| 2130 |
+
return {"quantization": "gguf"}
|
| 2131 |
+
return {}
|
| 2132 |
+
|
| 2133 |
+
|
| 2134 |
+
@register_post_process
|
| 2135 |
+
def _dllm_attention_backend(view: Any) -> dict:
|
| 2136 |
+
if view.dllm_algorithm is None:
|
| 2137 |
+
return {}
|
| 2138 |
+
if is_hip():
|
| 2139 |
+
if view.attention_backend not in ["triton", "aiter"]:
|
| 2140 |
+
logger.warning(
|
| 2141 |
+
"Attention backend is set to triton for diffusion LLM inference on AMD GPUs"
|
| 2142 |
+
)
|
| 2143 |
+
return {"attention_backend": "triton"}
|
| 2144 |
+
elif is_npu():
|
| 2145 |
+
if view.attention_backend != "ascend":
|
| 2146 |
+
logger.warning(
|
| 2147 |
+
"Attention backend is overridden to 'ascend' when running on NPU for diffusion LLM inference."
|
| 2148 |
+
)
|
| 2149 |
+
return {"attention_backend": "ascend"}
|
| 2150 |
+
elif view.cuda_graph_config.decode.backend != Backend.DISABLED:
|
| 2151 |
+
if view.attention_backend != "flashinfer":
|
| 2152 |
+
logger.warning(
|
| 2153 |
+
"Attention backend is set to flashinfer because of enabling cuda graph in diffusion LLM inference"
|
| 2154 |
+
)
|
| 2155 |
+
return {"attention_backend": "flashinfer"}
|
| 2156 |
+
return {}
|
| 2157 |
+
|
| 2158 |
+
|
| 2159 |
+
@register_post_process
|
| 2160 |
+
def _dllm_overlap_disable(view: Any) -> dict:
|
| 2161 |
+
if view.dllm_algorithm is None:
|
| 2162 |
+
return {}
|
| 2163 |
+
if view.disable_overlap_schedule:
|
| 2164 |
+
return {}
|
| 2165 |
+
logger.warning(
|
| 2166 |
+
"Overlap schedule is disabled because of using diffusion LLM inference"
|
| 2167 |
+
)
|
| 2168 |
+
return {"disable_overlap_schedule": True}
|
| 2169 |
+
|
| 2170 |
+
|
| 2171 |
+
@register_post_process
|
| 2172 |
+
def _dllm_page_size(view: Any) -> dict:
|
| 2173 |
+
if view.dllm_algorithm is None:
|
| 2174 |
+
return {}
|
| 2175 |
+
from sglang.srt.dllm.config import DllmConfig
|
| 2176 |
+
|
| 2177 |
+
config = DllmConfig.from_server_args(view)
|
| 2178 |
+
if not view.disable_radix_cache and view.page_size % config.block_size != 0:
|
| 2179 |
+
logger.warning(
|
| 2180 |
+
f"Setting page size to {config.block_size} for diffusion LLM inference"
|
| 2181 |
+
)
|
| 2182 |
+
return {"page_size": config.block_size}
|
| 2183 |
+
if view.page_size > config.block_size:
|
| 2184 |
+
# Legacy scheduler-init fallback, folded into the pass: the page
|
| 2185 |
+
# size must not exceed the dllm block size.
|
| 2186 |
+
logger.warning(
|
| 2187 |
+
"WARNING: "
|
| 2188 |
+
f"The page size {view.page_size} should not be larger than dllm block size {config.block_size}."
|
| 2189 |
+
f"Page size now falls back to {config.block_size}"
|
| 2190 |
+
)
|
| 2191 |
+
return {"page_size": config.block_size}
|
| 2192 |
+
return {}
|
| 2193 |
+
|
| 2194 |
+
|
| 2195 |
+
def validate_declarations(
|
| 2196 |
+
server_args: Any,
|
| 2197 |
+
declarations: Sequence[Tuple[str, Dict[str, Any]]],
|
| 2198 |
+
) -> None:
|
| 2199 |
+
"""Fail-fast whitelist check at declaration time: a registry typo or a
|
| 2200 |
+
not-yet-resolvable field must be rejected at its slot, not only at
|
| 2201 |
+
publish time. Declarations never mutate ``server_args``.
|
| 2202 |
+
"""
|
| 2203 |
+
# Non-dataclass fixtures carry no Arg metadata (mirrors the
|
| 2204 |
+
# resolvable_fields escape); only real ServerArgs is validated.
|
| 2205 |
+
if not dataclasses.is_dataclass(type(server_args)):
|
| 2206 |
+
return
|
| 2207 |
+
whitelist = resolvable_fields(type(server_args))
|
| 2208 |
+
for source, decl in declarations:
|
| 2209 |
+
unknown = set(decl) - whitelist
|
| 2210 |
+
if unknown:
|
| 2211 |
+
raise ValueError(
|
| 2212 |
+
f"{source}: {sorted(unknown)} not model-overridable; "
|
| 2213 |
+
"declarations are limited to the fields the publish gate "
|
| 2214 |
+
"accepts."
|
| 2215 |
+
)
|
| 2216 |
+
|
| 2217 |
+
|
| 2218 |
+
def _hrm_text_attention_force(view: Any) -> dict:
|
| 2219 |
+
"""HRM-Text's bidirectional prefix attention only works on the Triton
|
| 2220 |
+
backend. Invoked as the last attention declaration of the resolution
|
| 2221 |
+
(mirroring the legacy runner-side force, which ran after the whole
|
| 2222 |
+
pipeline)."""
|
| 2223 |
+
if view.attention_backend not in (None, "triton"):
|
| 2224 |
+
logger.warning(
|
| 2225 |
+
f"Overriding --attention-backend "
|
| 2226 |
+
f"{view.attention_backend!r} -> 'triton': only the "
|
| 2227 |
+
"Triton backend supports HRM-Text's bidirectional prefix "
|
| 2228 |
+
"attention."
|
| 2229 |
+
)
|
| 2230 |
+
return {"attention_backend": "triton"}
|
sglang_patch/srt/arg_groups/speculative_hook.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
from typing import TYPE_CHECKING, Optional
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
from sglang.srt.server_args import ServerArgs
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _disable_overlap_schedule_for_cpu(server_args: ServerArgs) -> None:
|
| 15 |
+
if server_args.device != "cpu" or server_args.disable_overlap_schedule:
|
| 16 |
+
return
|
| 17 |
+
|
| 18 |
+
server_args.disable_overlap_schedule = True
|
| 19 |
+
logger.warning(
|
| 20 |
+
"Overlap schedule is not implemented for speculative decoding on CPU."
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _resolve_speculative_algorithm_alias(
|
| 25 |
+
speculative_algorithm: Optional[str],
|
| 26 |
+
speculative_draft_model_path: Optional[str],
|
| 27 |
+
trust_remote_code: bool = False,
|
| 28 |
+
kwargs: Optional[dict] = {},
|
| 29 |
+
) -> Optional[str]:
|
| 30 |
+
"""Resolve CLI speculative algorithm; NEXTN/EAGLE may become FROZEN_KV_MTP for Gemma4 assistant drafts."""
|
| 31 |
+
|
| 32 |
+
is_gemma4_draft = False
|
| 33 |
+
if speculative_draft_model_path:
|
| 34 |
+
from sglang.srt.utils.hf_transformers_utils import get_config
|
| 35 |
+
|
| 36 |
+
cfg = get_config(
|
| 37 |
+
speculative_draft_model_path, trust_remote_code=trust_remote_code, **kwargs
|
| 38 |
+
)
|
| 39 |
+
draft_archs = getattr(cfg, "architectures", None) or []
|
| 40 |
+
is_gemma4_draft = any(
|
| 41 |
+
arch in ("Gemma4AssistantForCausalLM", "Gemma4UnifiedAssistantForCausalLM")
|
| 42 |
+
for arch in draft_archs
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
if speculative_algorithm == "EAGLE3" and is_gemma4_draft:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
"Gemma4AssistantForCausalLM draft requires "
|
| 48 |
+
"--speculative-algorithm NEXTN or EAGLE; EAGLE3 is "
|
| 49 |
+
"not supported for this draft architecture."
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
if speculative_algorithm == "NEXTN" or speculative_algorithm == "EAGLE":
|
| 53 |
+
if is_gemma4_draft:
|
| 54 |
+
logger.info(
|
| 55 |
+
"Detected Gemma4AssistantForCausalLM draft; "
|
| 56 |
+
f"promoting --speculative-algorithm {speculative_algorithm} to FROZEN_KV_MTP."
|
| 57 |
+
)
|
| 58 |
+
return "FROZEN_KV_MTP"
|
| 59 |
+
return "EAGLE"
|
| 60 |
+
|
| 61 |
+
return speculative_algorithm
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def handle_speculative_decoding(server_args: ServerArgs) -> None:
|
| 65 |
+
if (
|
| 66 |
+
server_args.speculative_draft_model_path is not None
|
| 67 |
+
and server_args.speculative_draft_model_revision is None
|
| 68 |
+
):
|
| 69 |
+
server_args.speculative_draft_model_revision = "main"
|
| 70 |
+
|
| 71 |
+
# Moved to the resolution pipeline (arg_groups/overrides.py:
|
| 72 |
+
# _speculative_moe_runner_default), invoked here at its legacy slot.
|
| 73 |
+
from sglang.srt.arg_groups.overrides import (
|
| 74 |
+
_speculative_moe_runner_default,
|
| 75 |
+
run_post_process_pass,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
run_post_process_pass(server_args, _speculative_moe_runner_default)
|
| 79 |
+
|
| 80 |
+
if server_args.speculative_algorithm is not None:
|
| 81 |
+
server_args.speculative_algorithm = server_args.speculative_algorithm.upper()
|
| 82 |
+
|
| 83 |
+
# Removal notice for the retired env var; raw os.getenv on purpose -- the
|
| 84 |
+
# Envs descriptor is gone. Drop this check after one release.
|
| 85 |
+
if os.getenv("SGLANG_ENABLE_SPEC_V2") is not None:
|
| 86 |
+
logger.warning(
|
| 87 |
+
"SGLANG_ENABLE_SPEC_V2 has been removed: speculative decoding "
|
| 88 |
+
"always runs the V2 worker. Use --disable-overlap-schedule to "
|
| 89 |
+
"select the non-overlap (synchronous) path."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
kwargs = {}
|
| 93 |
+
|
| 94 |
+
override_config_file = server_args.decrypted_draft_config_file
|
| 95 |
+
if override_config_file and override_config_file.strip():
|
| 96 |
+
kwargs["_configuration_file"] = override_config_file.strip()
|
| 97 |
+
|
| 98 |
+
server_args.speculative_algorithm = _resolve_speculative_algorithm_alias(
|
| 99 |
+
server_args.speculative_algorithm,
|
| 100 |
+
server_args.speculative_draft_model_path,
|
| 101 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 102 |
+
kwargs=kwargs,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Validate --speculative-draft-window-size once, regardless of algorithm.
|
| 106 |
+
# Consumed by DFLASH (compact draft KV cache) and Llama EAGLE-3 (drafter attention SWA).
|
| 107 |
+
if server_args.speculative_draft_window_size is not None:
|
| 108 |
+
window_size = int(server_args.speculative_draft_window_size)
|
| 109 |
+
if window_size <= 0:
|
| 110 |
+
raise ValueError(
|
| 111 |
+
f"--speculative-draft-window-size must be positive, got {window_size}."
|
| 112 |
+
)
|
| 113 |
+
server_args.speculative_draft_window_size = window_size
|
| 114 |
+
if server_args.speculative_algorithm not in ("EAGLE3", "DFLASH"):
|
| 115 |
+
logger.warning(
|
| 116 |
+
"--speculative-draft-window-size has no effect with "
|
| 117 |
+
"speculative_algorithm=%s (honored by Llama EAGLE-3 and DFLASH only).",
|
| 118 |
+
server_args.speculative_algorithm,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
algo = None
|
| 122 |
+
if server_args.speculative_algorithm is not None:
|
| 123 |
+
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
| 124 |
+
from sglang.srt.speculative.spec_registry import CustomSpecAlgo
|
| 125 |
+
|
| 126 |
+
algo = SpeculativeAlgorithm.from_string(server_args.speculative_algorithm)
|
| 127 |
+
|
| 128 |
+
# TODO: move the per-algorithm validation below into spec module hooks.
|
| 129 |
+
if isinstance(algo, CustomSpecAlgo) and algo.validate_server_args is not None:
|
| 130 |
+
algo.validate_server_args(server_args)
|
| 131 |
+
|
| 132 |
+
if server_args.speculative_skip_dp_mlp_sync:
|
| 133 |
+
assert server_args.speculative_algorithm == "EAGLE", (
|
| 134 |
+
"--speculative-skip-dp-mlp-sync is only supported with "
|
| 135 |
+
f"speculative_algorithm == EAGLE, got {server_args.speculative_algorithm}."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
if server_args.speculative_adaptive:
|
| 139 |
+
_maybe_disable_adaptive(server_args)
|
| 140 |
+
if server_args.speculative_adaptive:
|
| 141 |
+
_init_adaptive_speculative_params(server_args)
|
| 142 |
+
|
| 143 |
+
if algo is not None:
|
| 144 |
+
algo.handle_server_args(server_args)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _handle_dflash(server_args: ServerArgs) -> None:
|
| 148 |
+
from sglang.srt.arg_groups.overrides import resolved_view
|
| 149 |
+
|
| 150 |
+
if not server_args.device.startswith("cuda"):
|
| 151 |
+
raise ValueError("DFLASH speculative decoding only supports CUDA device.")
|
| 152 |
+
|
| 153 |
+
if resolved_view(server_args).enable_dp_attention:
|
| 154 |
+
raise ValueError(
|
| 155 |
+
"Currently DFLASH speculative decoding does not support dp attention."
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
if server_args.pp_size != 1:
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"Currently DFLASH speculative decoding only supports pp_size == 1."
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
if server_args.speculative_draft_model_path is None:
|
| 164 |
+
raise ValueError(
|
| 165 |
+
"DFLASH speculative decoding requires setting --speculative-draft-model-path."
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
# DFLASH does not use EAGLE-style `num_steps`/`topk`, but those fields still
|
| 169 |
+
# affect generic scheduler/KV-cache accounting (buffer sizing, KV freeing,
|
| 170 |
+
# RoPE reservation). Force them to 1 to avoid surprising memory behavior.
|
| 171 |
+
#
|
| 172 |
+
# For DFlash, the natural unit is `block_size` (verify window length).
|
| 173 |
+
if server_args.speculative_num_steps is None:
|
| 174 |
+
server_args.speculative_num_steps = 1
|
| 175 |
+
elif int(server_args.speculative_num_steps) != 1:
|
| 176 |
+
logger.warning(
|
| 177 |
+
"DFLASH only supports speculative_num_steps == 1; overriding speculative_num_steps=%s to 1.",
|
| 178 |
+
server_args.speculative_num_steps,
|
| 179 |
+
)
|
| 180 |
+
server_args.speculative_num_steps = 1
|
| 181 |
+
|
| 182 |
+
if server_args.speculative_eagle_topk is None:
|
| 183 |
+
server_args.speculative_eagle_topk = 1
|
| 184 |
+
elif int(server_args.speculative_eagle_topk) != 1:
|
| 185 |
+
logger.warning(
|
| 186 |
+
"DFLASH only supports speculative_eagle_topk == 1; overriding speculative_eagle_topk=%s to 1.",
|
| 187 |
+
server_args.speculative_eagle_topk,
|
| 188 |
+
)
|
| 189 |
+
server_args.speculative_eagle_topk = 1
|
| 190 |
+
|
| 191 |
+
if server_args.speculative_dflash_block_size is not None:
|
| 192 |
+
if int(server_args.speculative_dflash_block_size) <= 0:
|
| 193 |
+
raise ValueError(
|
| 194 |
+
"DFLASH requires --speculative-dflash-block-size to be positive, "
|
| 195 |
+
f"got {server_args.speculative_dflash_block_size}."
|
| 196 |
+
)
|
| 197 |
+
if server_args.speculative_num_draft_tokens is not None and int(
|
| 198 |
+
server_args.speculative_num_draft_tokens
|
| 199 |
+
) != int(server_args.speculative_dflash_block_size):
|
| 200 |
+
raise ValueError(
|
| 201 |
+
"Both --speculative-num-draft-tokens and --speculative-dflash-block-size are set "
|
| 202 |
+
"but they differ. For DFLASH they must match. "
|
| 203 |
+
f"speculative_num_draft_tokens={server_args.speculative_num_draft_tokens}, "
|
| 204 |
+
f"speculative_dflash_block_size={server_args.speculative_dflash_block_size}."
|
| 205 |
+
)
|
| 206 |
+
server_args.speculative_num_draft_tokens = int(
|
| 207 |
+
server_args.speculative_dflash_block_size
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
if server_args.speculative_num_draft_tokens is None:
|
| 211 |
+
from sglang.srt.speculative.dflash_utils import (
|
| 212 |
+
parse_dflash_draft_config,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
model_override_args = json.loads(server_args.json_model_override_args)
|
| 216 |
+
inferred_block_size = None
|
| 217 |
+
try:
|
| 218 |
+
from sglang.srt.utils.hf_transformers_utils import get_config
|
| 219 |
+
|
| 220 |
+
draft_hf_config = get_config(
|
| 221 |
+
server_args.speculative_draft_model_path,
|
| 222 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 223 |
+
revision=server_args.speculative_draft_model_revision,
|
| 224 |
+
model_override_args=model_override_args,
|
| 225 |
+
)
|
| 226 |
+
inferred_block_size = parse_dflash_draft_config(
|
| 227 |
+
draft_hf_config=draft_hf_config
|
| 228 |
+
).resolve_block_size(default=None)
|
| 229 |
+
except Exception as e:
|
| 230 |
+
logger.warning(
|
| 231 |
+
"Failed to infer DFLASH block_size from draft model config; "
|
| 232 |
+
"defaulting speculative_num_draft_tokens to 16. Error: %s",
|
| 233 |
+
e,
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
if inferred_block_size is None:
|
| 237 |
+
inferred_block_size = 16
|
| 238 |
+
logger.warning(
|
| 239 |
+
"speculative_num_draft_tokens is not set; defaulting to %d for DFLASH.",
|
| 240 |
+
inferred_block_size,
|
| 241 |
+
)
|
| 242 |
+
server_args.speculative_num_draft_tokens = inferred_block_size
|
| 243 |
+
|
| 244 |
+
if server_args.speculative_draft_window_size is not None:
|
| 245 |
+
draft_tokens = int(server_args.speculative_num_draft_tokens)
|
| 246 |
+
if server_args.speculative_draft_window_size < draft_tokens:
|
| 247 |
+
raise ValueError(
|
| 248 |
+
"--speculative-draft-window-size must be >= "
|
| 249 |
+
"--speculative-num-draft-tokens (block_size). "
|
| 250 |
+
f"window_size={server_args.speculative_draft_window_size}, block_size={draft_tokens}."
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
_resolve_dflash_draft_attention_backend(server_args)
|
| 254 |
+
|
| 255 |
+
if server_args.max_running_requests is None:
|
| 256 |
+
server_args.max_running_requests = 48
|
| 257 |
+
logger.warning(
|
| 258 |
+
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
if server_args.enable_mixed_chunk:
|
| 262 |
+
server_args.enable_mixed_chunk = False
|
| 263 |
+
logger.warning(
|
| 264 |
+
"Mixed chunked prefill is disabled because of using dflash speculative decoding."
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _target_checkpoint_bundles_dspark_draft(server_args: ServerArgs) -> bool:
|
| 269 |
+
from sglang.srt.speculative.dspark_components.dspark_config import (
|
| 270 |
+
checkpoint_bundles_dspark_draft,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
return checkpoint_bundles_dspark_draft(server_args.get_model_config().hf_config)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _handle_dspark(server_args: ServerArgs) -> None:
|
| 277 |
+
if not server_args.device.startswith("cuda"):
|
| 278 |
+
raise ValueError("DSpark speculative decoding only supports CUDA device.")
|
| 279 |
+
|
| 280 |
+
if server_args.enable_dp_attention:
|
| 281 |
+
if not server_args.enable_dp_lm_head:
|
| 282 |
+
raise ValueError("DSpark with dp attention requires --enable-dp-lm-head.")
|
| 283 |
+
if server_args.moe_a2a_backend != "none":
|
| 284 |
+
raise ValueError(
|
| 285 |
+
"DSpark with dp attention only supports the built-in TP MoE "
|
| 286 |
+
f"(moe_a2a_backend='none'), got {server_args.moe_a2a_backend!r}."
|
| 287 |
+
)
|
| 288 |
+
if server_args.attn_cp_size > 1:
|
| 289 |
+
raise ValueError(
|
| 290 |
+
"DSpark with dp attention does not support context parallel "
|
| 291 |
+
f"(attn_cp_size={server_args.attn_cp_size})."
|
| 292 |
+
)
|
| 293 |
+
if (
|
| 294 |
+
server_args.speculative_moe_a2a_backend is not None
|
| 295 |
+
and server_args.speculative_moe_a2a_backend != server_args.moe_a2a_backend
|
| 296 |
+
):
|
| 297 |
+
raise ValueError(
|
| 298 |
+
"DSpark ignores --speculative-moe-a2a-backend; with dp attention it "
|
| 299 |
+
f"must match the target moe_a2a_backend={server_args.moe_a2a_backend!r} "
|
| 300 |
+
f"(got {server_args.speculative_moe_a2a_backend!r})."
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
if server_args.pp_size != 1:
|
| 304 |
+
raise ValueError(
|
| 305 |
+
"Currently DSpark speculative decoding only supports pp_size == 1."
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
if server_args.speculative_draft_model_path is None:
|
| 309 |
+
if _target_checkpoint_bundles_dspark_draft(server_args):
|
| 310 |
+
server_args.speculative_draft_model_path = server_args.model_path
|
| 311 |
+
server_args.speculative_draft_model_revision = server_args.revision
|
| 312 |
+
logger.info(
|
| 313 |
+
"DSpark draft weights are bundled in the target checkpoint; "
|
| 314 |
+
"defaulting --speculative-draft-model-path to --model-path (%s).",
|
| 315 |
+
server_args.model_path,
|
| 316 |
+
)
|
| 317 |
+
else:
|
| 318 |
+
raise ValueError(
|
| 319 |
+
"DSpark dense speculative decoding requires setting "
|
| 320 |
+
"--speculative-draft-model-path."
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
if server_args.speculative_num_steps is None:
|
| 324 |
+
server_args.speculative_num_steps = 1
|
| 325 |
+
elif int(server_args.speculative_num_steps) != 1:
|
| 326 |
+
logger.warning(
|
| 327 |
+
"DSpark only supports speculative_num_steps == 1; overriding speculative_num_steps=%s to 1.",
|
| 328 |
+
server_args.speculative_num_steps,
|
| 329 |
+
)
|
| 330 |
+
server_args.speculative_num_steps = 1
|
| 331 |
+
|
| 332 |
+
if server_args.speculative_eagle_topk is None:
|
| 333 |
+
server_args.speculative_eagle_topk = 1
|
| 334 |
+
elif int(server_args.speculative_eagle_topk) != 1:
|
| 335 |
+
logger.warning(
|
| 336 |
+
"DSpark only supports speculative_eagle_topk == 1; overriding speculative_eagle_topk=%s to 1.",
|
| 337 |
+
server_args.speculative_eagle_topk,
|
| 338 |
+
)
|
| 339 |
+
server_args.speculative_eagle_topk = 1
|
| 340 |
+
|
| 341 |
+
gamma: Optional[int] = None
|
| 342 |
+
if server_args.speculative_dspark_block_size is not None:
|
| 343 |
+
if int(server_args.speculative_dspark_block_size) <= 0:
|
| 344 |
+
raise ValueError(
|
| 345 |
+
"DSpark requires --speculative-dspark-block-size to be positive, "
|
| 346 |
+
f"got {server_args.speculative_dspark_block_size}."
|
| 347 |
+
)
|
| 348 |
+
gamma = int(server_args.speculative_dspark_block_size)
|
| 349 |
+
else:
|
| 350 |
+
from sglang.srt.speculative.dspark_components.dspark_config import (
|
| 351 |
+
DEFAULT_DSPARK_GAMMA,
|
| 352 |
+
read_draft_checkpoint_gamma,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
try:
|
| 356 |
+
gamma = read_draft_checkpoint_gamma(server_args=server_args)
|
| 357 |
+
except Exception as e:
|
| 358 |
+
logger.warning(
|
| 359 |
+
"Failed to read DSpark gamma from draft model config; "
|
| 360 |
+
"cannot cross-check --speculative-num-draft-tokens. Error: %s",
|
| 361 |
+
e,
|
| 362 |
+
)
|
| 363 |
+
if gamma is None and server_args.speculative_num_draft_tokens is None:
|
| 364 |
+
gamma = DEFAULT_DSPARK_GAMMA
|
| 365 |
+
logger.warning(
|
| 366 |
+
"DSpark gamma is not set; defaulting to %d.",
|
| 367 |
+
gamma,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
if gamma is not None:
|
| 371 |
+
verify_window = int(gamma) + 1
|
| 372 |
+
if (
|
| 373 |
+
server_args.speculative_num_draft_tokens is not None
|
| 374 |
+
and int(server_args.speculative_num_draft_tokens) != verify_window
|
| 375 |
+
):
|
| 376 |
+
raise ValueError(
|
| 377 |
+
"DSpark speculative_num_draft_tokens must equal gamma + 1 "
|
| 378 |
+
f"(= {verify_window} for gamma={gamma}), but got "
|
| 379 |
+
f"speculative_num_draft_tokens={server_args.speculative_num_draft_tokens}."
|
| 380 |
+
)
|
| 381 |
+
server_args.speculative_num_draft_tokens = verify_window
|
| 382 |
+
|
| 383 |
+
if server_args.speculative_num_draft_tokens is None:
|
| 384 |
+
raise ValueError(
|
| 385 |
+
"DSpark could not resolve speculative_num_draft_tokens; set "
|
| 386 |
+
"--speculative-dspark-block-size (= gamma)."
|
| 387 |
+
)
|
| 388 |
+
if int(server_args.speculative_num_draft_tokens) < 2:
|
| 389 |
+
raise ValueError(
|
| 390 |
+
"DSpark speculative_num_draft_tokens must be >= 2 (= gamma + 1), "
|
| 391 |
+
f"got {server_args.speculative_num_draft_tokens}."
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
if server_args.max_running_requests is None:
|
| 395 |
+
server_args.max_running_requests = 48
|
| 396 |
+
logger.warning(
|
| 397 |
+
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
if server_args.enable_mixed_chunk:
|
| 401 |
+
server_args.enable_mixed_chunk = False
|
| 402 |
+
logger.warning(
|
| 403 |
+
"Mixed chunked prefill is disabled because of using dspark speculative decoding."
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
from sglang.srt.speculative.ragged_verify import (
|
| 407 |
+
RaggedVerifyMode,
|
| 408 |
+
read_ragged_verify_mode,
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
ragged_mode = read_ragged_verify_mode()
|
| 412 |
+
if (
|
| 413 |
+
server_args.speculative_dspark_align_verify_tokens_to_graph_tier
|
| 414 |
+
and ragged_mode is not RaggedVerifyMode.COMPACT
|
| 415 |
+
):
|
| 416 |
+
logger.warning(
|
| 417 |
+
"--speculative-dspark-align-verify-tokens-to-graph-tier only takes "
|
| 418 |
+
"effect with SGLANG_RAGGED_VERIFY_MODE=compact (got %r); it will be "
|
| 419 |
+
"a no-op.",
|
| 420 |
+
ragged_mode.value,
|
| 421 |
+
)
|
| 422 |
+
if (
|
| 423 |
+
server_args.speculative_dspark_sps_table_path
|
| 424 |
+
and ragged_mode is RaggedVerifyMode.STATIC
|
| 425 |
+
):
|
| 426 |
+
logger.warning(
|
| 427 |
+
"--speculative-dspark-sps-table-path feeds the ragged-verify budget "
|
| 428 |
+
"scheduler, which is off under SGLANG_RAGGED_VERIFY_MODE=static; it "
|
| 429 |
+
"will be a no-op."
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def _resolve_dflash_draft_attention_backend(server_args: ServerArgs) -> None:
|
| 434 |
+
"""Resolve `speculative_draft_attention_backend` to a final, supported value.
|
| 435 |
+
|
| 436 |
+
Consumed by ModelRunner's `is_draft_worker` override (one backend for all
|
| 437 |
+
draft modes).
|
| 438 |
+
"""
|
| 439 |
+
from sglang.srt.utils import is_hip
|
| 440 |
+
|
| 441 |
+
supported_draft_backends = ("flashinfer", "fa3", "fa4", "triton", "ascend")
|
| 442 |
+
# Use triton on ROCm (no FlashInfer), flashinfer on CUDA.
|
| 443 |
+
fallback_backend = "triton" if is_hip() else "flashinfer"
|
| 444 |
+
|
| 445 |
+
draft_backend = server_args.speculative_draft_attention_backend
|
| 446 |
+
if draft_backend is None:
|
| 447 |
+
from sglang.srt.arg_groups.overrides import (
|
| 448 |
+
attention_backends_of,
|
| 449 |
+
resolved_view,
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
draft_backend, _ = attention_backends_of(resolved_view(server_args))
|
| 453 |
+
if draft_backend is None:
|
| 454 |
+
draft_backend = fallback_backend
|
| 455 |
+
elif draft_backend == "trtllm_mha":
|
| 456 |
+
logger.warning(
|
| 457 |
+
"DFLASH draft worker does not support 'trtllm_mha' because the "
|
| 458 |
+
"draft path requires per-layer DFlash attention. Falling back to "
|
| 459 |
+
"'%s'.",
|
| 460 |
+
fallback_backend,
|
| 461 |
+
)
|
| 462 |
+
draft_backend = fallback_backend
|
| 463 |
+
elif draft_backend not in supported_draft_backends:
|
| 464 |
+
logger.warning(
|
| 465 |
+
"DFLASH draft worker only supports attention_backend in %s for now, "
|
| 466 |
+
"but got %r. Falling back to '%s'.",
|
| 467 |
+
supported_draft_backends,
|
| 468 |
+
draft_backend,
|
| 469 |
+
fallback_backend,
|
| 470 |
+
)
|
| 471 |
+
draft_backend = fallback_backend
|
| 472 |
+
# FIXME: avoid overriding server args directly; pass the resolved draft
|
| 473 |
+
# backend to the draft worker explicitly instead.
|
| 474 |
+
server_args.speculative_draft_attention_backend = draft_backend
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def _handle_frozen_kv_mtp(server_args: ServerArgs) -> None:
|
| 478 |
+
if server_args.max_running_requests is None:
|
| 479 |
+
server_args.max_running_requests = 48
|
| 480 |
+
logger.warning(
|
| 481 |
+
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
if server_args.enable_mixed_chunk:
|
| 485 |
+
server_args.enable_mixed_chunk = False
|
| 486 |
+
logger.warning(
|
| 487 |
+
"Mixed chunked prefill is disabled because of using "
|
| 488 |
+
"Frozen-KV MTP speculative decoding."
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def _handle_eagle_family(server_args: ServerArgs) -> None:
|
| 493 |
+
from sglang.srt.arg_groups.overrides import (
|
| 494 |
+
attention_backends_of,
|
| 495 |
+
resolved_view,
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
if (
|
| 499 |
+
server_args.speculative_algorithm == "STANDALONE"
|
| 500 |
+
and resolved_view(server_args).enable_dp_attention
|
| 501 |
+
):
|
| 502 |
+
# TODO: support dp attention for standalone speculative decoding
|
| 503 |
+
raise ValueError(
|
| 504 |
+
"Currently standalone speculative decoding does not support dp attention."
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
if server_args.max_running_requests is None:
|
| 508 |
+
server_args.max_running_requests = 48
|
| 509 |
+
logger.warning(
|
| 510 |
+
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
_disable_overlap_schedule_for_cpu(server_args)
|
| 514 |
+
|
| 515 |
+
if resolved_view(server_args).disable_overlap_schedule:
|
| 516 |
+
logger.warning(
|
| 517 |
+
"Non-overlap (synchronous) spec v2 is used for eagle/eagle3/standalone "
|
| 518 |
+
"speculative decoding."
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
if server_args.enable_mixed_chunk:
|
| 522 |
+
server_args.enable_mixed_chunk = False
|
| 523 |
+
logger.warning(
|
| 524 |
+
"Mixed chunked prefill is disabled because of using "
|
| 525 |
+
"eagle speculative decoding."
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
model_arch = server_args.get_model_config().hf_config.architectures[0]
|
| 529 |
+
if model_arch in [
|
| 530 |
+
"DeepseekV32ForCausalLM",
|
| 531 |
+
"DeepseekV3ForCausalLM",
|
| 532 |
+
"AgnesForCausalLM",
|
| 533 |
+
"Glm4MoeForCausalLM",
|
| 534 |
+
"Glm4MoeLiteForCausalLM",
|
| 535 |
+
"GlmMoeDsaForCausalLM",
|
| 536 |
+
"BailingMoeForCausalLM",
|
| 537 |
+
"BailingMoeV2ForCausalLM",
|
| 538 |
+
"BailingMoeV2_5ForCausalLM",
|
| 539 |
+
"MistralLarge3ForCausalLM",
|
| 540 |
+
"PixtralForConditionalGeneration",
|
| 541 |
+
"HYV3ForCausalLM",
|
| 542 |
+
]:
|
| 543 |
+
if server_args.speculative_draft_model_path is None:
|
| 544 |
+
server_args.speculative_draft_model_path = server_args.model_path
|
| 545 |
+
server_args.speculative_draft_model_revision = server_args.revision
|
| 546 |
+
else:
|
| 547 |
+
if model_arch not in [
|
| 548 |
+
"MistralLarge3ForCausalLM",
|
| 549 |
+
"PixtralForConditionalGeneration",
|
| 550 |
+
]:
|
| 551 |
+
logger.warning(
|
| 552 |
+
"DeepSeek MTP does not require setting speculative_draft_model_path."
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
if (
|
| 556 |
+
not server_args.speculative_adaptive
|
| 557 |
+
and server_args.speculative_num_steps is None
|
| 558 |
+
):
|
| 559 |
+
assert (
|
| 560 |
+
server_args.speculative_eagle_topk is None
|
| 561 |
+
and server_args.speculative_num_draft_tokens is None
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
(
|
| 565 |
+
server_args.speculative_num_steps,
|
| 566 |
+
server_args.speculative_eagle_topk,
|
| 567 |
+
server_args.speculative_num_draft_tokens,
|
| 568 |
+
) = _auto_choose_speculative_params(server_args, model_arch)
|
| 569 |
+
|
| 570 |
+
if "trtllm_mha" in attention_backends_of(resolved_view(server_args)):
|
| 571 |
+
if server_args.speculative_eagle_topk > 1:
|
| 572 |
+
raise ValueError(
|
| 573 |
+
"trtllm_mha backend only supports topk = 1 for speculative decoding."
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
if server_args.speculative_use_rejection_sampling:
|
| 577 |
+
# Resolved alias by now: NEXTN -> EAGLE, Gemma4 draft -> FROZEN_KV_MTP.
|
| 578 |
+
# Only the EAGLE/EAGLE3 draft workers emit a target-vocab proposal that
|
| 579 |
+
# the rejection-sampling kernel consumes; everything else (STANDALONE,
|
| 580 |
+
# FROZEN_KV_MTP, NGRAM, DFLASH) is unsupported.
|
| 581 |
+
if server_args.speculative_algorithm not in ("EAGLE", "EAGLE3"):
|
| 582 |
+
raise NotImplementedError(
|
| 583 |
+
"--speculative-use-rejection-sampling is only supported for "
|
| 584 |
+
"EAGLE / EAGLE3 / NEXTN, not "
|
| 585 |
+
f"speculative_algorithm={server_args.speculative_algorithm}."
|
| 586 |
+
)
|
| 587 |
+
if server_args.speculative_eagle_topk != 1:
|
| 588 |
+
raise ValueError(
|
| 589 |
+
"--speculative-use-rejection-sampling requires --speculative-eagle-topk=1."
|
| 590 |
+
)
|
| 591 |
+
if (
|
| 592 |
+
server_args.speculative_accept_threshold_single != 1.0
|
| 593 |
+
or server_args.speculative_accept_threshold_acc != 1.0
|
| 594 |
+
):
|
| 595 |
+
raise ValueError(
|
| 596 |
+
"--speculative-use-rejection-sampling is incompatible with "
|
| 597 |
+
"--speculative-accept-threshold-single / "
|
| 598 |
+
"--speculative-accept-threshold-acc; rejection sampling ignores "
|
| 599 |
+
"the accept thresholds."
|
| 600 |
+
)
|
| 601 |
+
if server_args.enable_deterministic_inference:
|
| 602 |
+
raise ValueError(
|
| 603 |
+
"--speculative-use-rejection-sampling is incompatible with "
|
| 604 |
+
"--enable-deterministic-inference; the sampling kernel draws "
|
| 605 |
+
"coins from the global RNG and is not batch-invariant."
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
from sglang.srt.arg_groups.overrides import resolved_view
|
| 609 |
+
|
| 610 |
+
if (
|
| 611 |
+
resolved_view(server_args).enable_multi_layer_eagle
|
| 612 |
+
and server_args.speculative_eagle_topk != 1
|
| 613 |
+
):
|
| 614 |
+
raise ValueError(
|
| 615 |
+
"--speculative-use-rejection-sampling with multi-layer EAGLE "
|
| 616 |
+
"(--enable-multi-layer-eagle) requires --speculative-eagle-topk 1; "
|
| 617 |
+
"rejection sampling is only implemented for the linear (topk=1) chain."
|
| 618 |
+
)
|
| 619 |
+
logger.info(
|
| 620 |
+
"Rejection sampling is enabled for speculative decoding "
|
| 621 |
+
"(speculative_use_rejection_sampling=True)."
|
| 622 |
+
)
|
| 623 |
+
|
| 624 |
+
if (
|
| 625 |
+
server_args.speculative_eagle_topk == 1
|
| 626 |
+
and server_args.speculative_num_draft_tokens
|
| 627 |
+
!= server_args.speculative_num_steps + 1
|
| 628 |
+
):
|
| 629 |
+
logger.warning(
|
| 630 |
+
"speculative_num_draft_tokens is adjusted to speculative_num_steps + 1 when speculative_eagle_topk == 1"
|
| 631 |
+
)
|
| 632 |
+
server_args.speculative_num_draft_tokens = server_args.speculative_num_steps + 1
|
| 633 |
+
|
| 634 |
+
# topk > 1 + page_size > 1 needs the two-pass cascade draft-decode (shared prefix
|
| 635 |
+
# pass + per-branch expand pass with prefix-tail dup). Only these backends implement
|
| 636 |
+
# it; flashmla / trtllm_mla / cutlass_mla can't express the per-branch tree, so reject.
|
| 637 |
+
_PAGE_TREE_SPEC_BACKENDS = ("flashinfer", "fa3", "triton")
|
| 638 |
+
view = resolved_view(server_args)
|
| 639 |
+
if (
|
| 640 |
+
server_args.speculative_eagle_topk > 1
|
| 641 |
+
and view.page_size > 1
|
| 642 |
+
and view.attention_backend not in _PAGE_TREE_SPEC_BACKENDS
|
| 643 |
+
):
|
| 644 |
+
raise ValueError(
|
| 645 |
+
f"speculative_eagle_topk > 1 with page_size > 1 is only supported on "
|
| 646 |
+
f"{_PAGE_TREE_SPEC_BACKENDS}; got attention_backend="
|
| 647 |
+
f"{view.attention_backend!r}. Use page_size == 1 or one of those backends."
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def _handle_ngram(server_args: ServerArgs) -> None:
|
| 652 |
+
if server_args.device not in ("cuda", "cpu"):
|
| 653 |
+
raise ValueError(
|
| 654 |
+
"Ngram speculative decoding only supports CUDA or CPU devices."
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
_disable_overlap_schedule_for_cpu(server_args)
|
| 658 |
+
|
| 659 |
+
if server_args.max_running_requests is None:
|
| 660 |
+
server_args.max_running_requests = 48
|
| 661 |
+
logger.warning(
|
| 662 |
+
"Max running requests is reset to 48 for speculative decoding. You can override this by explicitly setting --max-running-requests."
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
server_args.enable_mixed_chunk = False
|
| 666 |
+
server_args.speculative_eagle_topk = server_args.speculative_ngram_max_bfs_breadth
|
| 667 |
+
if server_args.speculative_num_draft_tokens is None:
|
| 668 |
+
server_args.speculative_num_draft_tokens = 12
|
| 669 |
+
logger.warning(
|
| 670 |
+
"speculative_num_draft_tokens is set to 12 by default for ngram speculative decoding. "
|
| 671 |
+
"You can override this by explicitly setting --speculative-num-draft-tokens."
|
| 672 |
+
)
|
| 673 |
+
if server_args.speculative_num_steps is None:
|
| 674 |
+
server_args.speculative_num_steps = (
|
| 675 |
+
server_args.speculative_num_draft_tokens
|
| 676 |
+
// server_args.speculative_eagle_topk
|
| 677 |
+
)
|
| 678 |
+
if server_args.speculative_ngram_external_corpus_path is not None:
|
| 679 |
+
if server_args.speculative_ngram_external_sam_budget <= 0:
|
| 680 |
+
raise ValueError(
|
| 681 |
+
"--speculative-ngram-external-sam-budget must be positive when "
|
| 682 |
+
"--speculative-ngram-external-corpus-path is set."
|
| 683 |
+
)
|
| 684 |
+
if server_args.speculative_ngram_external_corpus_max_tokens <= 0:
|
| 685 |
+
raise ValueError(
|
| 686 |
+
"--speculative-ngram-external-corpus-max-tokens must be positive when "
|
| 687 |
+
"--speculative-ngram-external-corpus-path is set."
|
| 688 |
+
)
|
| 689 |
+
if (
|
| 690 |
+
server_args.speculative_ngram_external_sam_budget
|
| 691 |
+
> server_args.speculative_num_draft_tokens - 1
|
| 692 |
+
):
|
| 693 |
+
raise ValueError(
|
| 694 |
+
"speculative_ngram_external_sam_budget must be less than or equal to "
|
| 695 |
+
f"speculative_num_draft_tokens - 1 ({server_args.speculative_num_draft_tokens - 1})."
|
| 696 |
+
)
|
| 697 |
+
logger.warning(
|
| 698 |
+
"The mixed chunked prefill are disabled because of "
|
| 699 |
+
"using ngram speculative decoding."
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
from sglang.srt.arg_groups.overrides import resolved_view
|
| 703 |
+
|
| 704 |
+
view = resolved_view(server_args)
|
| 705 |
+
if (
|
| 706 |
+
server_args.speculative_eagle_topk > 1
|
| 707 |
+
and view.page_size > 1
|
| 708 |
+
and view.attention_backend != "flashinfer"
|
| 709 |
+
):
|
| 710 |
+
raise ValueError(
|
| 711 |
+
f"speculative_eagle_topk({server_args.speculative_eagle_topk}) > 1 "
|
| 712 |
+
f"with page_size({view.page_size}) > 1 is unstable "
|
| 713 |
+
"and produces incorrect results for paged attention backends. "
|
| 714 |
+
"This combination is only supported for the 'flashinfer' backend."
|
| 715 |
+
)
|
| 716 |
+
if view.enable_dp_attention:
|
| 717 |
+
# TODO: support dp attention for ngram speculative decoding
|
| 718 |
+
raise ValueError(
|
| 719 |
+
"Currently ngram speculative decoding does not support dp attention."
|
| 720 |
+
)
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
def _maybe_disable_adaptive(server_args: ServerArgs) -> None:
|
| 724 |
+
from sglang.srt.speculative.adaptive_spec_params import (
|
| 725 |
+
adaptive_unsupported_reason,
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
reason = adaptive_unsupported_reason(server_args)
|
| 729 |
+
if reason is not None:
|
| 730 |
+
logger.warning(
|
| 731 |
+
f"speculative_adaptive disabled: {reason}. "
|
| 732 |
+
"Falling back to static speculative params."
|
| 733 |
+
)
|
| 734 |
+
server_args.speculative_adaptive = False
|
| 735 |
+
|
| 736 |
+
|
| 737 |
+
def _init_adaptive_speculative_params(server_args: ServerArgs) -> None:
|
| 738 |
+
from sglang.srt.speculative.adaptive_spec_params import (
|
| 739 |
+
resolve_candidate_steps_from_config,
|
| 740 |
+
)
|
| 741 |
+
|
| 742 |
+
candidate_steps = resolve_candidate_steps_from_config(
|
| 743 |
+
cfg_path=server_args.speculative_adaptive_config,
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
if server_args.speculative_eagle_topk is None:
|
| 747 |
+
server_args.speculative_eagle_topk = 1
|
| 748 |
+
|
| 749 |
+
if server_args.speculative_num_steps is None:
|
| 750 |
+
server_args.speculative_num_steps = candidate_steps[len(candidate_steps) // 2]
|
| 751 |
+
|
| 752 |
+
if server_args.speculative_num_steps not in candidate_steps:
|
| 753 |
+
raise ValueError(
|
| 754 |
+
f"--speculative-num-steps={server_args.speculative_num_steps} "
|
| 755 |
+
f"is not in the adaptive config candidate_steps {candidate_steps}. "
|
| 756 |
+
"Pass one of those values."
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
server_args.speculative_num_draft_tokens = server_args.speculative_num_steps + 1
|
| 760 |
+
|
| 761 |
+
|
| 762 |
+
def _auto_choose_speculative_params(server_args: ServerArgs, model_arch: str) -> tuple:
|
| 763 |
+
"""
|
| 764 |
+
Automatically choose the parameters for speculative decoding.
|
| 765 |
+
|
| 766 |
+
You can tune them on your own models and prompts with scripts/playground/bench_speculative.py
|
| 767 |
+
"""
|
| 768 |
+
if server_args.speculative_algorithm == "STANDALONE":
|
| 769 |
+
return (3, 1, 4)
|
| 770 |
+
if model_arch in ["LlamaForCausalLM"]:
|
| 771 |
+
return (5, 4, 8)
|
| 772 |
+
elif model_arch in [
|
| 773 |
+
"DeepseekV32ForCausalLM",
|
| 774 |
+
"DeepseekV3ForCausalLM",
|
| 775 |
+
"DeepseekV2ForCausalLM",
|
| 776 |
+
"GptOssForCausalLM",
|
| 777 |
+
"Glm4MoeForCausalLM",
|
| 778 |
+
"Glm4MoeLiteForCausalLM",
|
| 779 |
+
"GlmMoeDsaForCausalLM",
|
| 780 |
+
"BailingMoeForCausalLM",
|
| 781 |
+
"BailingMoeV2ForCausalLM",
|
| 782 |
+
"BailingMoeV2_5ForCausalLM",
|
| 783 |
+
"MistralLarge3ForCausalLM",
|
| 784 |
+
"PixtralForConditionalGeneration",
|
| 785 |
+
"MiMoV2ForCausalLM",
|
| 786 |
+
"MiMoV2FlashForCausalLM",
|
| 787 |
+
]:
|
| 788 |
+
return (3, 1, 4)
|
| 789 |
+
elif model_arch in ["Grok1ForCausalLM", "Grok1VForCausalLM"]:
|
| 790 |
+
return (5, 4, 8)
|
| 791 |
+
else:
|
| 792 |
+
return (3, 1, 4)
|
sglang_patch/srt/batch_overlap/operations_strategy.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from sglang.srt.batch_overlap import operations
|
| 7 |
+
from sglang.srt.batch_overlap.operations import Operation
|
| 8 |
+
from sglang.srt.layers.moe.token_dispatcher import DeepEPConfig
|
| 9 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
| 10 |
+
from sglang.srt.utils import is_hip
|
| 11 |
+
|
| 12 |
+
_is_hip = is_hip()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class OperationsStrategy:
|
| 17 |
+
operations: List[Operation]
|
| 18 |
+
deep_gemm_num_sms: Optional[int] = None
|
| 19 |
+
tbo_delta_stages: Optional[int] = None
|
| 20 |
+
|
| 21 |
+
@classmethod
|
| 22 |
+
def concat(cls, items: List["OperationsStrategy"]) -> "OperationsStrategy":
|
| 23 |
+
return OperationsStrategy(
|
| 24 |
+
operations=[x for item in items for x in item.operations],
|
| 25 |
+
deep_gemm_num_sms=_assert_all_same(
|
| 26 |
+
[item.deep_gemm_num_sms for item in items]
|
| 27 |
+
),
|
| 28 |
+
tbo_delta_stages=_assert_all_same(
|
| 29 |
+
[item.tbo_delta_stages for item in items]
|
| 30 |
+
),
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
def init_new_tbo(
|
| 35 |
+
layers: torch.nn.ModuleList,
|
| 36 |
+
forward_mode: ForwardMode,
|
| 37 |
+
) -> "OperationsStrategy":
|
| 38 |
+
layer_name = layers[0].__class__.__name__
|
| 39 |
+
if layer_name == "DeepseekV2DecoderLayer":
|
| 40 |
+
return OperationsStrategy.concat(
|
| 41 |
+
[
|
| 42 |
+
_compute_moe_deepseek_layer_operations_strategy_tbo(
|
| 43 |
+
layer, forward_mode
|
| 44 |
+
)
|
| 45 |
+
for layer in layers
|
| 46 |
+
]
|
| 47 |
+
)
|
| 48 |
+
elif layer_name == "Qwen3MoeDecoderLayer":
|
| 49 |
+
return OperationsStrategy.concat(
|
| 50 |
+
[
|
| 51 |
+
_compute_moe_qwen3_layer_operations_strategy_tbo(
|
| 52 |
+
layer, forward_mode
|
| 53 |
+
)
|
| 54 |
+
for layer in layers
|
| 55 |
+
]
|
| 56 |
+
)
|
| 57 |
+
elif layer_name == "MiMoV2DecoderLayer":
|
| 58 |
+
return OperationsStrategy.concat(
|
| 59 |
+
[
|
| 60 |
+
_compute_moe_mimov2_layer_operations_strategy_tbo(
|
| 61 |
+
layer, forward_mode
|
| 62 |
+
)
|
| 63 |
+
for layer in layers
|
| 64 |
+
]
|
| 65 |
+
)
|
| 66 |
+
elif layer_name == "AgnesDecoderLayer":
|
| 67 |
+
return OperationsStrategy.concat(
|
| 68 |
+
[
|
| 69 |
+
_compute_moe_deepseek_v4_layer_operations_strategy_tbo(
|
| 70 |
+
layer, forward_mode
|
| 71 |
+
)
|
| 72 |
+
for layer in layers
|
| 73 |
+
]
|
| 74 |
+
)
|
| 75 |
+
else:
|
| 76 |
+
raise NotImplementedError
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _assert_all_same(items: List):
|
| 80 |
+
assert all(item == items[0] for item in items)
|
| 81 |
+
return items[0]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# -------------------------------- Strategy for DeepSeek ---------------------------------------
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# TODO can refactor to make it more fancy if we have more complex strategies
|
| 88 |
+
def _compute_moe_deepseek_layer_operations_strategy_tbo(
|
| 89 |
+
layer: torch.nn.Module,
|
| 90 |
+
forward_mode: ForwardMode,
|
| 91 |
+
) -> OperationsStrategy:
|
| 92 |
+
assert layer.is_layer_sparse, "dense layer TBO not yet implemented"
|
| 93 |
+
if forward_mode == ForwardMode.EXTEND:
|
| 94 |
+
return _compute_moe_deepseek_blog_prefill(layer)
|
| 95 |
+
elif (
|
| 96 |
+
forward_mode == ForwardMode.DECODE or forward_mode == ForwardMode.TARGET_VERIFY
|
| 97 |
+
):
|
| 98 |
+
return _compute_moe_deepseek_blog_decode(layer)
|
| 99 |
+
else:
|
| 100 |
+
raise NotImplementedError(f"Unsupported {forward_mode=}")
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _compute_moe_deepseek_blog_prefill(layer):
|
| 104 |
+
device_properties = torch.cuda.get_device_properties(device="cuda")
|
| 105 |
+
total_num_sms = device_properties.multi_processor_count
|
| 106 |
+
deep_gemm_num_sms = None
|
| 107 |
+
if not _is_hip:
|
| 108 |
+
deep_gemm_num_sms = total_num_sms - DeepEPConfig.get_instance().num_sms
|
| 109 |
+
|
| 110 |
+
return OperationsStrategy(
|
| 111 |
+
deep_gemm_num_sms=deep_gemm_num_sms,
|
| 112 |
+
tbo_delta_stages=0,
|
| 113 |
+
operations=[
|
| 114 |
+
layer.op_comm_prepare_attn,
|
| 115 |
+
layer.self_attn.op_prepare,
|
| 116 |
+
layer.self_attn.op_core,
|
| 117 |
+
layer.op_comm_prepare_mlp,
|
| 118 |
+
layer.mlp.op_gate,
|
| 119 |
+
layer.mlp.op_select_experts,
|
| 120 |
+
layer.mlp.op_dispatch_a,
|
| 121 |
+
operations.YieldOperation(),
|
| 122 |
+
layer.mlp.op_dispatch_b,
|
| 123 |
+
layer.mlp.op_experts,
|
| 124 |
+
layer.mlp.op_combine_a,
|
| 125 |
+
operations.YieldOperation(),
|
| 126 |
+
layer.mlp.op_shared_experts,
|
| 127 |
+
layer.mlp.op_combine_b,
|
| 128 |
+
layer.mlp.op_output,
|
| 129 |
+
layer.op_comm_postprocess_layer,
|
| 130 |
+
],
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _compute_moe_deepseek_blog_decode(layer):
|
| 135 |
+
return OperationsStrategy(
|
| 136 |
+
deep_gemm_num_sms=None,
|
| 137 |
+
tbo_delta_stages=2,
|
| 138 |
+
operations=[
|
| 139 |
+
layer.op_comm_prepare_attn,
|
| 140 |
+
layer.self_attn.op_prepare,
|
| 141 |
+
operations.YieldOperation(),
|
| 142 |
+
layer.self_attn.op_core,
|
| 143 |
+
layer.op_comm_prepare_mlp,
|
| 144 |
+
layer.mlp.op_gate,
|
| 145 |
+
layer.mlp.op_select_experts,
|
| 146 |
+
operations.YieldOperation(),
|
| 147 |
+
layer.mlp.op_dispatch_a,
|
| 148 |
+
layer.mlp.op_shared_experts,
|
| 149 |
+
operations.YieldOperation(),
|
| 150 |
+
layer.mlp.op_dispatch_b,
|
| 151 |
+
layer.mlp.op_experts,
|
| 152 |
+
layer.mlp.op_combine_a,
|
| 153 |
+
operations.YieldOperation(),
|
| 154 |
+
layer.mlp.op_combine_b,
|
| 155 |
+
operations.YieldOperation(),
|
| 156 |
+
layer.mlp.op_output,
|
| 157 |
+
layer.op_comm_postprocess_layer,
|
| 158 |
+
],
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# -------------------------------- Strategy for DeepSeek V4 ---------------------------------------
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# DSV4 prefill TBO (EP / mori path). Cross-layer mHC fusion is disabled under
|
| 166 |
+
# TBO, so each layer is self-contained: attn-side mHC pre+norm -> attn ->
|
| 167 |
+
# ffn-side mHC pre+norm -> MoE (a2a dispatch/combine overlapped) -> mHC post.
|
| 168 |
+
# The MoE ops are reused from self.mlp (DeepseekV2MoE) and decompose
|
| 169 |
+
# forward_deepep; the layer-level op_mhc_* wrap DSV4's hc_pre / hc_post.
|
| 170 |
+
def _compute_moe_deepseek_v4_layer_operations_strategy_tbo(
|
| 171 |
+
layer: torch.nn.Module,
|
| 172 |
+
forward_mode: ForwardMode,
|
| 173 |
+
) -> OperationsStrategy:
|
| 174 |
+
if forward_mode == ForwardMode.EXTEND:
|
| 175 |
+
return _compute_moe_deepseek_v4_prefill(layer)
|
| 176 |
+
else:
|
| 177 |
+
# Decode TBO for DSV4 is not implemented yet (ATOM data: decode TBO
|
| 178 |
+
# regresses; needs cuda-graph capture work). Prefill-only for now.
|
| 179 |
+
raise NotImplementedError(
|
| 180 |
+
f"DeepseekV4 TBO only supports prefill (EXTEND), got {forward_mode=}"
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _compute_moe_deepseek_v4_prefill(layer):
|
| 185 |
+
from sglang.srt.layers.moe import get_moe_a2a_backend
|
| 186 |
+
|
| 187 |
+
if get_moe_a2a_backend().is_none():
|
| 188 |
+
# Non-EP DP TP-MoE: overlap the DP all_gatherv (gather) + reduce_scatterv
|
| 189 |
+
# (combine) with the other ubatch's attn+MoE compute (ATOM's DSV4 path).
|
| 190 |
+
ops = [
|
| 191 |
+
layer.op_mhc_prepare_attn,
|
| 192 |
+
layer.self_attn.op_attn,
|
| 193 |
+
layer.op_mhc_post_attn_pre_mlp,
|
| 194 |
+
layer.op_gather_a,
|
| 195 |
+
operations.YieldOperation(),
|
| 196 |
+
layer.op_gather_b,
|
| 197 |
+
layer.op_moe,
|
| 198 |
+
layer.op_combine_a,
|
| 199 |
+
operations.YieldOperation(),
|
| 200 |
+
layer.op_combine_b,
|
| 201 |
+
layer.op_mhc_postprocess,
|
| 202 |
+
]
|
| 203 |
+
else:
|
| 204 |
+
# EP / mori a2a: reuse DeepseekV2MoE's deepep dispatch/combine ops.
|
| 205 |
+
ops = [
|
| 206 |
+
layer.op_mhc_prepare_attn,
|
| 207 |
+
layer.self_attn.op_attn,
|
| 208 |
+
layer.op_mhc_post_attn_pre_mlp,
|
| 209 |
+
layer.mlp.op_gate,
|
| 210 |
+
layer.mlp.op_select_experts,
|
| 211 |
+
layer.mlp.op_dispatch_a,
|
| 212 |
+
operations.YieldOperation(),
|
| 213 |
+
layer.mlp.op_dispatch_b,
|
| 214 |
+
layer.mlp.op_experts,
|
| 215 |
+
layer.mlp.op_combine_a,
|
| 216 |
+
operations.YieldOperation(),
|
| 217 |
+
layer.mlp.op_shared_experts,
|
| 218 |
+
layer.mlp.op_combine_b,
|
| 219 |
+
layer.mlp.op_output,
|
| 220 |
+
layer.op_mhc_postprocess,
|
| 221 |
+
]
|
| 222 |
+
return OperationsStrategy(
|
| 223 |
+
deep_gemm_num_sms=None,
|
| 224 |
+
tbo_delta_stages=0,
|
| 225 |
+
operations=ops,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
# -------------------------------- Strategy for Qwen3 ---------------------------------------
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
# TODO: unstable, current strategy is almost the same as DeepSeek, keep redundant code here for
|
| 233 |
+
# convenience to adjust strategy
|
| 234 |
+
def _compute_moe_qwen3_layer_operations_strategy_tbo(
|
| 235 |
+
layer: torch.nn.Module,
|
| 236 |
+
forward_mode: ForwardMode,
|
| 237 |
+
) -> OperationsStrategy:
|
| 238 |
+
assert layer.is_layer_sparse, "qwen3 moe only support sparse layers"
|
| 239 |
+
if forward_mode == ForwardMode.EXTEND:
|
| 240 |
+
return _compute_moe_qwen3_prefill(layer)
|
| 241 |
+
elif (
|
| 242 |
+
forward_mode == ForwardMode.DECODE or forward_mode == ForwardMode.TARGET_VERIFY
|
| 243 |
+
):
|
| 244 |
+
return _compute_moe_qwen3_decode(layer)
|
| 245 |
+
else:
|
| 246 |
+
raise NotImplementedError(f"Unsupported {forward_mode=}")
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def _compute_moe_qwen3_prefill(layer):
|
| 250 |
+
device_properties = torch.cuda.get_device_properties(device="cuda")
|
| 251 |
+
total_num_sms = device_properties.multi_processor_count
|
| 252 |
+
deep_gemm_num_sms = None
|
| 253 |
+
if not _is_hip:
|
| 254 |
+
deep_gemm_num_sms = total_num_sms - DeepEPConfig.get_instance().num_sms
|
| 255 |
+
|
| 256 |
+
return OperationsStrategy(
|
| 257 |
+
deep_gemm_num_sms=deep_gemm_num_sms,
|
| 258 |
+
tbo_delta_stages=0,
|
| 259 |
+
operations=[
|
| 260 |
+
layer.op_comm_prepare_attn,
|
| 261 |
+
layer.self_attn.op_prepare,
|
| 262 |
+
layer.self_attn.op_core,
|
| 263 |
+
layer.op_comm_prepare_mlp,
|
| 264 |
+
layer.mlp.op_gate,
|
| 265 |
+
layer.mlp.op_select_experts,
|
| 266 |
+
layer.mlp.op_dispatch_a,
|
| 267 |
+
operations.YieldOperation(),
|
| 268 |
+
layer.mlp.op_dispatch_b,
|
| 269 |
+
layer.mlp.op_experts,
|
| 270 |
+
layer.mlp.op_combine_a,
|
| 271 |
+
operations.YieldOperation(),
|
| 272 |
+
layer.mlp.op_combine_b,
|
| 273 |
+
layer.mlp.op_output,
|
| 274 |
+
layer.op_comm_postprocess_layer,
|
| 275 |
+
],
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def _compute_moe_qwen3_decode(layer):
|
| 280 |
+
return OperationsStrategy(
|
| 281 |
+
deep_gemm_num_sms=None,
|
| 282 |
+
tbo_delta_stages=2,
|
| 283 |
+
operations=[
|
| 284 |
+
layer.op_comm_prepare_attn,
|
| 285 |
+
layer.self_attn.op_prepare,
|
| 286 |
+
operations.YieldOperation(),
|
| 287 |
+
layer.self_attn.op_core,
|
| 288 |
+
layer.op_comm_prepare_mlp,
|
| 289 |
+
layer.mlp.op_gate,
|
| 290 |
+
layer.mlp.op_select_experts,
|
| 291 |
+
operations.YieldOperation(),
|
| 292 |
+
layer.mlp.op_dispatch_a,
|
| 293 |
+
operations.YieldOperation(),
|
| 294 |
+
layer.mlp.op_dispatch_b,
|
| 295 |
+
layer.mlp.op_experts,
|
| 296 |
+
layer.mlp.op_combine_a,
|
| 297 |
+
operations.YieldOperation(),
|
| 298 |
+
layer.mlp.op_combine_b,
|
| 299 |
+
layer.mlp.op_output,
|
| 300 |
+
layer.op_comm_postprocess_layer,
|
| 301 |
+
operations.YieldOperation(),
|
| 302 |
+
],
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
# -------------------------------- Strategy for MiMoV2DecoderLayer ---------------------------------------
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
# TODO: unstable; current strategy matches DeepSeek for the common operations (MiMoV2 has no op_shared_experts),
|
| 310 |
+
# so we keep this redundant code here for convenience when adjusting the strategy
|
| 311 |
+
def _compute_moe_mimov2_layer_operations_strategy_tbo(
|
| 312 |
+
layer: torch.nn.Module,
|
| 313 |
+
forward_mode: ForwardMode,
|
| 314 |
+
) -> OperationsStrategy:
|
| 315 |
+
assert layer.is_layer_sparse, "MiMoV2DecoderLayer moe only support sparse layers"
|
| 316 |
+
if forward_mode == ForwardMode.EXTEND:
|
| 317 |
+
return _compute_moe_mimov2_prefill(layer)
|
| 318 |
+
elif (
|
| 319 |
+
forward_mode == ForwardMode.DECODE or forward_mode == ForwardMode.TARGET_VERIFY
|
| 320 |
+
):
|
| 321 |
+
return _compute_moe_mimov2_decode(layer)
|
| 322 |
+
else:
|
| 323 |
+
raise NotImplementedError(f"Unsupported {forward_mode=}")
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def _compute_moe_mimov2_prefill(layer):
|
| 327 |
+
device_properties = torch.cuda.get_device_properties(device="cuda")
|
| 328 |
+
total_num_sms = device_properties.multi_processor_count
|
| 329 |
+
deep_gemm_num_sms = total_num_sms - DeepEPConfig.get_instance().num_sms
|
| 330 |
+
|
| 331 |
+
return OperationsStrategy(
|
| 332 |
+
deep_gemm_num_sms=deep_gemm_num_sms,
|
| 333 |
+
tbo_delta_stages=0,
|
| 334 |
+
operations=[
|
| 335 |
+
layer.op_comm_prepare_attn,
|
| 336 |
+
layer.self_attn.op_prepare,
|
| 337 |
+
layer.self_attn.op_core,
|
| 338 |
+
layer.op_comm_prepare_mlp,
|
| 339 |
+
layer.mlp.op_gate,
|
| 340 |
+
layer.mlp.op_select_experts,
|
| 341 |
+
layer.mlp.op_dispatch_a,
|
| 342 |
+
operations.YieldOperation(),
|
| 343 |
+
layer.mlp.op_dispatch_b,
|
| 344 |
+
layer.mlp.op_experts,
|
| 345 |
+
layer.mlp.op_combine_a,
|
| 346 |
+
operations.YieldOperation(),
|
| 347 |
+
layer.mlp.op_combine_b,
|
| 348 |
+
layer.mlp.op_output,
|
| 349 |
+
layer.op_comm_postprocess_layer,
|
| 350 |
+
],
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def _compute_moe_mimov2_decode(layer):
|
| 355 |
+
return OperationsStrategy(
|
| 356 |
+
deep_gemm_num_sms=None,
|
| 357 |
+
tbo_delta_stages=2,
|
| 358 |
+
operations=[
|
| 359 |
+
layer.op_comm_prepare_attn,
|
| 360 |
+
layer.self_attn.op_prepare,
|
| 361 |
+
operations.YieldOperation(),
|
| 362 |
+
layer.self_attn.op_core,
|
| 363 |
+
layer.op_comm_prepare_mlp,
|
| 364 |
+
layer.mlp.op_gate,
|
| 365 |
+
layer.mlp.op_select_experts,
|
| 366 |
+
operations.YieldOperation(),
|
| 367 |
+
layer.mlp.op_dispatch_a,
|
| 368 |
+
operations.YieldOperation(),
|
| 369 |
+
layer.mlp.op_dispatch_b,
|
| 370 |
+
layer.mlp.op_experts,
|
| 371 |
+
layer.mlp.op_combine_a,
|
| 372 |
+
operations.YieldOperation(),
|
| 373 |
+
layer.mlp.op_combine_b,
|
| 374 |
+
layer.mlp.op_output,
|
| 375 |
+
layer.op_comm_postprocess_layer,
|
| 376 |
+
operations.YieldOperation(),
|
| 377 |
+
],
|
| 378 |
+
)
|
sglang_patch/srt/configs/agnes.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
from transformers import PretrainedConfig
|
| 7 |
+
|
| 8 |
+
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def try_detect_fp4_experts(model_path: str) -> Optional[bool]:
|
| 14 |
+
"""True = mxfp4-packed (U8/I8/F4), False = converted FP8 (F8_E4M3),
|
| 15 |
+
None when the header isn't readable (HF slug not cached yet, etc.).
|
| 16 |
+
Caller falls back to user default. Pure read; never mutates env.
|
| 17 |
+
"""
|
| 18 |
+
from sglang.srt.model_loader.weight_utils import (
|
| 19 |
+
probe_routed_expert_weight_dtype,
|
| 20 |
+
)
|
| 21 |
+
from sglang.srt.utils import find_local_repo_dir
|
| 22 |
+
|
| 23 |
+
if os.path.isdir(model_path):
|
| 24 |
+
local_path = model_path
|
| 25 |
+
else:
|
| 26 |
+
local_path = find_local_repo_dir(model_path)
|
| 27 |
+
if not local_path or not os.path.isdir(local_path):
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
dtype = probe_routed_expert_weight_dtype(local_path)
|
| 32 |
+
except Exception as e:
|
| 33 |
+
logger.warning("Failed to probe routed-expert dtype for %s: %s", model_path, e)
|
| 34 |
+
return None
|
| 35 |
+
if dtype is None:
|
| 36 |
+
return None
|
| 37 |
+
if dtype in ("U8", "I8", "F4"):
|
| 38 |
+
return True
|
| 39 |
+
if dtype == "F8_E4M3":
|
| 40 |
+
return False
|
| 41 |
+
logger.warning(
|
| 42 |
+
"Unexpected routed-expert safetensors dtype=%s for DeepSeek V4", dtype
|
| 43 |
+
)
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass(kw_only=True)
|
| 48 |
+
class AgnesConfig(PretrainedConfig):
|
| 49 |
+
architectures: List[str]
|
| 50 |
+
attention_bias: bool = False
|
| 51 |
+
attention_dropout: float = 0.0
|
| 52 |
+
bos_token_id: int = 0
|
| 53 |
+
eos_token_id: int = 1
|
| 54 |
+
ep_size: int = 1
|
| 55 |
+
first_k_dense_replace: int = 0
|
| 56 |
+
hidden_act: str = "silu"
|
| 57 |
+
hidden_size: int = 4096
|
| 58 |
+
index_head_dim: int = 128
|
| 59 |
+
index_n_heads: int = 64
|
| 60 |
+
index_topk: int = 512
|
| 61 |
+
initializer_range: float = 0.02
|
| 62 |
+
intermediate_size: int = 2048
|
| 63 |
+
kv_lora_rank: int = 512
|
| 64 |
+
max_position_embeddings: int = 65536
|
| 65 |
+
model_type: str = "agnes"
|
| 66 |
+
moe_intermediate_size: int = 2048
|
| 67 |
+
moe_layer_freq: int = 1
|
| 68 |
+
n_group: int = 8
|
| 69 |
+
n_routed_experts: int = 256
|
| 70 |
+
n_shared_experts: int = 1
|
| 71 |
+
norm_topk_prob: bool = True
|
| 72 |
+
|
| 73 |
+
num_attention_heads: int = 64
|
| 74 |
+
num_experts_per_tok: int = 6
|
| 75 |
+
num_hidden_layers: int = 43
|
| 76 |
+
num_key_value_heads: int = 1
|
| 77 |
+
|
| 78 |
+
q_lora_rank: int = 1024
|
| 79 |
+
qk_nope_head_dim: int = 448
|
| 80 |
+
qk_rope_head_dim: int = 64
|
| 81 |
+
|
| 82 |
+
quantization_config: QuantizationConfig = field(default_factory=QuantizationConfig)
|
| 83 |
+
|
| 84 |
+
rms_norm_eps: float = 1e-6
|
| 85 |
+
|
| 86 |
+
rope_scaling: Dict[str, float] = field(default_factory=dict)
|
| 87 |
+
rope_theta: int = 10000
|
| 88 |
+
|
| 89 |
+
routed_scaling_factor: float = 1.5
|
| 90 |
+
scoring_func: str = "sqrtsoftplus"
|
| 91 |
+
|
| 92 |
+
tie_word_embeddings: bool = False
|
| 93 |
+
|
| 94 |
+
topk_group: int = 8
|
| 95 |
+
topk_method: str = "noaux_tc"
|
| 96 |
+
|
| 97 |
+
use_cache: bool = True
|
| 98 |
+
v_head_dim: int = 512
|
| 99 |
+
vocab_size: int = 129280
|
| 100 |
+
o_lora_rank: int = 1024
|
| 101 |
+
o_groups: int = 8
|
| 102 |
+
window_size: int = 128
|
| 103 |
+
|
| 104 |
+
compress_rope_theta: int = 40000
|
| 105 |
+
compress_ratios: List[int] = field(default_factory=list)
|
| 106 |
+
|
| 107 |
+
n_hash_layers: int = 3
|
| 108 |
+
hc_mult: int = 4
|
| 109 |
+
hc_sinkhorn_iters: int = 20
|
| 110 |
+
hc_eps: float = 1e-6
|
sglang_patch/srt/configs/model_config.py
ADDED
|
@@ -0,0 +1,2055 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023-2024 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
|
| 15 |
+
import copy
|
| 16 |
+
import json
|
| 17 |
+
import logging
|
| 18 |
+
import math
|
| 19 |
+
import os
|
| 20 |
+
from enum import Enum, IntEnum, auto
|
| 21 |
+
from functools import cached_property
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
from typing import Any, List, Optional, Set, Union
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
from transformers import PretrainedConfig
|
| 27 |
+
|
| 28 |
+
from sglang.srt.configs.linear_attn_model_registry import get_linear_attn_config
|
| 29 |
+
from sglang.srt.environ import envs
|
| 30 |
+
from sglang.srt.layers.quantization import QUANTIZATION_METHODS
|
| 31 |
+
from sglang.srt.server_args import ServerArgs
|
| 32 |
+
from sglang.srt.utils import is_hip, is_sm100_supported, retry
|
| 33 |
+
from sglang.srt.utils.hf_transformers_utils import (
|
| 34 |
+
get_config,
|
| 35 |
+
get_context_length,
|
| 36 |
+
get_generation_config,
|
| 37 |
+
get_hf_text_config,
|
| 38 |
+
get_sparse_attention_config,
|
| 39 |
+
)
|
| 40 |
+
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
|
| 41 |
+
from sglang.utils import is_in_ci
|
| 42 |
+
|
| 43 |
+
logger = logging.getLogger(__name__)
|
| 44 |
+
|
| 45 |
+
MIMO_V2_MODEL_ARCHS = (
|
| 46 |
+
"MiMoV2ForCausalLM",
|
| 47 |
+
"MiMoV2FlashForCausalLM",
|
| 48 |
+
)
|
| 49 |
+
MIMO_V2_MULTIMODAL_ARCHS = ("MiMoV2ForCausalLM",)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_mimo_v2_fused_qkv_expected_tp_size(hf_config):
|
| 53 |
+
layout = getattr(hf_config, "attention_projection_layout", None)
|
| 54 |
+
if layout is None:
|
| 55 |
+
return None
|
| 56 |
+
if layout != "fused_qkv":
|
| 57 |
+
raise ValueError(
|
| 58 |
+
"MiMoV2 hf_config has unsupported "
|
| 59 |
+
f"attention_projection_layout={layout!r}; expected 'fused_qkv' "
|
| 60 |
+
"or unset."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
num_key_value_heads = getattr(hf_config, "num_key_value_heads", None)
|
| 64 |
+
text_config = getattr(hf_config, "text_config", None)
|
| 65 |
+
if num_key_value_heads is None and text_config is not None:
|
| 66 |
+
num_key_value_heads = getattr(text_config, "num_key_value_heads", None)
|
| 67 |
+
if num_key_value_heads is None:
|
| 68 |
+
raise ValueError(
|
| 69 |
+
"MiMoV2 hf_config has attention_projection_layout='fused_qkv' "
|
| 70 |
+
"but num_key_value_heads is missing; this value is required to "
|
| 71 |
+
"derive the fused qkv_proj TP size."
|
| 72 |
+
)
|
| 73 |
+
return num_key_value_heads
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class AttentionArch(IntEnum):
|
| 77 |
+
MLA = auto()
|
| 78 |
+
MHA = auto()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class ModelImpl(str, Enum):
|
| 82 |
+
AUTO = "auto"
|
| 83 |
+
SGLANG = "sglang"
|
| 84 |
+
TRANSFORMERS = "transformers"
|
| 85 |
+
MINDSPORE = "mindspore"
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _hf_arch(config) -> Optional[str]:
|
| 89 |
+
"""First architecture from a HF config dict or PretrainedConfig (or None)."""
|
| 90 |
+
archs = (
|
| 91 |
+
config.get("architectures")
|
| 92 |
+
if isinstance(config, dict)
|
| 93 |
+
else getattr(config, "architectures", None)
|
| 94 |
+
)
|
| 95 |
+
return archs[0] if archs else None
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _hf_attr(config, name):
|
| 99 |
+
"""Read an arbitrary field from a HF config dict or PretrainedConfig."""
|
| 100 |
+
if isinstance(config, dict):
|
| 101 |
+
return config.get(name)
|
| 102 |
+
return getattr(config, name, None)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def is_deepseek_dsa(config) -> bool:
|
| 106 |
+
return (
|
| 107 |
+
_hf_arch(config)
|
| 108 |
+
in (
|
| 109 |
+
"DeepseekV3ForCausalLM",
|
| 110 |
+
"DeepseekV32ForCausalLM",
|
| 111 |
+
"DeepseekV3ForCausalLMNextN",
|
| 112 |
+
"MistralLarge3ForCausalLM",
|
| 113 |
+
"PixtralForConditionalGeneration",
|
| 114 |
+
"GlmMoeDsaForCausalLM",
|
| 115 |
+
"GlmMoeDsaForCausalLMNextN",
|
| 116 |
+
"LongcatFlashForCausalLM",
|
| 117 |
+
"LongcatFlashForCausalLMNextN",
|
| 118 |
+
)
|
| 119 |
+
and _hf_attr(config, "index_topk") is not None
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def is_deepseek_v4(config) -> bool:
|
| 124 |
+
return _hf_arch(config) in (
|
| 125 |
+
"AgnesForCausalLM",
|
| 126 |
+
"AgnesForCausalLMNextN",
|
| 127 |
+
"AgnesForCausalLMDSpark",
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def get_dsa_index_head_dim(config: PretrainedConfig) -> int:
|
| 132 |
+
assert is_deepseek_dsa(config) or is_deepseek_v4(config)
|
| 133 |
+
return config.index_head_dim
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def is_minimax_sparse(config: PretrainedConfig) -> bool:
|
| 137 |
+
arch = (config.architectures or [None])[0]
|
| 138 |
+
return arch in (
|
| 139 |
+
"MiniMaxM3SparseForCausalLM",
|
| 140 |
+
"MiniMaxM3SparseForConditionalGeneration",
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def get_minimax_sparse_attention_config(config: PretrainedConfig) -> dict:
|
| 145 |
+
text_cfg = getattr(config, "text_config", None)
|
| 146 |
+
cfg = (
|
| 147 |
+
getattr(text_cfg, "sparse_attention_config", None)
|
| 148 |
+
if text_cfg is not None
|
| 149 |
+
else None
|
| 150 |
+
)
|
| 151 |
+
if cfg is None:
|
| 152 |
+
cfg = getattr(config, "sparse_attention_config", None)
|
| 153 |
+
if cfg is None:
|
| 154 |
+
raise ValueError("Could not find sparse config. Is it MiniMax M3 Sparse model?")
|
| 155 |
+
return cfg
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def get_minimax_sparse_layer_ids(sparse_cfg: dict) -> tuple[list[int], list[int]]:
|
| 159 |
+
sparse_freq = sparse_cfg["sparse_attention_freq"]
|
| 160 |
+
dense_layer_ids = [i for i, f in enumerate(sparse_freq) if f == 0]
|
| 161 |
+
sparse_layer_ids = [i for i, f in enumerate(sparse_freq) if f != 0]
|
| 162 |
+
return dense_layer_ids, sparse_layer_ids
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def get_minimax_sparse_disable_value_layer_ids(sparse_cfg: dict) -> list[int]:
|
| 166 |
+
flags = sparse_cfg.get("sparse_disable_index_value")
|
| 167 |
+
if flags is None:
|
| 168 |
+
return []
|
| 169 |
+
return [i for i, f in enumerate(flags) if f != 0]
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def get_minimax_sparse_score_type(sparse_cfg: dict) -> str:
|
| 173 |
+
score_type = sparse_cfg.get("sparse_score_type", "max")
|
| 174 |
+
assert score_type in (
|
| 175 |
+
"max",
|
| 176 |
+
"lse",
|
| 177 |
+
), f"sparse_score_type must be 'max' or 'lse', got {score_type!r}"
|
| 178 |
+
return score_type
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def get_dsa_index_topk(config: PretrainedConfig) -> int:
|
| 182 |
+
assert is_deepseek_dsa(config)
|
| 183 |
+
return config.index_topk
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def dsa_layer_skips_topk(config: PretrainedConfig, layer_id: int) -> bool:
|
| 187 |
+
"""Return whether a DSA layer reuses the previous layer's top-k indices."""
|
| 188 |
+
assert is_deepseek_dsa(config)
|
| 189 |
+
|
| 190 |
+
pattern = getattr(config, "index_topk_pattern", None)
|
| 191 |
+
if pattern is not None:
|
| 192 |
+
return layer_id < len(pattern) and pattern[layer_id] == "S"
|
| 193 |
+
|
| 194 |
+
freq = getattr(config, "index_topk_freq", 1)
|
| 195 |
+
if freq is None:
|
| 196 |
+
freq = 1
|
| 197 |
+
assert freq > 0, f"index_topk_freq must be positive, got {freq}"
|
| 198 |
+
offset = getattr(config, "index_skip_topk_offset", None)
|
| 199 |
+
if offset is not None:
|
| 200 |
+
assert offset > 0, (
|
| 201 |
+
"index_skip_topk_offset must be positive; offset <= 0 "
|
| 202 |
+
"marks layer 0 as skip_topk with no prior topk to reuse"
|
| 203 |
+
)
|
| 204 |
+
return max(layer_id - offset + 1, 0) % freq != 0
|
| 205 |
+
|
| 206 |
+
return max(layer_id - 1, 0) % freq != 0
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def get_dsa_index_n_heads(config: PretrainedConfig) -> int:
|
| 210 |
+
assert is_deepseek_dsa(config)
|
| 211 |
+
return config.index_n_heads
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def get_num_indexer_layers(config) -> int:
|
| 215 |
+
"""Layer count for the global indexer-topk capturer's host buffer.
|
| 216 |
+
|
| 217 |
+
DSA models (V3.2) instantiate an Indexer on every transformer layer.
|
| 218 |
+
With index_topk_freq > 1 some layers reuse prev layer's topk; those still
|
| 219 |
+
get a slot (mirrored at the MLA call site). DSv4 has C4 indexers only on
|
| 220 |
+
layers whose compress_ratio == 4. Other architectures: set
|
| 221 |
+
num_indexer_layers on hf_text_config; 0 disables the capturer.
|
| 222 |
+
"""
|
| 223 |
+
if is_deepseek_dsa(config):
|
| 224 |
+
return config.num_hidden_layers
|
| 225 |
+
if is_deepseek_v4(config):
|
| 226 |
+
compress_ratios = getattr(config, "compress_ratios", None) or []
|
| 227 |
+
return sum(1 for r in compress_ratios if r == 4)
|
| 228 |
+
return getattr(config, "num_indexer_layers", 0)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class ModelConfig:
|
| 232 |
+
def __init__(
|
| 233 |
+
self,
|
| 234 |
+
model_path: str,
|
| 235 |
+
trust_remote_code: bool = True,
|
| 236 |
+
revision: Optional[str] = None,
|
| 237 |
+
context_length: Optional[int] = None,
|
| 238 |
+
model_override_args: str = "{}",
|
| 239 |
+
is_embedding: Optional[bool] = None,
|
| 240 |
+
enable_multimodal: Optional[bool] = None,
|
| 241 |
+
dtype: str = "auto",
|
| 242 |
+
quantization: Optional[str] = None,
|
| 243 |
+
override_config_file: Optional[str] = None,
|
| 244 |
+
is_draft_model: bool = False,
|
| 245 |
+
model_impl: Union[str, ModelImpl] = ModelImpl.AUTO,
|
| 246 |
+
sampling_defaults: str = "openai",
|
| 247 |
+
quantize_and_serve: bool = False,
|
| 248 |
+
is_multi_layer_eagle: bool = False,
|
| 249 |
+
encoder_only: bool = False,
|
| 250 |
+
language_only: bool = False,
|
| 251 |
+
disable_hybrid_swa_memory: bool = False,
|
| 252 |
+
model_config_parser: str = "auto",
|
| 253 |
+
speculative_algorithm: Optional[str] = None,
|
| 254 |
+
) -> None:
|
| 255 |
+
# Parse args
|
| 256 |
+
self.model_path = model_path
|
| 257 |
+
self.revision = revision
|
| 258 |
+
self.quantization = quantization
|
| 259 |
+
self.is_draft_model = is_draft_model
|
| 260 |
+
self.speculative_algorithm = speculative_algorithm
|
| 261 |
+
self.model_impl = model_impl
|
| 262 |
+
self.sampling_defaults = sampling_defaults
|
| 263 |
+
self.quantize_and_serve = quantize_and_serve
|
| 264 |
+
self.is_multi_layer_eagle = is_multi_layer_eagle
|
| 265 |
+
self.disable_hybrid_swa_memory = disable_hybrid_swa_memory
|
| 266 |
+
self.model_config_parser = model_config_parser
|
| 267 |
+
|
| 268 |
+
# Validate quantize_and_serve configuration
|
| 269 |
+
self._validate_quantize_and_serve_config()
|
| 270 |
+
|
| 271 |
+
# Get hf config
|
| 272 |
+
self._maybe_pull_model_for_runai(self.model_path)
|
| 273 |
+
self._maybe_pull_model_tokenizer_from_remote()
|
| 274 |
+
self.model_override_args = json.loads(model_override_args)
|
| 275 |
+
kwargs = {}
|
| 276 |
+
if override_config_file and override_config_file.strip():
|
| 277 |
+
kwargs["_configuration_file"] = override_config_file.strip()
|
| 278 |
+
# get_config() is cached. ModelConfig mutates hf_config for draft-model
|
| 279 |
+
# remapping and architecture-specific normalization, so each instance
|
| 280 |
+
# must own an isolated copy.
|
| 281 |
+
self.hf_config = copy.deepcopy(
|
| 282 |
+
get_config(
|
| 283 |
+
self.model_path,
|
| 284 |
+
trust_remote_code=trust_remote_code,
|
| 285 |
+
revision=revision,
|
| 286 |
+
model_override_args=self.model_override_args,
|
| 287 |
+
model_config_parser=model_config_parser,
|
| 288 |
+
**kwargs,
|
| 289 |
+
)
|
| 290 |
+
)
|
| 291 |
+
self.hf_text_config = get_hf_text_config(self.hf_config)
|
| 292 |
+
|
| 293 |
+
rope_scaling = getattr(self.hf_text_config, "rope_parameters", None) or getattr(
|
| 294 |
+
self.hf_text_config, "rope_scaling", {}
|
| 295 |
+
)
|
| 296 |
+
self.model_is_mrope = (
|
| 297 |
+
rope_scaling is not None and "mrope_section" in rope_scaling
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
self.hf_generation_config = get_generation_config(
|
| 301 |
+
self.model_path,
|
| 302 |
+
trust_remote_code=trust_remote_code,
|
| 303 |
+
revision=revision,
|
| 304 |
+
**kwargs,
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# Set enable_multimodal
|
| 308 |
+
if enable_multimodal is None:
|
| 309 |
+
mm_disabled_models = [
|
| 310 |
+
"Gemma3ForConditionalGeneration",
|
| 311 |
+
"Llama4ForConditionalGeneration",
|
| 312 |
+
"Step3VLForConditionalGeneration",
|
| 313 |
+
"InklingForConditionalGeneration",
|
| 314 |
+
]
|
| 315 |
+
if (
|
| 316 |
+
self.hf_config.architectures[0] in mm_disabled_models
|
| 317 |
+
and self.model_impl != ModelImpl.TRANSFORMERS
|
| 318 |
+
):
|
| 319 |
+
enable_multimodal = False
|
| 320 |
+
logger.info(
|
| 321 |
+
f"Multimodal is disabled for {self.hf_config.model_type}. To enable it, set --enable-multimodal."
|
| 322 |
+
)
|
| 323 |
+
elif self.hf_config.architectures[0] in MIMO_V2_MULTIMODAL_ARCHS and not (
|
| 324 |
+
hasattr(self.hf_config, "vision_config")
|
| 325 |
+
and hasattr(self.hf_config, "audio_config")
|
| 326 |
+
):
|
| 327 |
+
enable_multimodal = False
|
| 328 |
+
logger.info(
|
| 329 |
+
"Multimodal is disabled for this MiMoV2 checkpoint: "
|
| 330 |
+
"vision_config/audio_config not found in the model config "
|
| 331 |
+
"(likely a text-only MiMoV2 variant)."
|
| 332 |
+
)
|
| 333 |
+
else:
|
| 334 |
+
enable_multimodal = True
|
| 335 |
+
|
| 336 |
+
# Config draft model
|
| 337 |
+
self._config_draft_model()
|
| 338 |
+
|
| 339 |
+
# DSV4 expert layout: env (default True = mxfp4) applies only to V4.
|
| 340 |
+
# Other FP8 MoE models (for example DeepSeek V3.2) must keep the normal
|
| 341 |
+
# FP8 expert tensor layout.
|
| 342 |
+
self.is_fp4_experts: bool = False
|
| 343 |
+
if is_deepseek_v4(self.hf_config):
|
| 344 |
+
self.is_fp4_experts = envs.SGLANG_DSV4_FP4_EXPERTS.get()
|
| 345 |
+
if (
|
| 346 |
+
not envs.SGLANG_DSV4_FP4_EXPERTS.is_set()
|
| 347 |
+
or envs.SGLANG_DSV4_FP4_DEQUANT.is_set()
|
| 348 |
+
):
|
| 349 |
+
from sglang.srt.configs.agnes import try_detect_fp4_experts
|
| 350 |
+
|
| 351 |
+
detected = try_detect_fp4_experts(self.model_path)
|
| 352 |
+
if detected is not None:
|
| 353 |
+
self.is_fp4_experts = detected
|
| 354 |
+
logger.info(
|
| 355 |
+
"Auto-detected DSV4 routed-expert layout: is_fp4_experts=%s",
|
| 356 |
+
self.is_fp4_experts,
|
| 357 |
+
)
|
| 358 |
+
if envs.SGLANG_DSV4_FP4_DEQUANT.get():
|
| 359 |
+
envs.SGLANG_DSV4_FP4_DEQUANT.set(self.is_fp4_experts is not None)
|
| 360 |
+
|
| 361 |
+
# HF config.json inherits topk_group=4 from the V3 template, but
|
| 362 |
+
# DSV4 trains with no group limiting (sqrtsoftplus + full-expert
|
| 363 |
+
# top-k). Force topk_group == n_group so deepseek_v2.py:531's
|
| 364 |
+
# `n_group > topk_group` evaluates False and routes to the
|
| 365 |
+
# ungrouped sqrtsoftplus path. The grouped impl only supports
|
| 366 |
+
# sigmoid scoring (topk.py:722) and would silently corrupt expert
|
| 367 |
+
# weights if hit.
|
| 368 |
+
n_group = getattr(self.hf_config, "n_group", None)
|
| 369 |
+
if n_group is not None:
|
| 370 |
+
self.hf_config.topk_group = n_group
|
| 371 |
+
|
| 372 |
+
# Handle hybrid NVFP4 moe (nvidia/DeepSeek-V4-Pro-NVFP4)
|
| 373 |
+
self.nvfp4_moe_meta: Optional[dict] = None
|
| 374 |
+
hybrid_quant_cfg = getattr(self.hf_config, "quantization_config", None)
|
| 375 |
+
if hybrid_quant_cfg is not None and not isinstance(hybrid_quant_cfg, dict):
|
| 376 |
+
hybrid_quant_cfg = hybrid_quant_cfg.to_dict()
|
| 377 |
+
if (
|
| 378 |
+
hybrid_quant_cfg is not None
|
| 379 |
+
and str(hybrid_quant_cfg.get("quant_algo", "")).upper() == "MIXED_PRECISION"
|
| 380 |
+
and str(hybrid_quant_cfg.get("moe_quant_algo", "")).upper() == "NVFP4"
|
| 381 |
+
and hybrid_quant_cfg.get("group_size") is not None
|
| 382 |
+
):
|
| 383 |
+
self.nvfp4_moe_meta = {
|
| 384 |
+
"group_size": int(hybrid_quant_cfg["group_size"]),
|
| 385 |
+
"exclude_modules": list(hybrid_quant_cfg.get("ignore") or []),
|
| 386 |
+
}
|
| 387 |
+
logger.info(
|
| 388 |
+
"Auto-detected hybrid FP8+NVFP4 checkpoint "
|
| 389 |
+
"(NVFP4 MoE group_size=%d, %d exclude_modules)",
|
| 390 |
+
self.nvfp4_moe_meta["group_size"],
|
| 391 |
+
len(self.nvfp4_moe_meta["exclude_modules"]),
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
# Check model type
|
| 395 |
+
self.attention_chunk_size = getattr(
|
| 396 |
+
self.hf_text_config, "attention_chunk_size", None
|
| 397 |
+
)
|
| 398 |
+
self.sliding_window_size = self._get_sliding_window_size()
|
| 399 |
+
self.is_generation = is_generation_model(
|
| 400 |
+
self.hf_config.architectures, is_embedding
|
| 401 |
+
)
|
| 402 |
+
# The vision_config/audio_config attribute heuristic is only applied when
|
| 403 |
+
# the transformers backend is explicitly requested. Some text-only models
|
| 404 |
+
# (e.g. xai-org/grok-2 with model_type="git") would otherwise be
|
| 405 |
+
# false-positively detected because their HF config auto-populates a
|
| 406 |
+
# `vision_config` in __post_init__.
|
| 407 |
+
has_multimodal_subconfig = self.hf_config is not self.hf_text_config or (
|
| 408 |
+
self.model_impl == ModelImpl.TRANSFORMERS
|
| 409 |
+
and (
|
| 410 |
+
hasattr(self.hf_config, "vision_config")
|
| 411 |
+
or hasattr(self.hf_config, "audio_config")
|
| 412 |
+
)
|
| 413 |
+
)
|
| 414 |
+
self.is_multimodal = enable_multimodal and (
|
| 415 |
+
is_multimodal_model(self.hf_config.architectures)
|
| 416 |
+
or has_multimodal_subconfig
|
| 417 |
+
)
|
| 418 |
+
self.is_audio_model = enable_multimodal and is_audio_model(
|
| 419 |
+
self.hf_config.architectures
|
| 420 |
+
)
|
| 421 |
+
# TODO: requires further polishing
|
| 422 |
+
self.is_image_understandable_model = enable_multimodal and hasattr(
|
| 423 |
+
self.hf_config, "vision_config"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# Models expose audio_config at different nesting levels:
|
| 427 |
+
# - top-level audio_config: e.g. Qwen2Audio
|
| 428 |
+
# - thinker_config.audio_config: Qwen3-Omni, Qwen3-ASR (nested thinker arch)
|
| 429 |
+
# - sound_config: Nemotron AVLM with Parakeet audio encoder
|
| 430 |
+
# - is_audio_model(): Whisper, Qwen3-ASR (architecture-based fallback)
|
| 431 |
+
# TODO: Handle this more robustly by standardizing the config structure in the future
|
| 432 |
+
self.is_audio_understandable_model = enable_multimodal and (
|
| 433 |
+
hasattr(self.hf_config, "audio_config")
|
| 434 |
+
or hasattr(getattr(self.hf_config, "thinker_config", None), "audio_config")
|
| 435 |
+
or getattr(self.hf_config, "sound_config", None) is not None
|
| 436 |
+
or is_audio_model(self.hf_config.architectures)
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
self.is_multimodal_chunked_prefill_supported = (
|
| 440 |
+
enable_multimodal
|
| 441 |
+
and is_multimodal_chunked_prefill_supported(self.hf_config.architectures)
|
| 442 |
+
)
|
| 443 |
+
self.is_encoder_decoder = is_encoder_decoder_model(self.hf_config.architectures)
|
| 444 |
+
self.is_local_attention_model = is_local_attention_model(
|
| 445 |
+
self.hf_config.architectures
|
| 446 |
+
)
|
| 447 |
+
self.use_ngram_embedding = getattr(self.hf_config, "use_ngram_embedding", False)
|
| 448 |
+
# A multimodal arch is piecewise-incompatible until its LM prefill is validated.
|
| 449 |
+
self.is_piecewise_cuda_graph_disabled_model = (
|
| 450 |
+
is_piecewise_cuda_graph_disabled_model(self.hf_config.architectures)
|
| 451 |
+
or (
|
| 452 |
+
self.is_multimodal
|
| 453 |
+
and not is_multimodal_piecewise_cuda_graph_supported(
|
| 454 |
+
self.hf_config.architectures
|
| 455 |
+
)
|
| 456 |
+
)
|
| 457 |
+
)
|
| 458 |
+
# Multimodal archs whose language-model prefill is verified safe to capture
|
| 459 |
+
# under piecewise CUDA graph. ServerArgs otherwise disables prefill piecewise
|
| 460 |
+
# CG for every multimodal model; this opt-in re-enables it for listed archs
|
| 461 |
+
# (the vision encoder still runs eagerly via general_mm_embed_routine, only the
|
| 462 |
+
# LM forward is captured).
|
| 463 |
+
self.is_multimodal_piecewise_cuda_graph_supported = enable_multimodal and (
|
| 464 |
+
is_multimodal_piecewise_cuda_graph_supported(self.hf_config.architectures)
|
| 465 |
+
)
|
| 466 |
+
self.is_multimodal_breakable_cuda_graph_supported = enable_multimodal and (
|
| 467 |
+
is_multimodal_breakable_cuda_graph_supported(self.hf_config.architectures)
|
| 468 |
+
)
|
| 469 |
+
self.dtype = _get_and_verify_dtype(self.hf_text_config, dtype)
|
| 470 |
+
|
| 471 |
+
# Derive context length and model shapes
|
| 472 |
+
self._derive_context_length(context_length)
|
| 473 |
+
self._derive_model_shapes()
|
| 474 |
+
|
| 475 |
+
# Update hybrid model
|
| 476 |
+
self._derive_hybrid_model()
|
| 477 |
+
|
| 478 |
+
# Verify quantization
|
| 479 |
+
self._verify_quantization()
|
| 480 |
+
|
| 481 |
+
# Verify dual-chunk attention config
|
| 482 |
+
self._verify_dual_chunk_attention_config()
|
| 483 |
+
|
| 484 |
+
# Cache attributes
|
| 485 |
+
self.hf_eos_token_id = self._get_hf_eos_token_id()
|
| 486 |
+
# Set by scheduler when reasoning_parser is enabled
|
| 487 |
+
self.think_end_id: Optional[int] = None
|
| 488 |
+
|
| 489 |
+
# multimodal
|
| 490 |
+
self.image_token_id = getattr(
|
| 491 |
+
self.hf_config, "image_token_id", None
|
| 492 |
+
) or getattr(self.hf_config, "image_token_index", None)
|
| 493 |
+
|
| 494 |
+
self.hf_config.encoder_only = encoder_only
|
| 495 |
+
self.hf_config.language_only = language_only
|
| 496 |
+
|
| 497 |
+
# matryoshka embeddings
|
| 498 |
+
self.matryoshka_dimensions = getattr(
|
| 499 |
+
self.hf_config, "matryoshka_dimensions", None
|
| 500 |
+
)
|
| 501 |
+
self.is_matryoshka = self.matryoshka_dimensions or getattr(
|
| 502 |
+
self.hf_config, "is_matryoshka", False
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
@staticmethod
|
| 506 |
+
def from_server_args(
|
| 507 |
+
server_args: ServerArgs,
|
| 508 |
+
model_path: str = None,
|
| 509 |
+
model_revision: str = None,
|
| 510 |
+
is_draft_model: bool = False,
|
| 511 |
+
context_length: Optional[int] = None,
|
| 512 |
+
**kwargs,
|
| 513 |
+
):
|
| 514 |
+
quantization = (
|
| 515 |
+
server_args.speculative_draft_model_quantization
|
| 516 |
+
if is_draft_model
|
| 517 |
+
else server_args.quantization
|
| 518 |
+
)
|
| 519 |
+
override_config_file = (
|
| 520 |
+
server_args.decrypted_draft_config_file
|
| 521 |
+
if is_draft_model
|
| 522 |
+
else server_args.decrypted_config_file
|
| 523 |
+
)
|
| 524 |
+
return ModelConfig(
|
| 525 |
+
model_path=model_path or server_args.model_path,
|
| 526 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 527 |
+
revision=model_revision or server_args.revision,
|
| 528 |
+
context_length=(
|
| 529 |
+
context_length
|
| 530 |
+
if context_length is not None
|
| 531 |
+
else server_args.context_length
|
| 532 |
+
),
|
| 533 |
+
model_override_args=server_args.json_model_override_args,
|
| 534 |
+
is_embedding=server_args.is_embedding,
|
| 535 |
+
enable_multimodal=server_args.enable_multimodal,
|
| 536 |
+
dtype=server_args.dtype,
|
| 537 |
+
quantization=quantization,
|
| 538 |
+
model_impl=server_args.model_impl,
|
| 539 |
+
sampling_defaults=server_args.sampling_defaults,
|
| 540 |
+
quantize_and_serve=server_args.quantize_and_serve,
|
| 541 |
+
override_config_file=override_config_file,
|
| 542 |
+
is_multi_layer_eagle=server_args.enable_multi_layer_eagle,
|
| 543 |
+
language_only=server_args.language_only,
|
| 544 |
+
encoder_only=server_args.encoder_only,
|
| 545 |
+
is_draft_model=is_draft_model,
|
| 546 |
+
disable_hybrid_swa_memory=server_args.disable_hybrid_swa_memory,
|
| 547 |
+
model_config_parser=server_args.model_config_parser,
|
| 548 |
+
speculative_algorithm=server_args.speculative_algorithm,
|
| 549 |
+
**kwargs,
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
def _config_draft_model(self):
|
| 553 |
+
is_draft_model = self.is_draft_model
|
| 554 |
+
|
| 555 |
+
if is_draft_model and self.hf_config.architectures[0] in [
|
| 556 |
+
"DeepseekV3ForCausalLM",
|
| 557 |
+
"DeepseekV32ForCausalLM",
|
| 558 |
+
]:
|
| 559 |
+
self.hf_config.architectures[0] = "DeepseekV3ForCausalLMNextN"
|
| 560 |
+
|
| 561 |
+
if is_draft_model and self.hf_config.architectures[0] == "GlmMoeDsaForCausalLM":
|
| 562 |
+
self.hf_config.architectures[0] = "GlmMoeDsaForCausalLMNextN"
|
| 563 |
+
|
| 564 |
+
if (
|
| 565 |
+
is_draft_model
|
| 566 |
+
and self.hf_config.architectures[0] == "AgnesForCausalLM"
|
| 567 |
+
):
|
| 568 |
+
from sglang.srt.speculative.dspark_components.dspark_config import (
|
| 569 |
+
checkpoint_bundles_dspark_draft,
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
# A dspark-bundled checkpoint may also carry MTP layers; the
|
| 573 |
+
# selected algorithm decides which draft arch to load.
|
| 574 |
+
if checkpoint_bundles_dspark_draft(self.hf_config) and (
|
| 575 |
+
self.speculative_algorithm in (None, "DSPARK")
|
| 576 |
+
):
|
| 577 |
+
self.hf_config.architectures[0] = "AgnesForCausalLMDSpark"
|
| 578 |
+
logger.info(
|
| 579 |
+
"Draft checkpoint bundles a DSpark head; loading draft arch "
|
| 580 |
+
"AgnesForCausalLMDSpark."
|
| 581 |
+
)
|
| 582 |
+
else:
|
| 583 |
+
self.hf_config.architectures[0] = "AgnesForCausalLMNextN"
|
| 584 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 585 |
+
|
| 586 |
+
if is_draft_model and self.hf_config.architectures[0] == "Glm4MoeForCausalLM":
|
| 587 |
+
self.hf_config.architectures[0] = "Glm4MoeForCausalLMNextN"
|
| 588 |
+
if (
|
| 589 |
+
is_draft_model
|
| 590 |
+
and self.hf_config.architectures[0] == "Glm4MoeLiteForCausalLM"
|
| 591 |
+
):
|
| 592 |
+
self.hf_config.architectures[0] = "Glm4MoeLiteForCausalLMNextN"
|
| 593 |
+
|
| 594 |
+
if is_draft_model and self.hf_config.architectures[0] in [
|
| 595 |
+
"GlmOcrForConditionalGeneration",
|
| 596 |
+
]:
|
| 597 |
+
self.hf_config.architectures[0] = "GlmOcrForConditionalGenerationNextN"
|
| 598 |
+
|
| 599 |
+
if (
|
| 600 |
+
is_draft_model
|
| 601 |
+
and self.hf_config.architectures[0] == "LongcatFlashForCausalLM"
|
| 602 |
+
):
|
| 603 |
+
self.hf_config.architectures[0] = "LongcatFlashForCausalLMNextN"
|
| 604 |
+
self.hf_config.num_hidden_layers = self.hf_config.num_nextn_predict_layers
|
| 605 |
+
|
| 606 |
+
if is_draft_model and self.hf_config.architectures[0] == "MiMoForCausalLM":
|
| 607 |
+
self.hf_config.architectures[0] = "MiMoMTP"
|
| 608 |
+
if is_draft_model and self.hf_config.architectures[0] in MIMO_V2_MODEL_ARCHS:
|
| 609 |
+
self.hf_config.architectures[0] = "MiMoV2MTP"
|
| 610 |
+
if is_draft_model and self.hf_config.architectures[0] == "Step3p5ForCausalLM":
|
| 611 |
+
self.hf_config.architectures[0] = "Step3p5MTP"
|
| 612 |
+
if (
|
| 613 |
+
is_draft_model
|
| 614 |
+
and self.hf_config.architectures[0] == "InklingForConditionalGeneration"
|
| 615 |
+
):
|
| 616 |
+
self.hf_config.architectures[0] = "InklingForConditionalGenerationMTP"
|
| 617 |
+
if (
|
| 618 |
+
is_draft_model
|
| 619 |
+
and self.hf_config.architectures[0] == "Step3p7ForConditionalGeneration"
|
| 620 |
+
):
|
| 621 |
+
self.hf_config = self.hf_text_config
|
| 622 |
+
self.hf_config.architectures = ["Step3p5MTP"]
|
| 623 |
+
if is_draft_model and self.hf_config.architectures[0] in [
|
| 624 |
+
"BailingMoeV2ForCausalLM",
|
| 625 |
+
"BailingMoeForCausalLM",
|
| 626 |
+
"BailingMoeV2_5ForCausalLM",
|
| 627 |
+
]:
|
| 628 |
+
self.hf_config.architectures[0] = "BailingMoeForCausalLMNextN"
|
| 629 |
+
if (
|
| 630 |
+
is_draft_model
|
| 631 |
+
and self.hf_config.architectures[0] == "Ernie4_5_MoeForCausalLM"
|
| 632 |
+
):
|
| 633 |
+
self.hf_config.architectures[0] = "Ernie4_5_MoeForCausalLMMTP"
|
| 634 |
+
|
| 635 |
+
if is_draft_model and self.hf_config.architectures[0] == "Qwen3NextForCausalLM":
|
| 636 |
+
self.hf_config.architectures[0] = "Qwen3NextForCausalLMMTP"
|
| 637 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 638 |
+
|
| 639 |
+
if is_draft_model and self.hf_config.architectures[0] == "Qwen3MoeForCausalLM":
|
| 640 |
+
self.hf_config.architectures[0] = "Qwen3MoeForCausalLMMTP"
|
| 641 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 642 |
+
|
| 643 |
+
if is_draft_model and self.hf_config.architectures[0] in [
|
| 644 |
+
"Qwen3_5ForConditionalGeneration",
|
| 645 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 646 |
+
"InternS2PreviewForConditionalGeneration",
|
| 647 |
+
]:
|
| 648 |
+
self.hf_config.architectures[0] = "Qwen3_5ForCausalLMMTP"
|
| 649 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 650 |
+
|
| 651 |
+
if is_draft_model and self.hf_config.architectures[0] == "ExaoneMoEForCausalLM":
|
| 652 |
+
self.hf_config.architectures[0] = "ExaoneMoEForCausalLMMTP"
|
| 653 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 654 |
+
|
| 655 |
+
if is_draft_model and self.hf_config.architectures[0] in [
|
| 656 |
+
"NemotronHForCausalLM",
|
| 657 |
+
"NemotronHPuzzleForCausalLM",
|
| 658 |
+
]:
|
| 659 |
+
self.hf_config.architectures[0] = "NemotronHForCausalLMMTP"
|
| 660 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 661 |
+
|
| 662 |
+
if is_draft_model and self.hf_config.architectures[0] == "HYV3ForCausalLM":
|
| 663 |
+
self.hf_config.architectures[0] = "HYV3ForCausalLMNextN"
|
| 664 |
+
self.hf_config.num_nextn_predict_layers = 1
|
| 665 |
+
|
| 666 |
+
def _derive_hybrid_model(self):
|
| 667 |
+
# Use self.context_len after it has been initialized to prevent using context_len which may be None.
|
| 668 |
+
self.is_hybrid_swa = (
|
| 669 |
+
is_hybrid_swa_model(self.hf_config.architectures, self.hf_text_config)
|
| 670 |
+
and not self.disable_hybrid_swa_memory
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
if self.is_hybrid_swa:
|
| 674 |
+
logger.info(f"Hybrid swa model: {self.hf_config.architectures=}")
|
| 675 |
+
|
| 676 |
+
self.is_deepseek_v4_arch = any(
|
| 677 |
+
arch
|
| 678 |
+
in [
|
| 679 |
+
"AgnesForCausalLM",
|
| 680 |
+
"AgnesForCausalLMNextN",
|
| 681 |
+
"AgnesForCausalLMDSpark",
|
| 682 |
+
]
|
| 683 |
+
for arch in self.hf_config.architectures
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
if not self.is_deepseek_v4_arch:
|
| 687 |
+
self.swa_attention_layer_ids, self.full_attention_layer_ids = (
|
| 688 |
+
get_hybrid_layer_ids(
|
| 689 |
+
self.hf_config.architectures,
|
| 690 |
+
self.hf_text_config,
|
| 691 |
+
)
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
self.has_attention_sinks = self._detect_attention_sinks()
|
| 695 |
+
|
| 696 |
+
self.is_hybrid_swa_compress = self.hf_config.architectures[0] in [
|
| 697 |
+
*MIMO_V2_MODEL_ARCHS,
|
| 698 |
+
"MiMoV2MTP",
|
| 699 |
+
"Gemma4ForCausalLM",
|
| 700 |
+
"Gemma4ForConditionalGeneration",
|
| 701 |
+
"InklingForConditionalGeneration",
|
| 702 |
+
"InklingForConditionalGenerationMTP",
|
| 703 |
+
"Gemma4UnifiedForConditionalGeneration",
|
| 704 |
+
]
|
| 705 |
+
|
| 706 |
+
@cached_property
|
| 707 |
+
def linear_attn_registry_result(self) -> Any:
|
| 708 |
+
return get_linear_attn_config(self.hf_config)
|
| 709 |
+
|
| 710 |
+
def _detect_attention_sinks(self) -> bool:
|
| 711 |
+
"""Check whether the model uses learned attention sinks.
|
| 712 |
+
|
| 713 |
+
Attention sinks are per-head scalars added to the softmax denominator
|
| 714 |
+
to compensate for evicted KV-cache entries under sliding-window
|
| 715 |
+
attention. Not every hybrid-SWA model uses them.
|
| 716 |
+
"""
|
| 717 |
+
archs = self.hf_config.architectures or []
|
| 718 |
+
# GptOss always creates sinks unconditionally.
|
| 719 |
+
if "GptOssForCausalLM" in archs:
|
| 720 |
+
return True
|
| 721 |
+
|
| 722 |
+
# MiMoV2 creates sinks only when the config flags are set.
|
| 723 |
+
if any(a in archs for a in (*MIMO_V2_MODEL_ARCHS, "MiMoV2MTP")):
|
| 724 |
+
return getattr(
|
| 725 |
+
self.hf_text_config, "add_swa_attention_sink_bias", False
|
| 726 |
+
) or getattr(self.hf_text_config, "add_full_attention_sink_bias", False)
|
| 727 |
+
return False
|
| 728 |
+
|
| 729 |
+
def _derive_context_length(self, context_length: int):
|
| 730 |
+
is_draft_model = self.is_draft_model
|
| 731 |
+
derived_context_len = get_context_length(self.hf_text_config)
|
| 732 |
+
|
| 733 |
+
if context_length is not None:
|
| 734 |
+
if context_length > derived_context_len:
|
| 735 |
+
reason = "Target model's" if is_draft_model else "User-specified"
|
| 736 |
+
msg = (
|
| 737 |
+
f"Warning: {reason} context_length ({context_length}) is greater than the derived context_length ({derived_context_len}). "
|
| 738 |
+
f"This may lead to incorrect model outputs or CUDA errors. Note that the derived context_length may differ from max_position_embeddings in the model's config."
|
| 739 |
+
)
|
| 740 |
+
if (
|
| 741 |
+
envs.SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN.get()
|
| 742 |
+
or is_in_ci() # FIXME: fix this special case
|
| 743 |
+
):
|
| 744 |
+
logger.warning(msg)
|
| 745 |
+
self.context_len = context_length
|
| 746 |
+
if is_draft_model:
|
| 747 |
+
self.hf_text_config.max_position_embeddings = context_length
|
| 748 |
+
logger.warning(
|
| 749 |
+
f"Overriding the draft model's max_position_embeddings to {context_length}."
|
| 750 |
+
)
|
| 751 |
+
else:
|
| 752 |
+
raise ValueError(
|
| 753 |
+
f"{msg} To allow overriding this maximum, set the env var SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1"
|
| 754 |
+
)
|
| 755 |
+
else:
|
| 756 |
+
self.context_len = context_length
|
| 757 |
+
else:
|
| 758 |
+
self.context_len = derived_context_len
|
| 759 |
+
|
| 760 |
+
# Transfer context_len to HuggingFace config so models can access it
|
| 761 |
+
self.hf_config.context_len = self.context_len
|
| 762 |
+
|
| 763 |
+
def _derive_model_shapes(self):
|
| 764 |
+
# Unify the config keys for hf_text_config
|
| 765 |
+
self.head_dim = getattr(self.hf_text_config, "head_dim", None)
|
| 766 |
+
if self.head_dim is None:
|
| 767 |
+
self.head_dim = (
|
| 768 |
+
self.hf_text_config.hidden_size
|
| 769 |
+
// self.hf_text_config.num_attention_heads
|
| 770 |
+
)
|
| 771 |
+
setattr(self.hf_text_config, "head_dim", self.head_dim)
|
| 772 |
+
|
| 773 |
+
self.v_head_dim = getattr(self.hf_text_config, "v_head_dim", None)
|
| 774 |
+
if self.v_head_dim is None:
|
| 775 |
+
self.v_head_dim = self.head_dim
|
| 776 |
+
setattr(self.hf_text_config, "v_head_dim", self.v_head_dim)
|
| 777 |
+
|
| 778 |
+
self.swa_head_dim = getattr(self.hf_text_config, "swa_head_dim", None)
|
| 779 |
+
if self.swa_head_dim is None:
|
| 780 |
+
self.swa_head_dim = self.head_dim
|
| 781 |
+
setattr(self.hf_text_config, "swa_head_dim", self.swa_head_dim)
|
| 782 |
+
|
| 783 |
+
self.swa_v_head_dim = getattr(self.hf_text_config, "swa_v_head_dim", None)
|
| 784 |
+
if self.swa_v_head_dim is None:
|
| 785 |
+
self.swa_v_head_dim = self.swa_head_dim
|
| 786 |
+
setattr(self.hf_text_config, "swa_v_head_dim", self.swa_v_head_dim)
|
| 787 |
+
# FIXME: temporary special judge for MLA architecture
|
| 788 |
+
if (
|
| 789 |
+
"DeepseekV2ForCausalLM" in self.hf_config.architectures
|
| 790 |
+
or "DeepseekV32ForCausalLM" in self.hf_config.architectures
|
| 791 |
+
or "DeepseekV3ForCausalLM" in self.hf_config.architectures
|
| 792 |
+
or "DeepseekV3ForCausalLMNextN" in self.hf_config.architectures
|
| 793 |
+
or "Glm4MoeLiteForCausalLM" in self.hf_config.architectures
|
| 794 |
+
or "Glm4MoeLiteForCausalLMNextN" in self.hf_config.architectures
|
| 795 |
+
or "GlmMoeDsaForCausalLM" in self.hf_config.architectures
|
| 796 |
+
or "GlmMoeDsaForCausalLMNextN" in self.hf_config.architectures
|
| 797 |
+
or "LongcatFlashForCausalLM" in self.hf_config.architectures
|
| 798 |
+
or "LongcatFlashForCausalLMNextN" in self.hf_config.architectures
|
| 799 |
+
or "DotsVLMForCausalLM" in self.hf_config.architectures
|
| 800 |
+
or "MistralLarge3ForCausalLM" in self.hf_config.architectures
|
| 801 |
+
or (
|
| 802 |
+
"PixtralForConditionalGeneration" in self.hf_config.architectures
|
| 803 |
+
and getattr(self.hf_text_config, "kv_lora_rank", None) is not None
|
| 804 |
+
)
|
| 805 |
+
or "MistralLarge3ForCausalLMEagle" in self.hf_config.architectures
|
| 806 |
+
or "KimiK25ForConditionalGeneration" in self.hf_config.architectures
|
| 807 |
+
or "Eagle3DeepseekV2ForCausalLM" in self.hf_config.architectures
|
| 808 |
+
):
|
| 809 |
+
self.head_dim = 256
|
| 810 |
+
self.attention_arch = AttentionArch.MLA
|
| 811 |
+
self.kv_lora_rank = self.hf_text_config.kv_lora_rank
|
| 812 |
+
self.qk_nope_head_dim = self.hf_text_config.qk_nope_head_dim
|
| 813 |
+
self.qk_rope_head_dim = self.hf_text_config.qk_rope_head_dim
|
| 814 |
+
self.v_head_dim = self.hf_text_config.v_head_dim
|
| 815 |
+
self.index_head_dim = (
|
| 816 |
+
get_dsa_index_head_dim(self.hf_text_config)
|
| 817 |
+
if is_deepseek_dsa(self.hf_text_config)
|
| 818 |
+
else None
|
| 819 |
+
)
|
| 820 |
+
# Handle rope scaling
|
| 821 |
+
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
| 822 |
+
# in transformers v5, rope_scaling is just rope_parameters for backward compatibility
|
| 823 |
+
rope_scaling = self.hf_text_config.rope_scaling
|
| 824 |
+
if rope_scaling:
|
| 825 |
+
# v5 uses "rope_type", v4 uses "type"
|
| 826 |
+
rope_type = (
|
| 827 |
+
rope_scaling.get("rope_type")
|
| 828 |
+
or rope_scaling.get("type")
|
| 829 |
+
or "default"
|
| 830 |
+
)
|
| 831 |
+
if rope_type != "default":
|
| 832 |
+
self.scaling = compute_mla_mscale_scaling(
|
| 833 |
+
rope_scaling, self.scaling
|
| 834 |
+
)
|
| 835 |
+
elif (
|
| 836 |
+
"AgnesForCausalLM" in self.hf_config.architectures
|
| 837 |
+
or "AgnesForCausalLMNextN" in self.hf_config.architectures
|
| 838 |
+
or "AgnesForCausalLMDSpark" in self.hf_config.architectures
|
| 839 |
+
):
|
| 840 |
+
self.qk_rope_head_dim = self.hf_config.qk_rope_head_dim
|
| 841 |
+
self.qk_nope_head_dim = self.hf_config.head_dim - self.qk_rope_head_dim
|
| 842 |
+
self.window_size = self.hf_config.sliding_window
|
| 843 |
+
self.head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
| 844 |
+
self.v_head_dim = self.head_dim
|
| 845 |
+
self.index_head_dim = self.hf_config.index_head_dim
|
| 846 |
+
self.compress_ratios = self.hf_config.compress_ratios
|
| 847 |
+
self.attention_arch = AttentionArch.MHA
|
| 848 |
+
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
| 849 |
+
if self.hf_config.rope_scaling:
|
| 850 |
+
self.scaling = compute_mla_mscale_scaling(
|
| 851 |
+
self.hf_config.rope_scaling, self.scaling
|
| 852 |
+
)
|
| 853 |
+
elif "Glm4MoeForCausalLMNextN" in self.hf_config.architectures:
|
| 854 |
+
if self.head_dim is None:
|
| 855 |
+
self.head_dim = (
|
| 856 |
+
self.hf_text_config.hidden_size
|
| 857 |
+
// self.hf_text_config.num_attention_heads
|
| 858 |
+
)
|
| 859 |
+
if self.swa_head_dim is None:
|
| 860 |
+
self.swa_head_dim = self.head_dim
|
| 861 |
+
self.v_head_dim = self.head_dim
|
| 862 |
+
self.swa_v_head_dim = self.swa_head_dim
|
| 863 |
+
self.attention_arch = AttentionArch.MHA
|
| 864 |
+
elif "MiniCPM3ForCausalLM" in self.hf_config.architectures:
|
| 865 |
+
self.head_dim = 128
|
| 866 |
+
self.attention_arch = AttentionArch.MLA
|
| 867 |
+
self.kv_lora_rank = self.hf_config.kv_lora_rank
|
| 868 |
+
self.qk_rope_head_dim = self.hf_config.qk_rope_head_dim
|
| 869 |
+
elif "DeepseekVL2ForCausalLM" in self.hf_config.architectures and getattr(
|
| 870 |
+
self.hf_text_config, "use_mla", True
|
| 871 |
+
):
|
| 872 |
+
self.head_dim = 256
|
| 873 |
+
self.attention_arch = AttentionArch.MLA
|
| 874 |
+
self.kv_lora_rank = self.hf_text_config.kv_lora_rank
|
| 875 |
+
self.qk_rope_head_dim = self.hf_text_config.qk_rope_head_dim
|
| 876 |
+
self.qk_nope_head_dim = self.hf_text_config.qk_nope_head_dim
|
| 877 |
+
elif "KimiVLForConditionalGeneration" in self.hf_config.architectures:
|
| 878 |
+
self.head_dim = 256
|
| 879 |
+
self.attention_arch = AttentionArch.MLA
|
| 880 |
+
self.kv_lora_rank = self.hf_text_config.kv_lora_rank
|
| 881 |
+
self.qk_rope_head_dim = self.hf_text_config.qk_rope_head_dim
|
| 882 |
+
self.v_head_dim = self.hf_text_config.v_head_dim
|
| 883 |
+
self.qk_nope_head_dim = self.hf_text_config.qk_nope_head_dim
|
| 884 |
+
elif "KimiLinearForCausalLM" in self.hf_config.architectures:
|
| 885 |
+
self.head_dim = 72
|
| 886 |
+
self.attention_arch = AttentionArch.MLA
|
| 887 |
+
self.kv_lora_rank = self.hf_config.kv_lora_rank
|
| 888 |
+
self.qk_rope_head_dim = self.hf_config.qk_rope_head_dim
|
| 889 |
+
self.v_head_dim = self.hf_config.v_head_dim
|
| 890 |
+
self.qk_nope_head_dim = self.hf_config.qk_nope_head_dim
|
| 891 |
+
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
| 892 |
+
if self.hf_config.rope_scaling:
|
| 893 |
+
self.scaling = compute_mla_mscale_scaling(
|
| 894 |
+
self.hf_config.rope_scaling, self.scaling
|
| 895 |
+
)
|
| 896 |
+
elif (
|
| 897 |
+
"BailingMoeV2_5ForCausalLM" in self.hf_config.architectures
|
| 898 |
+
or "BailingMoeForCausalLMNextN" in self.hf_config.architectures
|
| 899 |
+
):
|
| 900 |
+
self.head_dim = self.hf_text_config.head_dim
|
| 901 |
+
self.attention_arch = AttentionArch.MLA
|
| 902 |
+
self.kv_lora_rank = self.hf_text_config.kv_lora_rank
|
| 903 |
+
self.qk_nope_head_dim = self.hf_text_config.qk_nope_head_dim
|
| 904 |
+
self.qk_rope_head_dim = self.hf_text_config.qk_rope_head_dim
|
| 905 |
+
self.v_head_dim = self.hf_config.v_head_dim
|
| 906 |
+
# Handle rope scaling with yarn
|
| 907 |
+
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
| 908 |
+
if self.hf_config.rope_scaling:
|
| 909 |
+
self.scaling = compute_mla_mscale_scaling(
|
| 910 |
+
self.hf_config.rope_scaling, self.scaling
|
| 911 |
+
)
|
| 912 |
+
elif "SarvamMLAForCausalLM" in self.hf_config.architectures:
|
| 913 |
+
self.head_dim = (
|
| 914 |
+
self.hf_config.qk_nope_head_dim + self.hf_config.qk_rope_head_dim
|
| 915 |
+
)
|
| 916 |
+
self.attention_arch = AttentionArch.MLA
|
| 917 |
+
self.kv_lora_rank = self.hf_config.kv_lora_rank
|
| 918 |
+
self.qk_rope_head_dim = self.hf_config.qk_rope_head_dim
|
| 919 |
+
self.qk_nope_head_dim = self.hf_config.qk_nope_head_dim
|
| 920 |
+
self.v_head_dim = self.hf_config.v_head_dim
|
| 921 |
+
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
| 922 |
+
if self.hf_config.rope_scaling:
|
| 923 |
+
self.scaling = compute_mla_mscale_scaling(
|
| 924 |
+
self.hf_config.rope_scaling, self.scaling
|
| 925 |
+
)
|
| 926 |
+
else:
|
| 927 |
+
if (
|
| 928 |
+
"MistralModel" in self.hf_config.architectures
|
| 929 |
+
or "MixtralForCausalLM" in self.hf_config.architectures
|
| 930 |
+
or "MistralForCausalLM" in self.hf_config.architectures
|
| 931 |
+
):
|
| 932 |
+
if getattr(self, "head_dim", None) is None:
|
| 933 |
+
self.head_dim = (
|
| 934 |
+
self.hf_config.hidden_size // self.hf_config.num_attention_heads
|
| 935 |
+
)
|
| 936 |
+
# In transformers==4.52.3, the head_dim is null in MistralConfig
|
| 937 |
+
if (
|
| 938 |
+
not hasattr(self.hf_text_config, "head_dim")
|
| 939 |
+
or self.hf_text_config.head_dim is None
|
| 940 |
+
):
|
| 941 |
+
setattr(self.hf_text_config, "head_dim", self.head_dim)
|
| 942 |
+
|
| 943 |
+
elif "BaichuanForCausalLM" in self.hf_config.architectures:
|
| 944 |
+
self.use_alibi = self.hf_config.hidden_size != 4096
|
| 945 |
+
|
| 946 |
+
self.attention_arch = AttentionArch.MHA
|
| 947 |
+
|
| 948 |
+
self.num_attention_heads = self.hf_text_config.num_attention_heads
|
| 949 |
+
self.num_key_value_heads = getattr(
|
| 950 |
+
self.hf_text_config, "num_key_value_heads", None
|
| 951 |
+
)
|
| 952 |
+
self.first_k_dense_replace = getattr(
|
| 953 |
+
self.hf_text_config, "first_k_dense_replace", None
|
| 954 |
+
)
|
| 955 |
+
self.full_attention_interval = getattr(
|
| 956 |
+
self.hf_text_config, "full_attention_interval", None
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
# for Dbrx and MPT models
|
| 960 |
+
if self.hf_config.model_type in ["dbrx", "mpt"]:
|
| 961 |
+
self.num_key_value_heads = getattr(
|
| 962 |
+
self.hf_config.attn_config, "kv_n_heads", None
|
| 963 |
+
)
|
| 964 |
+
|
| 965 |
+
if self.num_key_value_heads is None:
|
| 966 |
+
self.num_key_value_heads = self.num_attention_heads
|
| 967 |
+
self.hidden_size = self.hf_text_config.hidden_size
|
| 968 |
+
hc_mult = getattr(self.hf_text_config, "hc_mult", 1)
|
| 969 |
+
self.spec_hidden_size = (
|
| 970 |
+
self.hidden_size * hc_mult if hc_mult > 1 else self.hidden_size
|
| 971 |
+
)
|
| 972 |
+
# mHC-flattened hidden size; None when not running an mHC model
|
| 973 |
+
# (e.g. non-DeepSeek-V4 configs without ``hc_mult``).
|
| 974 |
+
self.hc_hidden_size = self.spec_hidden_size if hc_mult > 1 else None
|
| 975 |
+
self.num_hidden_layers = self.hf_text_config.num_hidden_layers
|
| 976 |
+
self.num_attention_layers = self.num_hidden_layers
|
| 977 |
+
if "LongcatFlashForCausalLM" in self.hf_config.architectures:
|
| 978 |
+
self.num_attention_layers = self.num_hidden_layers * 2
|
| 979 |
+
if "IQuestLoopCoderForCausalLM" in self.hf_config.architectures:
|
| 980 |
+
loop_num = getattr(self.hf_text_config, "loop_num", 1)
|
| 981 |
+
self.num_attention_layers = int(self.num_hidden_layers * int(loop_num))
|
| 982 |
+
if "WhisperForConditionalGeneration" in self.hf_config.architectures:
|
| 983 |
+
# Whisper has unique layer ID scheme:
|
| 984 |
+
# - Encoder self-attention: 0 to encoder_layers-1 (no KV cache)
|
| 985 |
+
# - Decoder self-attention: encoder_layers to encoder_layers+decoder_layers-1 (uses KV cache)
|
| 986 |
+
# - Decoder cross-attention: encoder_layers+decoder_layers to encoder_layers+2*decoder_layers-1
|
| 987 |
+
# Even though cross-attention doesn't save KV cache, attention backend needs buffer to exist
|
| 988 |
+
encoder_layers = getattr(self.hf_text_config, "encoder_layers", 0)
|
| 989 |
+
decoder_layers = getattr(
|
| 990 |
+
self.hf_text_config, "decoder_layers", self.num_hidden_layers
|
| 991 |
+
)
|
| 992 |
+
self.num_attention_layers = encoder_layers + 2 * decoder_layers
|
| 993 |
+
self.num_nextn_predict_layers = getattr(
|
| 994 |
+
self.hf_text_config, "num_nextn_predict_layers", None
|
| 995 |
+
)
|
| 996 |
+
self.vocab_size = self.hf_text_config.vocab_size
|
| 997 |
+
# GLM-Image is the only model here whose output head predicts vision tokens.
|
| 998 |
+
# Use vision_vocab_size for lm_head, LogitsProcessor, and graph-mode logits buffers.
|
| 999 |
+
if _hf_arch(self.hf_config) == "GlmImageForConditionalGeneration":
|
| 1000 |
+
self.vocab_size = self.hf_text_config.vision_vocab_size
|
| 1001 |
+
|
| 1002 |
+
def get_total_num_attention_heads(self) -> int:
|
| 1003 |
+
return self.num_attention_heads
|
| 1004 |
+
|
| 1005 |
+
def get_num_attention_heads(self, tensor_parallel_size) -> int:
|
| 1006 |
+
total_num_attention_heads = self.num_attention_heads
|
| 1007 |
+
return max(1, total_num_attention_heads // tensor_parallel_size)
|
| 1008 |
+
|
| 1009 |
+
# adapted from https://github.com/vllm-project/vllm/blob/main/vllm/config.py#L289
|
| 1010 |
+
def get_total_num_kv_heads(self) -> int:
|
| 1011 |
+
"""Returns the total number of KV heads."""
|
| 1012 |
+
# For GPTBigCode & Falcon:
|
| 1013 |
+
# NOTE: for falcon, when new_decoder_architecture is True, the
|
| 1014 |
+
# multi_query flag is ignored and we use n_head_kv for the number of
|
| 1015 |
+
# KV heads.
|
| 1016 |
+
falcon_model_types = ["falcon", "RefinedWeb", "RefinedWebModel"]
|
| 1017 |
+
new_decoder_arch_falcon = (
|
| 1018 |
+
self.hf_config.model_type in falcon_model_types
|
| 1019 |
+
and getattr(self.hf_config, "new_decoder_architecture", False)
|
| 1020 |
+
)
|
| 1021 |
+
if not new_decoder_arch_falcon and getattr(
|
| 1022 |
+
self.hf_text_config, "multi_query", False
|
| 1023 |
+
):
|
| 1024 |
+
# Multi-query attention, only one KV head.
|
| 1025 |
+
# Currently, tensor parallelism is not supported in this case.
|
| 1026 |
+
return 1
|
| 1027 |
+
|
| 1028 |
+
# For DBRX and MPT
|
| 1029 |
+
if self.hf_config.model_type in ["mpt"]:
|
| 1030 |
+
if "kv_n_heads" in self.hf_config.attn_config:
|
| 1031 |
+
return self.hf_config.attn_config["kv_n_heads"]
|
| 1032 |
+
return self.hf_config.num_attention_heads
|
| 1033 |
+
if self.hf_config.model_type in ["dbrx"]:
|
| 1034 |
+
return getattr(
|
| 1035 |
+
self.hf_config.attn_config,
|
| 1036 |
+
"kv_n_heads",
|
| 1037 |
+
self.hf_config.num_attention_heads,
|
| 1038 |
+
)
|
| 1039 |
+
if self.hf_config.model_type in ["nemotron-nas"]:
|
| 1040 |
+
nkvh = {
|
| 1041 |
+
self.hf_config.num_attention_heads // block.attention.n_heads_in_group
|
| 1042 |
+
for block in self.hf_config.block_configs
|
| 1043 |
+
if not block.attention.no_op
|
| 1044 |
+
}
|
| 1045 |
+
if len(nkvh) == 0:
|
| 1046 |
+
raise RuntimeError("Couldn't determine number of kv heads")
|
| 1047 |
+
if len(nkvh) > 1:
|
| 1048 |
+
raise ValueError(
|
| 1049 |
+
"Variable GQA (VGQA) is not yet supported for nemotron-nas in sglang"
|
| 1050 |
+
)
|
| 1051 |
+
return next(iter(nkvh))
|
| 1052 |
+
|
| 1053 |
+
attributes = [
|
| 1054 |
+
# For Falcon:
|
| 1055 |
+
"n_head_kv",
|
| 1056 |
+
"num_kv_heads",
|
| 1057 |
+
# For LLaMA-2:
|
| 1058 |
+
"num_key_value_heads",
|
| 1059 |
+
# For ChatGLM:
|
| 1060 |
+
"multi_query_group_num",
|
| 1061 |
+
# For Step3
|
| 1062 |
+
"num_attention_groups",
|
| 1063 |
+
]
|
| 1064 |
+
for attr in attributes:
|
| 1065 |
+
num_kv_heads = getattr(self.hf_text_config, attr, None)
|
| 1066 |
+
if num_kv_heads is not None:
|
| 1067 |
+
return num_kv_heads
|
| 1068 |
+
|
| 1069 |
+
# For non-grouped-query attention models, the number of KV heads is
|
| 1070 |
+
# equal to the number of attention heads.
|
| 1071 |
+
return self.hf_text_config.num_attention_heads
|
| 1072 |
+
|
| 1073 |
+
def get_num_kv_heads(self, tensor_parallel_size) -> int:
|
| 1074 |
+
"""Returns the number of KV heads per GPU."""
|
| 1075 |
+
total_num_kv_heads = self.get_total_num_kv_heads()
|
| 1076 |
+
# If tensor parallelism is used, we divide the number of KV heads by
|
| 1077 |
+
# the tensor parallel size. We will replicate the KV heads in the
|
| 1078 |
+
# case where the number of KV heads is smaller than the tensor
|
| 1079 |
+
# parallel size so each GPU has at least one KV head.
|
| 1080 |
+
return max(1, total_num_kv_heads // tensor_parallel_size)
|
| 1081 |
+
|
| 1082 |
+
def get_swa_num_kv_heads(self, tensor_parallel_size) -> int:
|
| 1083 |
+
"""Similar to get_num_kv_heads(), but for SWA."""
|
| 1084 |
+
if hasattr(self.hf_text_config, "swa_num_key_value_heads"):
|
| 1085 |
+
total_num_kv_heads = self.hf_text_config.swa_num_key_value_heads
|
| 1086 |
+
return max(1, total_num_kv_heads // tensor_parallel_size)
|
| 1087 |
+
elif hasattr(self.hf_text_config, "attention_other_setting"): # For step3p5
|
| 1088 |
+
total_num_kv_heads = self.hf_text_config.attention_other_setting.get(
|
| 1089 |
+
"num_attention_groups"
|
| 1090 |
+
)
|
| 1091 |
+
return max(1, total_num_kv_heads // tensor_parallel_size)
|
| 1092 |
+
else:
|
| 1093 |
+
return self.get_num_kv_heads(tensor_parallel_size)
|
| 1094 |
+
|
| 1095 |
+
# adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/config.py
|
| 1096 |
+
def _parse_quant_hf_config(self):
|
| 1097 |
+
quant_cfg = getattr(self.hf_config, "quantization_config", None)
|
| 1098 |
+
if quant_cfg is not None and not isinstance(quant_cfg, dict):
|
| 1099 |
+
quant_cfg = quant_cfg.to_dict()
|
| 1100 |
+
if quant_cfg is not None:
|
| 1101 |
+
# Identify modelopt quantization
|
| 1102 |
+
if (
|
| 1103 |
+
"quant_method" not in quant_cfg
|
| 1104 |
+
or quant_cfg["quant_method"] == "modelopt"
|
| 1105 |
+
):
|
| 1106 |
+
parsed_cfg = self._parse_modelopt_quant_config(
|
| 1107 |
+
{"quantization": quant_cfg}
|
| 1108 |
+
)
|
| 1109 |
+
if parsed_cfg:
|
| 1110 |
+
quant_cfg.update(parsed_cfg)
|
| 1111 |
+
|
| 1112 |
+
if quant_cfg is None:
|
| 1113 |
+
# compressed-tensors uses a "compression_config" key
|
| 1114 |
+
quant_cfg = getattr(self.hf_config, "compression_config", None)
|
| 1115 |
+
if quant_cfg is None:
|
| 1116 |
+
# check if is modelopt or mixed-precision model -- Both of them don't have corresponding field
|
| 1117 |
+
# in hf `config.json` but has a standalone `hf_quant_config.json` in the root directory
|
| 1118 |
+
# example: https://huggingface.co/nvidia/Llama-3.1-8B-Instruct-FP8/tree/main
|
| 1119 |
+
# example: https://huggingface.co/Barrrrry/DeepSeek-R1-W4AFP8/tree/main
|
| 1120 |
+
is_local = os.path.exists(self.model_path)
|
| 1121 |
+
if not is_local:
|
| 1122 |
+
# Conditional import based on SGLANG_USE_MODELSCOPE environment variable
|
| 1123 |
+
if envs.SGLANG_USE_MODELSCOPE.get():
|
| 1124 |
+
|
| 1125 |
+
from modelscope import HubApi, model_file_download
|
| 1126 |
+
|
| 1127 |
+
hf_api = HubApi()
|
| 1128 |
+
else:
|
| 1129 |
+
import huggingface_hub
|
| 1130 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 1131 |
+
|
| 1132 |
+
hf_api = HfApi()
|
| 1133 |
+
try:
|
| 1134 |
+
# In offline mode, skip file_exists check to avoid OfflineModeIsEnabled error
|
| 1135 |
+
# Instead, directly try to download/read from cache with local_files_only
|
| 1136 |
+
file_exists = False # Initialize to avoid UnboundLocalError
|
| 1137 |
+
if not huggingface_hub.constants.HF_HUB_OFFLINE:
|
| 1138 |
+
# Online mode: check if file exists before attempting download (optimization)
|
| 1139 |
+
file_exists = retry(
|
| 1140 |
+
lambda: hf_api.file_exists(
|
| 1141 |
+
self.model_path, "hf_quant_config.json"
|
| 1142 |
+
),
|
| 1143 |
+
max_retry=2,
|
| 1144 |
+
initial_delay=1.0,
|
| 1145 |
+
max_delay=5.0,
|
| 1146 |
+
)
|
| 1147 |
+
if not file_exists:
|
| 1148 |
+
# File doesn't exist on hub, no need to try downloading
|
| 1149 |
+
return quant_cfg # None
|
| 1150 |
+
|
| 1151 |
+
# Download (online mode) or read from cache (offline mode)
|
| 1152 |
+
if envs.SGLANG_USE_MODELSCOPE.get():
|
| 1153 |
+
quant_config_file = model_file_download(
|
| 1154 |
+
model_id=self.model_path,
|
| 1155 |
+
file_path="hf_quant_config.json",
|
| 1156 |
+
revision=self.revision,
|
| 1157 |
+
)
|
| 1158 |
+
else:
|
| 1159 |
+
quant_config_file = hf_hub_download(
|
| 1160 |
+
repo_id=self.model_path,
|
| 1161 |
+
filename="hf_quant_config.json",
|
| 1162 |
+
revision=self.revision,
|
| 1163 |
+
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
|
| 1164 |
+
)
|
| 1165 |
+
with open(quant_config_file) as f:
|
| 1166 |
+
quant_config_dict = json.load(f)
|
| 1167 |
+
quant_cfg = self._parse_modelopt_quant_config(quant_config_dict)
|
| 1168 |
+
except huggingface_hub.errors.LocalEntryNotFoundError:
|
| 1169 |
+
# Offline mode and file not in cache - this is normal for non-quantized models
|
| 1170 |
+
logger.debug(
|
| 1171 |
+
f"hf_quant_config.json not found in cache for {self.model_path} "
|
| 1172 |
+
"(offline mode, normal for non-quantized models)"
|
| 1173 |
+
)
|
| 1174 |
+
except huggingface_hub.errors.OfflineModeIsEnabled:
|
| 1175 |
+
# Should not reach here after our changes, but keep for safety
|
| 1176 |
+
logger.warning(
|
| 1177 |
+
"Offline mode is enabled, skipping hf_quant_config.json check"
|
| 1178 |
+
)
|
| 1179 |
+
except Exception as e:
|
| 1180 |
+
logger.warning(
|
| 1181 |
+
"Failed to load hf_quant_config.json for model %s: %s",
|
| 1182 |
+
self.model_path,
|
| 1183 |
+
e,
|
| 1184 |
+
)
|
| 1185 |
+
elif os.path.exists(os.path.join(self.model_path, "hf_quant_config.json")):
|
| 1186 |
+
quant_config_file = os.path.join(
|
| 1187 |
+
self.model_path, "hf_quant_config.json"
|
| 1188 |
+
)
|
| 1189 |
+
with open(quant_config_file) as f:
|
| 1190 |
+
quant_config_dict = json.load(f)
|
| 1191 |
+
quant_cfg = self._parse_modelopt_quant_config(quant_config_dict)
|
| 1192 |
+
return quant_cfg
|
| 1193 |
+
|
| 1194 |
+
def _find_quant_modelslim_config(self):
|
| 1195 |
+
if self.is_draft_model:
|
| 1196 |
+
return None
|
| 1197 |
+
quant_config_file = Path(self.model_path, "quant_model_description.json")
|
| 1198 |
+
quant_cfg = None
|
| 1199 |
+
if quant_config_file.is_file():
|
| 1200 |
+
with open(quant_config_file) as f:
|
| 1201 |
+
quant_cfg = json.load(f)
|
| 1202 |
+
# This field is required for flagless model loading but is not present in
|
| 1203 |
+
# modelslim model description, so we're adding it here manually.
|
| 1204 |
+
quant_cfg["quant_method"] = "modelslim"
|
| 1205 |
+
|
| 1206 |
+
return quant_cfg
|
| 1207 |
+
|
| 1208 |
+
def _parse_modelopt_quant_config(self, quant_config_dict: dict) -> Optional[dict]:
|
| 1209 |
+
"""Parse ModelOpt quantization config and return the appropriate quant_method."""
|
| 1210 |
+
json_quant_configs = quant_config_dict["quantization"]
|
| 1211 |
+
quant_algo = json_quant_configs.get("quant_algo", None)
|
| 1212 |
+
|
| 1213 |
+
if quant_algo == "MIXED_PRECISION":
|
| 1214 |
+
quantized_layers = json_quant_configs.get("quantized_layers") or {}
|
| 1215 |
+
has_modelopt_nvfp4_layers = any(
|
| 1216 |
+
str(layer_info.get("quant_algo", "")).upper()
|
| 1217 |
+
in ("NVFP4", "W4A16_NVFP4")
|
| 1218 |
+
for layer_info in quantized_layers.values()
|
| 1219 |
+
if isinstance(layer_info, dict)
|
| 1220 |
+
)
|
| 1221 |
+
if has_modelopt_nvfp4_layers:
|
| 1222 |
+
return {"quant_method": "modelopt_mixed", "quant_algo": quant_algo}
|
| 1223 |
+
return {"quant_method": "w4afp8", "quant_algo": quant_algo}
|
| 1224 |
+
elif quant_algo and ("FP4" in quant_algo or "NVFP4" in quant_algo):
|
| 1225 |
+
return {"quant_method": "modelopt_fp4", "quant_algo": quant_algo}
|
| 1226 |
+
elif quant_algo and "FP8" in quant_algo:
|
| 1227 |
+
return {"quant_method": "modelopt_fp8", "quant_algo": quant_algo}
|
| 1228 |
+
else:
|
| 1229 |
+
return None
|
| 1230 |
+
|
| 1231 |
+
def get_quantization_config_log_str(self) -> Optional[str]:
|
| 1232 |
+
"""
|
| 1233 |
+
Get a concise string representation of the quantization config for logging.
|
| 1234 |
+
Returns something like "quant=fp8, fmt=e4m3" or "quant=gptq, bits=4".
|
| 1235 |
+
"""
|
| 1236 |
+
try:
|
| 1237 |
+
quant_cfg = self._parse_quant_hf_config()
|
| 1238 |
+
if not quant_cfg:
|
| 1239 |
+
return None
|
| 1240 |
+
|
| 1241 |
+
quant_method = quant_cfg.get("quant_method", "quantized")
|
| 1242 |
+
log_str = f"quant={quant_method}"
|
| 1243 |
+
|
| 1244 |
+
# Append interesting fields if they exist
|
| 1245 |
+
for field in ["bits", "quant_algo", "fmt"]:
|
| 1246 |
+
if field in quant_cfg:
|
| 1247 |
+
log_str += f", {field}={quant_cfg[field]}"
|
| 1248 |
+
|
| 1249 |
+
return log_str
|
| 1250 |
+
except Exception:
|
| 1251 |
+
return None
|
| 1252 |
+
|
| 1253 |
+
def _is_already_quantized(self) -> bool:
|
| 1254 |
+
"""Check if the model is already quantized based on config files."""
|
| 1255 |
+
# Check for quantization in hf_config (config.json)
|
| 1256 |
+
if getattr(self.hf_config, "quantization_config", None) or getattr(
|
| 1257 |
+
self.hf_config, "compression_config", None
|
| 1258 |
+
):
|
| 1259 |
+
return True
|
| 1260 |
+
|
| 1261 |
+
# Check for HuggingFace quantization config
|
| 1262 |
+
quant_cfg = getattr(self.hf_config, "quantization_config", None)
|
| 1263 |
+
if quant_cfg is None:
|
| 1264 |
+
from sglang.srt.utils import has_hf_quant_config
|
| 1265 |
+
|
| 1266 |
+
return has_hf_quant_config(self.model_path)
|
| 1267 |
+
return True
|
| 1268 |
+
|
| 1269 |
+
def _get_modelopt_quant_type(self) -> str:
|
| 1270 |
+
"""Extract ModelOpt quantization type from unified quantization flag."""
|
| 1271 |
+
if self.quantization == "modelopt_fp8":
|
| 1272 |
+
return "fp8"
|
| 1273 |
+
elif self.quantization == "modelopt_fp4":
|
| 1274 |
+
return "nvfp4"
|
| 1275 |
+
elif self.quantization == "modelopt_mixed":
|
| 1276 |
+
raise ValueError(
|
| 1277 |
+
"modelopt_mixed is only supported for pre-quantized checkpoints."
|
| 1278 |
+
)
|
| 1279 |
+
elif self.quantization == "modelopt":
|
| 1280 |
+
# Auto-detect from model config
|
| 1281 |
+
quant_cfg = self._parse_quant_hf_config()
|
| 1282 |
+
if quant_cfg:
|
| 1283 |
+
quant_method = quant_cfg.get("quant_method", "").lower()
|
| 1284 |
+
if "fp4" in quant_method:
|
| 1285 |
+
return "fp4"
|
| 1286 |
+
elif "fp8" in quant_method:
|
| 1287 |
+
return "fp8"
|
| 1288 |
+
# Default to fp8 if can't detect
|
| 1289 |
+
return "fp8"
|
| 1290 |
+
else:
|
| 1291 |
+
return "fp8" # Default fallback
|
| 1292 |
+
|
| 1293 |
+
def _get_sliding_window_size(self) -> Optional[int]:
|
| 1294 |
+
for key in ("sliding_window_size", "sliding_window", "window_size"):
|
| 1295 |
+
value = getattr(self.hf_text_config, key, None)
|
| 1296 |
+
if value is not None:
|
| 1297 |
+
return value
|
| 1298 |
+
return None
|
| 1299 |
+
|
| 1300 |
+
def _validate_quantize_and_serve_config(self):
|
| 1301 |
+
"""Validate quantize_and_serve configuration."""
|
| 1302 |
+
if not self.quantize_and_serve:
|
| 1303 |
+
return
|
| 1304 |
+
|
| 1305 |
+
# Check if ModelOpt quantization is specified
|
| 1306 |
+
_MODELOPT_QUANTIZATION_METHODS = [
|
| 1307 |
+
"modelopt",
|
| 1308 |
+
"modelopt_fp8",
|
| 1309 |
+
"modelopt_fp4",
|
| 1310 |
+
"nvfp4_online",
|
| 1311 |
+
"modelopt_mixed",
|
| 1312 |
+
]
|
| 1313 |
+
modelopt_quantization_specified = (
|
| 1314 |
+
self.quantization in _MODELOPT_QUANTIZATION_METHODS
|
| 1315 |
+
)
|
| 1316 |
+
|
| 1317 |
+
if not modelopt_quantization_specified:
|
| 1318 |
+
raise ValueError(
|
| 1319 |
+
"quantize_and_serve requires ModelOpt quantization (set with --quantization "
|
| 1320 |
+
f"{{{', '.join(sorted(_MODELOPT_QUANTIZATION_METHODS))}}})"
|
| 1321 |
+
)
|
| 1322 |
+
|
| 1323 |
+
# quantize_and_serve is disabled due to compatibility issues
|
| 1324 |
+
raise NotImplementedError(
|
| 1325 |
+
"quantize_and_serve functionality is currently disabled due to compatibility issues. "
|
| 1326 |
+
"Please use the separate quantize-then-deploy workflow instead. "
|
| 1327 |
+
"Step 1: Quantize and export model. "
|
| 1328 |
+
"Step 2: Deploy the exported model."
|
| 1329 |
+
)
|
| 1330 |
+
|
| 1331 |
+
# adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/config.py
|
| 1332 |
+
def _verify_quantization(self) -> None:
|
| 1333 |
+
supported_quantization = [*QUANTIZATION_METHODS]
|
| 1334 |
+
rocm_supported_quantization = [
|
| 1335 |
+
"awq",
|
| 1336 |
+
"gptq",
|
| 1337 |
+
"fp8",
|
| 1338 |
+
"compressed_tensors",
|
| 1339 |
+
"compressed-tensors",
|
| 1340 |
+
"w8a8_fp8",
|
| 1341 |
+
"petit_nvfp4",
|
| 1342 |
+
"quark",
|
| 1343 |
+
"mxfp4",
|
| 1344 |
+
"mxfp8",
|
| 1345 |
+
"auto-round",
|
| 1346 |
+
"auto-round-int8",
|
| 1347 |
+
"quark_int4fp8_moe",
|
| 1348 |
+
"quark_mxfp4",
|
| 1349 |
+
]
|
| 1350 |
+
optimized_quantization_methods = [
|
| 1351 |
+
"fp8",
|
| 1352 |
+
"marlin",
|
| 1353 |
+
"modelopt_fp8",
|
| 1354 |
+
"modelopt_fp4",
|
| 1355 |
+
"modelopt_mixed",
|
| 1356 |
+
"nvfp4_online",
|
| 1357 |
+
"gptq_marlin_24",
|
| 1358 |
+
"gptq_marlin",
|
| 1359 |
+
"awq_marlin",
|
| 1360 |
+
"compressed_tensors",
|
| 1361 |
+
"compressed-tensors",
|
| 1362 |
+
"experts_int8",
|
| 1363 |
+
"w8a8_int8",
|
| 1364 |
+
"w8a8_fp8",
|
| 1365 |
+
"moe_wna16",
|
| 1366 |
+
"w4afp8",
|
| 1367 |
+
"petit_nvfp4",
|
| 1368 |
+
"quark",
|
| 1369 |
+
"modelslim",
|
| 1370 |
+
"humming",
|
| 1371 |
+
"quark_mxfp4",
|
| 1372 |
+
]
|
| 1373 |
+
compatible_quantization_methods = {
|
| 1374 |
+
"modelopt_fp8": ["modelopt"],
|
| 1375 |
+
"modelopt_fp4": ["modelopt"],
|
| 1376 |
+
"modelopt_mixed": ["modelopt"],
|
| 1377 |
+
"nvfp4_online": ["fp8"],
|
| 1378 |
+
"petit_nvfp4": ["modelopt"],
|
| 1379 |
+
"w8a8_int8": ["compressed-tensors", "compressed_tensors"],
|
| 1380 |
+
"w8a8_fp8": ["compressed-tensors", "compressed_tensors"],
|
| 1381 |
+
"auto-round-int8": ["compressed-tensors", "compressed_tensors"],
|
| 1382 |
+
}
|
| 1383 |
+
if self.quantization is not None:
|
| 1384 |
+
self.quantization = self.quantization.lower()
|
| 1385 |
+
|
| 1386 |
+
# Parse quantization method from the HF and ModelSlim model config, if available.
|
| 1387 |
+
# Only one function should return config, other should return None.
|
| 1388 |
+
cfg_list = []
|
| 1389 |
+
hf_config = self._parse_quant_hf_config()
|
| 1390 |
+
modelslim_config = self._find_quant_modelslim_config()
|
| 1391 |
+
quant_config = modelslim_config or hf_config
|
| 1392 |
+
if quant_config is not None:
|
| 1393 |
+
cfg_list.append(quant_config)
|
| 1394 |
+
|
| 1395 |
+
# Filter out None values
|
| 1396 |
+
cfg_list = [item for item in cfg_list if item is not None]
|
| 1397 |
+
if len(cfg_list) > 1:
|
| 1398 |
+
raise ValueError(
|
| 1399 |
+
"Config list contains configs from 2 methods, must be only 1"
|
| 1400 |
+
)
|
| 1401 |
+
quant_cfg = cfg_list[0] if cfg_list else None
|
| 1402 |
+
|
| 1403 |
+
if quant_cfg is not None:
|
| 1404 |
+
quant_method = quant_cfg.get(
|
| 1405 |
+
"quant_method", "" if not self.quantization else self.quantization
|
| 1406 |
+
).lower()
|
| 1407 |
+
|
| 1408 |
+
# Detect which checkpoint is it
|
| 1409 |
+
for _, method in QUANTIZATION_METHODS.items():
|
| 1410 |
+
quantization_override = method.override_quantization_method(
|
| 1411 |
+
quant_cfg, self.quantization
|
| 1412 |
+
)
|
| 1413 |
+
if quantization_override:
|
| 1414 |
+
quant_method = quantization_override
|
| 1415 |
+
self.quantization = quantization_override
|
| 1416 |
+
break
|
| 1417 |
+
|
| 1418 |
+
# Verify quantization configurations.
|
| 1419 |
+
if self.quantization is None:
|
| 1420 |
+
self.quantization = quant_method
|
| 1421 |
+
elif self.quantization != quant_method:
|
| 1422 |
+
# Check if the CLI-specified quantization is compatible with HF config's quant_method
|
| 1423 |
+
is_compatible = (
|
| 1424 |
+
self.quantization in compatible_quantization_methods
|
| 1425 |
+
and quant_method
|
| 1426 |
+
in compatible_quantization_methods[self.quantization]
|
| 1427 |
+
)
|
| 1428 |
+
if is_compatible:
|
| 1429 |
+
# Keep the CLI-specified quantization (e.g., modelopt_fp4) even if
|
| 1430 |
+
# HF config says "modelopt" - they are compatible
|
| 1431 |
+
logger.info(
|
| 1432 |
+
f"Using CLI-specified quantization ({self.quantization}) which is "
|
| 1433 |
+
f"compatible with HF config quant_method ({quant_method})."
|
| 1434 |
+
)
|
| 1435 |
+
elif self.is_draft_model:
|
| 1436 |
+
# Allow auto-detection of quantization from checkpoint for draft model
|
| 1437 |
+
# only if the CLI quantization is not compatible
|
| 1438 |
+
logger.info(
|
| 1439 |
+
f"Draft model quantization ({quant_method}) differs from "
|
| 1440 |
+
f"main model quantization ({self.quantization}). "
|
| 1441 |
+
f"Using draft model's detected quantization: {quant_method}"
|
| 1442 |
+
)
|
| 1443 |
+
self.quantization = quant_method
|
| 1444 |
+
else:
|
| 1445 |
+
raise ValueError(
|
| 1446 |
+
"Quantization method specified in the model config "
|
| 1447 |
+
f"({quant_method}) does not match the quantization "
|
| 1448 |
+
f"method specified in the `quantization` argument "
|
| 1449 |
+
f"({self.quantization})."
|
| 1450 |
+
)
|
| 1451 |
+
|
| 1452 |
+
# Warn if DeepGemm is enabled for a non-ue8m0 checkpoint on Blackwell.
|
| 1453 |
+
# MXFP8 stores E8M0 block scales that DeepGemm consumes losslessly, so skip the warning there.
|
| 1454 |
+
self.use_scale_ue8m0 = quant_cfg.get("scale_fmt", None) == "ue8m0"
|
| 1455 |
+
from sglang.srt.layers import deep_gemm_wrapper
|
| 1456 |
+
|
| 1457 |
+
if (
|
| 1458 |
+
not self.use_scale_ue8m0
|
| 1459 |
+
and deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0
|
| 1460 |
+
and self.quantization != "mxfp8"
|
| 1461 |
+
):
|
| 1462 |
+
logger.warning(
|
| 1463 |
+
"DeepGemm is enabled but the scale_fmt of checkpoint is not ue8m0. This might cause accuracy degradation on Blackwell."
|
| 1464 |
+
)
|
| 1465 |
+
|
| 1466 |
+
if self.quantization is not None:
|
| 1467 |
+
if self.quantization not in supported_quantization:
|
| 1468 |
+
raise ValueError(
|
| 1469 |
+
f"Unknown quantization method: {self.quantization}. Must "
|
| 1470 |
+
f"be one of {supported_quantization}."
|
| 1471 |
+
)
|
| 1472 |
+
if is_hip() and self.quantization not in rocm_supported_quantization:
|
| 1473 |
+
raise ValueError(
|
| 1474 |
+
f"{self.quantization} quantization is currently not "
|
| 1475 |
+
f"supported in ROCm."
|
| 1476 |
+
)
|
| 1477 |
+
if self.quantization not in optimized_quantization_methods:
|
| 1478 |
+
# Don't warn for MXFP4/MXFP8 on SM100 since they have optimized kernels
|
| 1479 |
+
if not (
|
| 1480 |
+
self.quantization in ["mxfp4", "mxfp8"] and is_sm100_supported()
|
| 1481 |
+
):
|
| 1482 |
+
logger.warning(
|
| 1483 |
+
"%s quantization is not fully "
|
| 1484 |
+
"optimized yet. The speed can be slower than "
|
| 1485 |
+
"non-quantized models.",
|
| 1486 |
+
self.quantization,
|
| 1487 |
+
)
|
| 1488 |
+
|
| 1489 |
+
def _verify_dual_chunk_attention_config(self) -> None:
|
| 1490 |
+
if hasattr(self.hf_config, "dual_chunk_attention_config"):
|
| 1491 |
+
# Try loading the sparse attention config
|
| 1492 |
+
sparse_attn_config = get_sparse_attention_config(self.model_path)
|
| 1493 |
+
if not sparse_attn_config:
|
| 1494 |
+
return
|
| 1495 |
+
self.hf_config.dual_chunk_attention_config["sparse_attention_config"] = (
|
| 1496 |
+
sparse_attn_config
|
| 1497 |
+
)
|
| 1498 |
+
if (
|
| 1499 |
+
"sparse_attention_enabled"
|
| 1500 |
+
not in self.hf_config.dual_chunk_attention_config
|
| 1501 |
+
):
|
| 1502 |
+
self.hf_config.dual_chunk_attention_config[
|
| 1503 |
+
"sparse_attention_enabled"
|
| 1504 |
+
] = True
|
| 1505 |
+
|
| 1506 |
+
def _get_hf_eos_token_id(self) -> Optional[Set[int]]:
|
| 1507 |
+
eos_ids = getattr(self.hf_config, "eos_token_id", None)
|
| 1508 |
+
if eos_ids is not None:
|
| 1509 |
+
# it can be either int or list of int
|
| 1510 |
+
eos_ids = {eos_ids} if isinstance(eos_ids, int) else set(eos_ids)
|
| 1511 |
+
if eos_ids is None:
|
| 1512 |
+
eos_ids = set()
|
| 1513 |
+
if self.hf_generation_config:
|
| 1514 |
+
generation_eos_ids = getattr(
|
| 1515 |
+
self.hf_generation_config, "eos_token_id", None
|
| 1516 |
+
)
|
| 1517 |
+
if generation_eos_ids:
|
| 1518 |
+
generation_eos_ids = (
|
| 1519 |
+
{generation_eos_ids}
|
| 1520 |
+
if isinstance(generation_eos_ids, int)
|
| 1521 |
+
else set(generation_eos_ids)
|
| 1522 |
+
)
|
| 1523 |
+
eos_ids = eos_ids | generation_eos_ids
|
| 1524 |
+
return eos_ids
|
| 1525 |
+
|
| 1526 |
+
def get_default_sampling_params(self) -> dict[str, Any]:
|
| 1527 |
+
"""
|
| 1528 |
+
Get default sampling parameters from the model's generation config.
|
| 1529 |
+
|
| 1530 |
+
This method returns non-default sampling parameters from the model's
|
| 1531 |
+
generation_config.json when sampling_defaults is set to "model".
|
| 1532 |
+
|
| 1533 |
+
Returns:
|
| 1534 |
+
A dictionary containing the non-default sampling parameters.
|
| 1535 |
+
"""
|
| 1536 |
+
if self.sampling_defaults != "model":
|
| 1537 |
+
return {}
|
| 1538 |
+
|
| 1539 |
+
if self.hf_generation_config is None:
|
| 1540 |
+
return {}
|
| 1541 |
+
|
| 1542 |
+
config = self.hf_generation_config.to_dict()
|
| 1543 |
+
|
| 1544 |
+
available_params = [
|
| 1545 |
+
"repetition_penalty",
|
| 1546 |
+
"temperature",
|
| 1547 |
+
"top_k",
|
| 1548 |
+
"top_p",
|
| 1549 |
+
"min_p",
|
| 1550 |
+
]
|
| 1551 |
+
|
| 1552 |
+
default_sampling_params = {
|
| 1553 |
+
p: config.get(p) for p in available_params if config.get(p) is not None
|
| 1554 |
+
}
|
| 1555 |
+
|
| 1556 |
+
return default_sampling_params
|
| 1557 |
+
|
| 1558 |
+
def _maybe_pull_model_for_runai(self, model: str) -> None:
|
| 1559 |
+
if is_runai_obj_uri(model):
|
| 1560 |
+
# local path for loading the config
|
| 1561 |
+
self.model_path = ObjectStorageModel.get_path(model)
|
| 1562 |
+
# remote path for loading the weights
|
| 1563 |
+
self.model_weights = model
|
| 1564 |
+
|
| 1565 |
+
def _maybe_pull_model_tokenizer_from_remote(self) -> None:
|
| 1566 |
+
"""
|
| 1567 |
+
Pull the model config files to a temporary
|
| 1568 |
+
directory in case of remote.
|
| 1569 |
+
|
| 1570 |
+
Args:
|
| 1571 |
+
model: The model name or path.
|
| 1572 |
+
|
| 1573 |
+
"""
|
| 1574 |
+
from sglang.srt.connector import create_remote_connector
|
| 1575 |
+
from sglang.srt.utils import is_remote_url
|
| 1576 |
+
|
| 1577 |
+
if is_remote_url(self.model_path):
|
| 1578 |
+
logger.info("Pulling model configs from remote...")
|
| 1579 |
+
# BaseConnector implements __del__() to clean up the local dir.
|
| 1580 |
+
# Since config files need to exist all the time, so we DO NOT use
|
| 1581 |
+
# with statement to avoid closing the client.
|
| 1582 |
+
client = create_remote_connector(self.model_path)
|
| 1583 |
+
if is_remote_url(self.model_path):
|
| 1584 |
+
client.pull_files(allow_pattern=["*config.json"])
|
| 1585 |
+
self.model_weights = self.model_path
|
| 1586 |
+
self.model_path = client.get_local_dir()
|
| 1587 |
+
|
| 1588 |
+
|
| 1589 |
+
# adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/config.py
|
| 1590 |
+
_STR_DTYPE_TO_TORCH_DTYPE = {
|
| 1591 |
+
"half": torch.float16,
|
| 1592 |
+
"float16": torch.float16,
|
| 1593 |
+
"float": torch.float32,
|
| 1594 |
+
"float32": torch.float32,
|
| 1595 |
+
"bfloat16": torch.bfloat16,
|
| 1596 |
+
}
|
| 1597 |
+
|
| 1598 |
+
|
| 1599 |
+
# adapted from https://github.com/vllm-project/vllm/blob/v0.6.4.post1/vllm/config.py
|
| 1600 |
+
def _get_and_verify_dtype(
|
| 1601 |
+
config: PretrainedConfig,
|
| 1602 |
+
dtype: Union[str, torch.dtype],
|
| 1603 |
+
) -> torch.dtype:
|
| 1604 |
+
# NOTE: getattr(config, "torch_dtype", torch.float32) is not correct
|
| 1605 |
+
# because config.torch_dtype can be None.
|
| 1606 |
+
if isinstance(config, dict):
|
| 1607 |
+
config_dtype = config.get("dtype", None) or config.get("torch_dtype", None)
|
| 1608 |
+
model_type = config.get("model_type", "")
|
| 1609 |
+
else:
|
| 1610 |
+
config_dtype = getattr(config, "dtype", None)
|
| 1611 |
+
model_type = getattr(config, "model_type", "")
|
| 1612 |
+
if isinstance(config_dtype, str):
|
| 1613 |
+
config_dtype = _STR_DTYPE_TO_TORCH_DTYPE.get(config_dtype, None)
|
| 1614 |
+
if config_dtype is None:
|
| 1615 |
+
config_dtype = torch.float32
|
| 1616 |
+
|
| 1617 |
+
if isinstance(dtype, str):
|
| 1618 |
+
dtype = dtype.lower()
|
| 1619 |
+
if dtype == "auto":
|
| 1620 |
+
if config_dtype == torch.float32:
|
| 1621 |
+
if model_type.startswith("gemma"):
|
| 1622 |
+
if model_type == "gemma":
|
| 1623 |
+
gemma_version = ""
|
| 1624 |
+
else:
|
| 1625 |
+
gemma_version = model_type[5]
|
| 1626 |
+
logger.info(
|
| 1627 |
+
f"For Gemma {gemma_version}, we downcast float32 to bfloat16 instead "
|
| 1628 |
+
"of float16 by default. Please specify `dtype` if you "
|
| 1629 |
+
"want to use float16."
|
| 1630 |
+
)
|
| 1631 |
+
torch_dtype = torch.bfloat16
|
| 1632 |
+
else:
|
| 1633 |
+
# Following the common practice, we use float16 for float32
|
| 1634 |
+
# models.
|
| 1635 |
+
torch_dtype = torch.float16
|
| 1636 |
+
else:
|
| 1637 |
+
torch_dtype = config_dtype
|
| 1638 |
+
else:
|
| 1639 |
+
if dtype not in _STR_DTYPE_TO_TORCH_DTYPE:
|
| 1640 |
+
raise ValueError(f"Unknown dtype: {dtype}")
|
| 1641 |
+
torch_dtype = _STR_DTYPE_TO_TORCH_DTYPE[dtype]
|
| 1642 |
+
elif isinstance(dtype, torch.dtype):
|
| 1643 |
+
torch_dtype = dtype
|
| 1644 |
+
else:
|
| 1645 |
+
raise ValueError(f"Unknown dtype: {dtype}")
|
| 1646 |
+
|
| 1647 |
+
# Verify the dtype.
|
| 1648 |
+
if torch_dtype != config_dtype:
|
| 1649 |
+
if torch_dtype == torch.float32:
|
| 1650 |
+
# Upcasting to float32 is allowed.
|
| 1651 |
+
logger.debug("Upcasting %s to %s.", config_dtype, torch_dtype)
|
| 1652 |
+
pass
|
| 1653 |
+
elif config_dtype == torch.float32:
|
| 1654 |
+
# Downcasting from float32 to float16 or bfloat16 is allowed.
|
| 1655 |
+
logger.debug("Downcasting %s to %s.", config_dtype, torch_dtype)
|
| 1656 |
+
pass
|
| 1657 |
+
else:
|
| 1658 |
+
# Casting between float16 and bfloat16 is allowed with a warning.
|
| 1659 |
+
logger.debug("Casting %s to %s.", config_dtype, torch_dtype)
|
| 1660 |
+
|
| 1661 |
+
return torch_dtype
|
| 1662 |
+
|
| 1663 |
+
|
| 1664 |
+
def is_generation_model(model_architectures: List[str], is_embedding: bool = False):
|
| 1665 |
+
# We have two ways to determine whether a model is a generative model.
|
| 1666 |
+
# 1. Check the model architecture
|
| 1667 |
+
# 2. check the `is_embedding` server args
|
| 1668 |
+
|
| 1669 |
+
if (
|
| 1670 |
+
"LlamaEmbeddingModel" in model_architectures
|
| 1671 |
+
or "MistralModel" in model_architectures
|
| 1672 |
+
or "LlamaForSequenceClassification" in model_architectures
|
| 1673 |
+
or "LlamaForSequenceClassificationWithNormal_Weights" in model_architectures
|
| 1674 |
+
or "InternLM2ForRewardModel" in model_architectures
|
| 1675 |
+
or "Qwen2ForRewardModel" in model_architectures
|
| 1676 |
+
or "Qwen3ForRewardModel" in model_architectures
|
| 1677 |
+
or "Qwen2ForSequenceClassification" in model_architectures
|
| 1678 |
+
or "Qwen3ForSequenceClassification" in model_architectures
|
| 1679 |
+
or "CLIPModel" in model_architectures
|
| 1680 |
+
or "BertModel" in model_architectures
|
| 1681 |
+
or "Contriever" in model_architectures
|
| 1682 |
+
or "BertForSequenceClassification" in model_architectures
|
| 1683 |
+
or "XLMRobertaModel" in model_architectures
|
| 1684 |
+
or "XLMRobertaForSequenceClassification" in model_architectures
|
| 1685 |
+
or "Gemma2ForSequenceClassification" in model_architectures
|
| 1686 |
+
):
|
| 1687 |
+
return False
|
| 1688 |
+
else:
|
| 1689 |
+
return not is_embedding
|
| 1690 |
+
|
| 1691 |
+
|
| 1692 |
+
multimodal_model_archs = [
|
| 1693 |
+
"CLIPModel",
|
| 1694 |
+
"Cohere2VisionForConditionalGeneration",
|
| 1695 |
+
"DeepseekVL2ForCausalLM",
|
| 1696 |
+
"Ernie4_5_VLMoeForConditionalGeneration",
|
| 1697 |
+
"MiniMaxM3SparseForConditionalGeneration",
|
| 1698 |
+
"Gemma3ForConditionalGeneration",
|
| 1699 |
+
"Gemma3nForConditionalGeneration",
|
| 1700 |
+
"Gemma4ForConditionalGeneration",
|
| 1701 |
+
"Gemma4UnifiedForConditionalGeneration",
|
| 1702 |
+
"Glm4vForConditionalGeneration",
|
| 1703 |
+
"Glm4vMoeForConditionalGeneration",
|
| 1704 |
+
"GlmOcrForConditionalGeneration",
|
| 1705 |
+
"GlmAsrForConditionalGeneration",
|
| 1706 |
+
"GlmImageForConditionalGeneration",
|
| 1707 |
+
"Grok1VForCausalLM",
|
| 1708 |
+
"Grok1AForCausalLM",
|
| 1709 |
+
"LlavaLlamaForCausalLM",
|
| 1710 |
+
"Llama4ForConditionalGeneration",
|
| 1711 |
+
"LlavaMistralForCausalLM",
|
| 1712 |
+
"LlavaQwenForCausalLM",
|
| 1713 |
+
"LlavaForConditionalGeneration",
|
| 1714 |
+
"LlavaVidForCausalLM",
|
| 1715 |
+
"Lfm2VlForConditionalGeneration",
|
| 1716 |
+
"LightOnOCRForConditionalGeneration",
|
| 1717 |
+
*MIMO_V2_MULTIMODAL_ARCHS,
|
| 1718 |
+
"MiMoV2ASRForCausalLM",
|
| 1719 |
+
"MiniCPMO",
|
| 1720 |
+
"MiniCPMV",
|
| 1721 |
+
"Mistral3ForConditionalGeneration",
|
| 1722 |
+
"MultiModalityCausalLM",
|
| 1723 |
+
"MllamaForConditionalGeneration",
|
| 1724 |
+
"MossVLForConditionalGeneration",
|
| 1725 |
+
"NemotronH_Nano_VL_V2",
|
| 1726 |
+
"NemotronH_Nano_Omni_Reasoning_V3",
|
| 1727 |
+
"PixtralForConditionalGeneration",
|
| 1728 |
+
"Qwen2AudioForConditionalGeneration",
|
| 1729 |
+
"Qwen2VLForConditionalGeneration",
|
| 1730 |
+
"Qwen2_5_VLForConditionalGeneration",
|
| 1731 |
+
"Qwen3VLForConditionalGeneration",
|
| 1732 |
+
"Qwen3VLMoeForConditionalGeneration",
|
| 1733 |
+
"Qwen3_5ForConditionalGeneration",
|
| 1734 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 1735 |
+
"InternS2PreviewForConditionalGeneration",
|
| 1736 |
+
"Qwen3ASRForConditionalGeneration",
|
| 1737 |
+
"Qwen3OmniMoeForConditionalGeneration",
|
| 1738 |
+
"KimiVLForConditionalGeneration",
|
| 1739 |
+
"LocateAnythingForConditionalGeneration",
|
| 1740 |
+
"InternVLChatModel",
|
| 1741 |
+
"InternS1ForConditionalGeneration",
|
| 1742 |
+
"InternS1ProForConditionalGeneration",
|
| 1743 |
+
"Phi4MMForCausalLM",
|
| 1744 |
+
"VoxtralForConditionalGeneration",
|
| 1745 |
+
"WhisperForConditionalGeneration",
|
| 1746 |
+
"Step3VLForConditionalGeneration",
|
| 1747 |
+
"POINTSV15ChatModel",
|
| 1748 |
+
"DotsVLMForCausalLM",
|
| 1749 |
+
"DotsOCRForCausalLM",
|
| 1750 |
+
"Sarashina2VisionForCausalLM",
|
| 1751 |
+
"NVILAForConditionalGeneration",
|
| 1752 |
+
"NVILALiteForConditionalGeneration",
|
| 1753 |
+
"DeepseekOCRForCausalLM",
|
| 1754 |
+
"UnlimitedOCRForCausalLM",
|
| 1755 |
+
"JetVLMForConditionalGeneration",
|
| 1756 |
+
"PaddleOCRVLForConditionalGeneration",
|
| 1757 |
+
"MiDashengLMModel",
|
| 1758 |
+
"StepVLForConditionalGeneration",
|
| 1759 |
+
"Step3p7ForConditionalGeneration",
|
| 1760 |
+
"KimiK25ForConditionalGeneration",
|
| 1761 |
+
]
|
| 1762 |
+
|
| 1763 |
+
piecewise_cuda_graph_disabled_model_archs = [
|
| 1764 |
+
"AgnesForCausalLM",
|
| 1765 |
+
"AgnesForCausalLMNextN",
|
| 1766 |
+
"AgnesForCausalLMDSpark",
|
| 1767 |
+
"Qwen3NextForCausalLM",
|
| 1768 |
+
"BailingMoeV2_5ForCausalLM",
|
| 1769 |
+
"LLaDAModelLM",
|
| 1770 |
+
]
|
| 1771 |
+
|
| 1772 |
+
# Multimodal archs allowed to keep prefill piecewise CUDA graph enabled. The
|
| 1773 |
+
# generic "multimodal model" rule in ServerArgs disables prefill piecewise CG for
|
| 1774 |
+
# all multimodal models; archs here opt back in because their LM prefill captures
|
| 1775 |
+
# cleanly (vision encoder runs eagerly outside the graph via general_mm_embed_routine).
|
| 1776 |
+
multimodal_piecewise_cuda_graph_supported_model_archs = [
|
| 1777 |
+
"Cohere2VisionForConditionalGeneration",
|
| 1778 |
+
"KimiK25ForConditionalGeneration",
|
| 1779 |
+
"MiniMaxM3SparseForCausalLM",
|
| 1780 |
+
"MiniMaxM3SparseForConditionalGeneration",
|
| 1781 |
+
]
|
| 1782 |
+
|
| 1783 |
+
# Multimodal archs whose LM prefill is validated under breakable CUDA graph;
|
| 1784 |
+
# embed-carrying batches are rejected at replay (can_run_graph) and run eager.
|
| 1785 |
+
multimodal_breakable_cuda_graph_supported_model_archs = [
|
| 1786 |
+
"Qwen3_5ForConditionalGeneration",
|
| 1787 |
+
"Qwen3_5MoeForConditionalGeneration",
|
| 1788 |
+
]
|
| 1789 |
+
|
| 1790 |
+
if external_mm_model_arch := envs.SGLANG_EXTERNAL_MM_MODEL_ARCH.get():
|
| 1791 |
+
multimodal_model_archs.append(external_mm_model_arch)
|
| 1792 |
+
|
| 1793 |
+
|
| 1794 |
+
def is_multimodal_model(model_architectures: List[str]):
|
| 1795 |
+
if any(
|
| 1796 |
+
multi_model_arch in model_architectures
|
| 1797 |
+
for multi_model_arch in multimodal_model_archs
|
| 1798 |
+
):
|
| 1799 |
+
return True
|
| 1800 |
+
else:
|
| 1801 |
+
return False
|
| 1802 |
+
|
| 1803 |
+
|
| 1804 |
+
def is_audio_model(model_architectures: List[str]):
|
| 1805 |
+
models = [
|
| 1806 |
+
"WhisperForConditionalGeneration",
|
| 1807 |
+
"Qwen3ASRForConditionalGeneration",
|
| 1808 |
+
]
|
| 1809 |
+
return any(model in model_architectures for model in models)
|
| 1810 |
+
|
| 1811 |
+
|
| 1812 |
+
def is_encoder_decoder_model(model_architectures: List[str]):
|
| 1813 |
+
models = [
|
| 1814 |
+
"WhisperForConditionalGeneration",
|
| 1815 |
+
"MllamaForConditionalGeneration",
|
| 1816 |
+
"MossVLForConditionalGeneration",
|
| 1817 |
+
]
|
| 1818 |
+
return any(model in model_architectures for model in models)
|
| 1819 |
+
|
| 1820 |
+
|
| 1821 |
+
def is_local_attention_model(model_architectures: List[str]):
|
| 1822 |
+
return "Llama4ForConditionalGeneration" in model_architectures
|
| 1823 |
+
|
| 1824 |
+
|
| 1825 |
+
def is_multimodal_chunked_prefill_supported(model_architectures: List[str]):
|
| 1826 |
+
"""Check if chunked prefill is supported for a MultiModal model."""
|
| 1827 |
+
unsupported = [
|
| 1828 |
+
"Grok1VForCausalLM",
|
| 1829 |
+
"Grok1AForCausalLM",
|
| 1830 |
+
"LlavaLlamaForCausalLM",
|
| 1831 |
+
"MllamaForConditionalGeneration",
|
| 1832 |
+
"MossVLForConditionalGeneration",
|
| 1833 |
+
"CLIPModel",
|
| 1834 |
+
]
|
| 1835 |
+
if any(multi_model_arch in unsupported for multi_model_arch in model_architectures):
|
| 1836 |
+
return False
|
| 1837 |
+
else:
|
| 1838 |
+
return True
|
| 1839 |
+
|
| 1840 |
+
|
| 1841 |
+
def is_piecewise_cuda_graph_disabled_model(model_architectures: List[str]):
|
| 1842 |
+
return any(
|
| 1843 |
+
arch in piecewise_cuda_graph_disabled_model_archs
|
| 1844 |
+
for arch in model_architectures
|
| 1845 |
+
)
|
| 1846 |
+
|
| 1847 |
+
|
| 1848 |
+
def is_multimodal_piecewise_cuda_graph_supported(model_architectures: List[str]):
|
| 1849 |
+
"""Whether a multimodal arch may keep prefill piecewise CUDA graph enabled."""
|
| 1850 |
+
return any(
|
| 1851 |
+
arch in multimodal_piecewise_cuda_graph_supported_model_archs
|
| 1852 |
+
for arch in model_architectures
|
| 1853 |
+
)
|
| 1854 |
+
|
| 1855 |
+
|
| 1856 |
+
def is_multimodal_breakable_cuda_graph_supported(model_architectures: List[str]):
|
| 1857 |
+
"""Whether a multimodal arch may keep prefill breakable CUDA graph enabled."""
|
| 1858 |
+
return any(
|
| 1859 |
+
arch in multimodal_breakable_cuda_graph_supported_model_archs
|
| 1860 |
+
for arch in model_architectures
|
| 1861 |
+
)
|
| 1862 |
+
|
| 1863 |
+
|
| 1864 |
+
# SequenceClassification models that use CrossEncodingPooler
|
| 1865 |
+
_cross_encoding_pooler_archs = [
|
| 1866 |
+
"BertForSequenceClassification",
|
| 1867 |
+
"XLMRobertaForSequenceClassification",
|
| 1868 |
+
]
|
| 1869 |
+
|
| 1870 |
+
|
| 1871 |
+
def is_cross_encoding_pooler_model(model_architectures: List[str]) -> bool:
|
| 1872 |
+
return any(arch in _cross_encoding_pooler_archs for arch in model_architectures)
|
| 1873 |
+
|
| 1874 |
+
|
| 1875 |
+
def yarn_get_mscale(scale: float = 1, mscale: float = 1) -> float:
|
| 1876 |
+
if scale <= 1:
|
| 1877 |
+
return 1.0
|
| 1878 |
+
return 0.1 * mscale * math.log(scale) + 1.0
|
| 1879 |
+
|
| 1880 |
+
|
| 1881 |
+
def compute_mla_mscale_scaling(rope_scaling: dict, base_scaling: float) -> float:
|
| 1882 |
+
"""Compute MLA attention scaling factor from rope_scaling with mscale.
|
| 1883 |
+
|
| 1884 |
+
Used by DeepSeek, BailingMoe, SarvamMLA and similar MLA models.
|
| 1885 |
+
Warns if 'factor' is missing from rope_scaling (common in v5 configs).
|
| 1886 |
+
"""
|
| 1887 |
+
if not rope_scaling.get("apply_yarn_scaling", True) or not rope_scaling.get(
|
| 1888 |
+
"apply_scale", True
|
| 1889 |
+
):
|
| 1890 |
+
return base_scaling
|
| 1891 |
+
|
| 1892 |
+
mscale_all_dim = rope_scaling.get("mscale_all_dim", False)
|
| 1893 |
+
if "factor" not in rope_scaling:
|
| 1894 |
+
logger.warning(
|
| 1895 |
+
"rope_scaling missing 'factor', defaulting to 1.0. "
|
| 1896 |
+
"Check model accuracy.",
|
| 1897 |
+
)
|
| 1898 |
+
scaling_factor = rope_scaling.get("factor", 1.0)
|
| 1899 |
+
mscale = yarn_get_mscale(scaling_factor, float(mscale_all_dim))
|
| 1900 |
+
return base_scaling * mscale * mscale
|
| 1901 |
+
|
| 1902 |
+
|
| 1903 |
+
def is_hybrid_swa_model(
|
| 1904 |
+
model_architectures: List[str],
|
| 1905 |
+
hf_text_config: Optional[PretrainedConfig] = None,
|
| 1906 |
+
):
|
| 1907 |
+
|
| 1908 |
+
hybrid_swa_archs = {
|
| 1909 |
+
"Llama4ForConditionalGeneration",
|
| 1910 |
+
"AgnesForCausalLM",
|
| 1911 |
+
"AgnesForCausalLMNextN",
|
| 1912 |
+
"AgnesForCausalLMDSpark",
|
| 1913 |
+
"GptOssForCausalLM",
|
| 1914 |
+
*MIMO_V2_MODEL_ARCHS,
|
| 1915 |
+
"MiMoV2MTP",
|
| 1916 |
+
"Step3p5ForCausalLM",
|
| 1917 |
+
"Step3p5MTP",
|
| 1918 |
+
"Step3p7ForConditionalGeneration",
|
| 1919 |
+
"Gemma4ForCausalLM",
|
| 1920 |
+
"Gemma4ForConditionalGeneration",
|
| 1921 |
+
"Gemma4UnifiedForConditionalGeneration",
|
| 1922 |
+
"LagunaForCausalLM",
|
| 1923 |
+
"MellumForCausalLM",
|
| 1924 |
+
"InklingForConditionalGeneration",
|
| 1925 |
+
"InklingForConditionalGenerationMTP",
|
| 1926 |
+
"UnlimitedOCRForCausalLM",
|
| 1927 |
+
}
|
| 1928 |
+
if any(arch in hybrid_swa_archs for arch in model_architectures):
|
| 1929 |
+
# Only treat Laguna as hybrid SWA when it actually has a sliding window.
|
| 1930 |
+
if (
|
| 1931 |
+
"LagunaForCausalLM" in model_architectures
|
| 1932 |
+
and hf_text_config is not None
|
| 1933 |
+
and not getattr(hf_text_config, "sliding_window", 0)
|
| 1934 |
+
):
|
| 1935 |
+
return False
|
| 1936 |
+
return True
|
| 1937 |
+
# Also recognize models that explicitly opt-in via their HF text config,
|
| 1938 |
+
# so custom hybrid-SWA architectures don't need to be added to the allowlist.
|
| 1939 |
+
if hf_text_config is not None and getattr(hf_text_config, "is_hybrid_swa", False):
|
| 1940 |
+
return True
|
| 1941 |
+
return False
|
| 1942 |
+
|
| 1943 |
+
|
| 1944 |
+
def get_hybrid_layer_ids(
|
| 1945 |
+
model_architectures: List[str],
|
| 1946 |
+
hf_text_config: PretrainedConfig,
|
| 1947 |
+
):
|
| 1948 |
+
num_hidden_layers = hf_text_config.num_hidden_layers
|
| 1949 |
+
if "Llama4ForConditionalGeneration" in model_architectures:
|
| 1950 |
+
swa_attention_layer_ids = [
|
| 1951 |
+
i for i in range(num_hidden_layers) if (i + 1) % 4 != 0
|
| 1952 |
+
]
|
| 1953 |
+
full_attention_layer_ids = [
|
| 1954 |
+
i for i in range(num_hidden_layers) if (i + 1) % 4 == 0
|
| 1955 |
+
]
|
| 1956 |
+
elif "GptOssForCausalLM" in model_architectures:
|
| 1957 |
+
layer_types = getattr(hf_text_config, "layer_types", [])
|
| 1958 |
+
swa_attention_layer_ids = [
|
| 1959 |
+
i for i, x in enumerate(layer_types) if x == "sliding_attention"
|
| 1960 |
+
]
|
| 1961 |
+
full_attention_layer_ids = [
|
| 1962 |
+
i for i, x in enumerate(layer_types) if x == "full_attention"
|
| 1963 |
+
]
|
| 1964 |
+
elif any(arch in MIMO_V2_MODEL_ARCHS for arch in model_architectures):
|
| 1965 |
+
hybrid_layer_pattern = getattr(hf_text_config, "hybrid_layer_pattern", None)
|
| 1966 |
+
swa_attention_layer_ids = [
|
| 1967 |
+
i for i in range(num_hidden_layers) if hybrid_layer_pattern[i] == 1
|
| 1968 |
+
]
|
| 1969 |
+
full_attention_layer_ids = [
|
| 1970 |
+
i for i in range(num_hidden_layers) if hybrid_layer_pattern[i] == 0
|
| 1971 |
+
]
|
| 1972 |
+
elif "MiMoV2MTP" in model_architectures:
|
| 1973 |
+
swa_attention_layer_ids = [0]
|
| 1974 |
+
full_attention_layer_ids = []
|
| 1975 |
+
elif (
|
| 1976 |
+
"Step3p5ForCausalLM" in model_architectures
|
| 1977 |
+
or "Step3p7ForConditionalGeneration" in model_architectures
|
| 1978 |
+
):
|
| 1979 |
+
layer_types = hf_text_config.layer_types
|
| 1980 |
+
swa_attention_layer_ids = [
|
| 1981 |
+
i
|
| 1982 |
+
for i, x in enumerate(layer_types)
|
| 1983 |
+
if x == "sliding_attention" and i < num_hidden_layers
|
| 1984 |
+
]
|
| 1985 |
+
full_attention_layer_ids = [
|
| 1986 |
+
i
|
| 1987 |
+
for i, x in enumerate(layer_types)
|
| 1988 |
+
if x == "full_attention" and i < num_hidden_layers
|
| 1989 |
+
]
|
| 1990 |
+
elif "Step3p5MTP" in model_architectures:
|
| 1991 |
+
swa_attention_layer_ids = [0]
|
| 1992 |
+
full_attention_layer_ids = []
|
| 1993 |
+
elif (
|
| 1994 |
+
"Gemma4ForCausalLM" in model_architectures
|
| 1995 |
+
or "Gemma4ForConditionalGeneration" in model_architectures
|
| 1996 |
+
or "Gemma4UnifiedForConditionalGeneration" in model_architectures
|
| 1997 |
+
or "LagunaForCausalLM" in model_architectures
|
| 1998 |
+
or "MellumForCausalLM" in model_architectures
|
| 1999 |
+
):
|
| 2000 |
+
layer_types = getattr(hf_text_config, "layer_types", [])
|
| 2001 |
+
swa_attention_layer_ids = [
|
| 2002 |
+
i for i, x in enumerate(layer_types) if x == "sliding_attention"
|
| 2003 |
+
]
|
| 2004 |
+
full_attention_layer_ids = [
|
| 2005 |
+
i for i, x in enumerate(layer_types) if x == "full_attention"
|
| 2006 |
+
]
|
| 2007 |
+
elif "InklingForConditionalGenerationMTP" in model_architectures:
|
| 2008 |
+
# One block per MTP depth; a banded head marks its sliding-window depths
|
| 2009 |
+
# in mtp_local_layer_ids. The per-depth pool routing in the KV-cache
|
| 2010 |
+
# mixin is authoritative; this keeps model_config's swa/full lists
|
| 2011 |
+
# self-consistent for other consumers.
|
| 2012 |
+
mtp_local_layer_ids = hf_text_config.mtp_local_layer_ids
|
| 2013 |
+
if mtp_local_layer_ids:
|
| 2014 |
+
num_depths = hf_text_config.num_nextn_predict_layers
|
| 2015 |
+
local_set = set(mtp_local_layer_ids)
|
| 2016 |
+
swa_attention_layer_ids = sorted(local_set)
|
| 2017 |
+
full_attention_layer_ids = [
|
| 2018 |
+
i for i in range(num_depths) if i not in local_set
|
| 2019 |
+
]
|
| 2020 |
+
else:
|
| 2021 |
+
swa_attention_layer_ids = []
|
| 2022 |
+
full_attention_layer_ids = [0]
|
| 2023 |
+
elif "InklingForConditionalGeneration" in model_architectures:
|
| 2024 |
+
local_layer_ids = hf_text_config.local_layer_ids
|
| 2025 |
+
local_layer_id_set = set(local_layer_ids)
|
| 2026 |
+
assert len(local_layer_id_set) == len(
|
| 2027 |
+
local_layer_ids
|
| 2028 |
+
), f"Inkling local_layer_ids must be unique: {local_layer_ids}"
|
| 2029 |
+
assert all(
|
| 2030 |
+
0 <= layer_id < num_hidden_layers for layer_id in local_layer_id_set
|
| 2031 |
+
), f"Inkling local_layer_ids must be in [0, {num_hidden_layers}): {local_layer_ids}"
|
| 2032 |
+
swa_attention_layer_ids = [
|
| 2033 |
+
i for i in range(num_hidden_layers) if i in local_layer_id_set
|
| 2034 |
+
]
|
| 2035 |
+
full_attention_layer_ids = [
|
| 2036 |
+
i for i in range(num_hidden_layers) if i not in local_layer_id_set
|
| 2037 |
+
]
|
| 2038 |
+
elif "UnlimitedOCRForCausalLM" in model_architectures:
|
| 2039 |
+
swa_attention_layer_ids = list(range(num_hidden_layers))
|
| 2040 |
+
full_attention_layer_ids = []
|
| 2041 |
+
elif getattr(hf_text_config, "hybrid_layer_pattern", None) is not None:
|
| 2042 |
+
# Generic fallback for custom hybrid SWA models that opt in via
|
| 2043 |
+
# hf_text_config.is_hybrid_swa and expose a hybrid_layer_pattern
|
| 2044 |
+
# (1 = SWA, 0 = full) without needing to be added to the allowlist.
|
| 2045 |
+
hybrid_layer_pattern = hf_text_config.hybrid_layer_pattern
|
| 2046 |
+
swa_attention_layer_ids = [
|
| 2047 |
+
i for i in range(num_hidden_layers) if hybrid_layer_pattern[i] == 1
|
| 2048 |
+
]
|
| 2049 |
+
full_attention_layer_ids = [
|
| 2050 |
+
i for i in range(num_hidden_layers) if hybrid_layer_pattern[i] == 0
|
| 2051 |
+
]
|
| 2052 |
+
else:
|
| 2053 |
+
swa_attention_layer_ids = None
|
| 2054 |
+
full_attention_layer_ids = None
|
| 2055 |
+
return swa_attention_layer_ids, full_attention_layer_ids
|
sglang_patch/srt/entrypoints/openai/chat_encoding.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single home for the chat-encoding dispatch.
|
| 2 |
+
|
| 3 |
+
Which encoder turns chat messages into prompt tokens is a property of the
|
| 4 |
+
model, so the serving path and offline tools (benchmarks, evals) must resolve
|
| 5 |
+
it here instead of re-deriving it from model architectures themselves.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Any, Dict, List, Optional
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def resolve_chat_encoding_spec(
|
| 14 |
+
*,
|
| 15 |
+
hf_config: Any,
|
| 16 |
+
tokenizer: Any,
|
| 17 |
+
tool_call_parser: Optional[str] = None,
|
| 18 |
+
) -> Optional[str]:
|
| 19 |
+
"""Return the chat encoding spec for a model: "dsv4", "dsv32", "inkling", or None.
|
| 20 |
+
|
| 21 |
+
None means the default path (HF chat template).
|
| 22 |
+
"""
|
| 23 |
+
if tool_call_parser == "deepseekv4":
|
| 24 |
+
return "dsv4"
|
| 25 |
+
if tool_call_parser == "deepseekv32":
|
| 26 |
+
return "dsv32"
|
| 27 |
+
|
| 28 |
+
architectures = hf_config.architectures
|
| 29 |
+
arch = architectures[0] if architectures else ""
|
| 30 |
+
|
| 31 |
+
if "Agnes" in arch:
|
| 32 |
+
return "dsv4"
|
| 33 |
+
|
| 34 |
+
# Inkling has no Jinja chat_template and uses a tiktoken base + a special-token
|
| 35 |
+
# overlay + negative MM placeholders, so it can't go through apply_chat_template;
|
| 36 |
+
# render input_ids directly via the Inkling renderer (serving_chat._encode_messages).
|
| 37 |
+
if "InklingForConditionalGeneration" in arch:
|
| 38 |
+
return "inkling"
|
| 39 |
+
|
| 40 |
+
has_chat_template = tokenizer is not None and tokenizer.chat_template is not None
|
| 41 |
+
if "DeepseekV3" in arch and not has_chat_template:
|
| 42 |
+
return "dsv32"
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def encode_simple_chat(
|
| 47 |
+
*,
|
| 48 |
+
tokenizer: Any,
|
| 49 |
+
spec: Optional[str],
|
| 50 |
+
messages: List[Dict[str, Any]],
|
| 51 |
+
thinking_mode: str = "chat",
|
| 52 |
+
) -> List[int]:
|
| 53 |
+
"""Encode a plain-text chat conversation into prompt token ids.
|
| 54 |
+
|
| 55 |
+
Minimal encode for offline tools: no tools, no multimodal content, no
|
| 56 |
+
continue_final_message; the serving path keeps its full request-level
|
| 57 |
+
pipeline in ``serving_chat``. Like
|
| 58 |
+
``serving_chat``, an empty system message is prepended when the
|
| 59 |
+
conversation does not start with one (for the dsv4/dsv32 encoders this
|
| 60 |
+
currently renders to zero tokens, but keeping the insertion explicit ties
|
| 61 |
+
this helper to the serving semantics rather than to that coincidence).
|
| 62 |
+
"""
|
| 63 |
+
if spec == "inkling":
|
| 64 |
+
from sglang.srt.parser.inkling_renderer import render_inkling_messages
|
| 65 |
+
from sglang.srt.parser.inkling_tokenizer import InklingTokenizer
|
| 66 |
+
|
| 67 |
+
return render_inkling_messages(
|
| 68 |
+
messages,
|
| 69 |
+
InklingTokenizer(tokenizer=tokenizer),
|
| 70 |
+
add_generation_prompt=False,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
if spec in ("dsv4", "dsv32"):
|
| 74 |
+
if messages and messages[0]["role"] != "system":
|
| 75 |
+
messages = [{"role": "system", "content": ""}] + list(messages)
|
| 76 |
+
if spec == "dsv4":
|
| 77 |
+
from sglang.srt.entrypoints.openai import encoding_dsv4
|
| 78 |
+
|
| 79 |
+
real_input = encoding_dsv4.encode_messages(
|
| 80 |
+
messages, thinking_mode=thinking_mode
|
| 81 |
+
)
|
| 82 |
+
else:
|
| 83 |
+
from sglang.srt.entrypoints.openai import encoding_dsv32
|
| 84 |
+
|
| 85 |
+
real_input = encoding_dsv32.encode_messages(
|
| 86 |
+
messages, thinking_mode=thinking_mode
|
| 87 |
+
)
|
| 88 |
+
return tokenizer.encode(real_input)
|
| 89 |
+
|
| 90 |
+
if getattr(tokenizer, "chat_template", None) is None:
|
| 91 |
+
raise ValueError(
|
| 92 |
+
"This model has no HF chat template and no custom chat encoder; "
|
| 93 |
+
f"cannot encode chat messages with {getattr(tokenizer, 'name_or_path', tokenizer)!r}."
|
| 94 |
+
)
|
| 95 |
+
return tokenizer.apply_chat_template(
|
| 96 |
+
messages, add_generation_prompt=True, tokenize=True
|
| 97 |
+
)
|
sglang_patch/srt/entrypoints/openai/serving_chat.py
ADDED
|
@@ -0,0 +1,2264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import copy
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import math
|
| 8 |
+
import time
|
| 9 |
+
import uuid
|
| 10 |
+
from enum import Enum
|
| 11 |
+
from http import HTTPStatus
|
| 12 |
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class ThinkingMode(str, Enum):
|
| 16 |
+
"""Mode for message encoding - chat vs thinking/reasoning."""
|
| 17 |
+
|
| 18 |
+
CHAT = "chat"
|
| 19 |
+
THINKING = "thinking"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
import jinja2
|
| 23 |
+
import orjson
|
| 24 |
+
from fastapi import Request
|
| 25 |
+
from fastapi.responses import ORJSONResponse, StreamingResponse
|
| 26 |
+
from jsonschema import Draft202012Validator, SchemaError
|
| 27 |
+
|
| 28 |
+
from sglang.srt.entrypoints.openai import encoding_dsv4, encoding_dsv32
|
| 29 |
+
from sglang.srt.entrypoints.openai.protocol import (
|
| 30 |
+
ChatCompletionRequest,
|
| 31 |
+
ChatCompletionResponse,
|
| 32 |
+
ChatCompletionResponseChoice,
|
| 33 |
+
ChatCompletionResponseStreamChoice,
|
| 34 |
+
ChatCompletionStreamResponse,
|
| 35 |
+
ChatCompletionTokenLogprob,
|
| 36 |
+
ChatMessage,
|
| 37 |
+
ChoiceLogprobs,
|
| 38 |
+
DeltaMessage,
|
| 39 |
+
ErrorResponse,
|
| 40 |
+
FunctionResponse,
|
| 41 |
+
LogProbs,
|
| 42 |
+
MessageProcessingResult,
|
| 43 |
+
PromptTokensDetails,
|
| 44 |
+
ResponseParserProtocol,
|
| 45 |
+
SglExt,
|
| 46 |
+
ToolCall,
|
| 47 |
+
ToolCallProcessingResult,
|
| 48 |
+
ToolChoice,
|
| 49 |
+
TopLogprob,
|
| 50 |
+
)
|
| 51 |
+
from sglang.srt.entrypoints.openai.serving_base import OpenAIServingBase
|
| 52 |
+
from sglang.srt.entrypoints.openai.sse_utils import build_sse_content
|
| 53 |
+
from sglang.srt.entrypoints.openai.usage_processor import UsageProcessor
|
| 54 |
+
from sglang.srt.entrypoints.openai.utils import (
|
| 55 |
+
cached_tokens_details_from_dict,
|
| 56 |
+
process_cached_tokens_details_from_ret,
|
| 57 |
+
process_hidden_states_from_ret,
|
| 58 |
+
process_routed_experts_from_ret,
|
| 59 |
+
should_include_usage,
|
| 60 |
+
to_openai_style_logprobs,
|
| 61 |
+
)
|
| 62 |
+
from sglang.srt.environ import envs
|
| 63 |
+
from sglang.srt.function_call.core_types import ToolCallItem
|
| 64 |
+
from sglang.srt.function_call.function_call_parser import FunctionCallParser
|
| 65 |
+
from sglang.srt.function_call.json_array_parser import JsonArrayParser
|
| 66 |
+
from sglang.srt.function_call.utils import (
|
| 67 |
+
get_json_schema_constraint,
|
| 68 |
+
normalize_json_schema_types,
|
| 69 |
+
)
|
| 70 |
+
from sglang.srt.managers.io_struct import GenerateReqInput
|
| 71 |
+
from sglang.srt.parser.conversation import generate_chat_conv
|
| 72 |
+
from sglang.srt.parser.jinja_template_utils import process_content_for_template_format
|
| 73 |
+
from sglang.srt.parser.reasoning_parser import ReasoningParser
|
| 74 |
+
|
| 75 |
+
if TYPE_CHECKING:
|
| 76 |
+
from sglang.srt.managers.tokenizer_manager import TokenizerManager
|
| 77 |
+
from sglang.srt.parser.template_manager import TemplateManager
|
| 78 |
+
|
| 79 |
+
logger = logging.getLogger(__name__)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def normalize_tool_content(role: str, content):
|
| 83 |
+
"""Normalize tool message content from OpenAI array format to plain string.
|
| 84 |
+
|
| 85 |
+
OpenAI clients may send tool content as a list of content parts
|
| 86 |
+
(e.g. [{"type":"text","text":"..."}]) but most chat templates expect
|
| 87 |
+
a plain string for tool messages. Only flatten when ALL items are
|
| 88 |
+
pure OpenAI text parts; preserve lists containing non-text-type items
|
| 89 |
+
that some templates intentionally iterate over.
|
| 90 |
+
"""
|
| 91 |
+
if role != "tool" or not isinstance(content, list):
|
| 92 |
+
return content
|
| 93 |
+
parts = content
|
| 94 |
+
is_openai_text_parts = all(
|
| 95 |
+
(isinstance(p, dict) and p.get("type") == "text") or isinstance(p, str)
|
| 96 |
+
for p in parts
|
| 97 |
+
)
|
| 98 |
+
if is_openai_text_parts:
|
| 99 |
+
text_parts = [p.get("text", "") if isinstance(p, dict) else p for p in parts]
|
| 100 |
+
return " ".join(text_parts)
|
| 101 |
+
return content
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def parse_tool_call_arguments(arguments: str) -> Dict[str, Any]:
|
| 105 |
+
"""Parse OpenAI tool call arguments for chat templates."""
|
| 106 |
+
try:
|
| 107 |
+
parsed_arguments = orjson.loads(arguments)
|
| 108 |
+
except orjson.JSONDecodeError as exc:
|
| 109 |
+
raise ValueError(
|
| 110 |
+
"Assistant tool call function.arguments must be valid JSON."
|
| 111 |
+
) from exc
|
| 112 |
+
|
| 113 |
+
if not isinstance(parsed_arguments, dict):
|
| 114 |
+
raise ValueError(
|
| 115 |
+
"Assistant tool call function.arguments must be a JSON object."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
return parsed_arguments
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def normalize_assistant_tool_call_arguments(message: Dict[str, Any]) -> None:
|
| 122 |
+
"""Normalize assistant history tool call arguments in-place."""
|
| 123 |
+
if message.get("role") != "assistant" or not isinstance(
|
| 124 |
+
message.get("tool_calls"), list
|
| 125 |
+
):
|
| 126 |
+
return
|
| 127 |
+
|
| 128 |
+
for item in message["tool_calls"]:
|
| 129 |
+
function = item.get("function") if isinstance(item, dict) else None
|
| 130 |
+
if not isinstance(function, dict):
|
| 131 |
+
continue
|
| 132 |
+
if "arguments" in function and isinstance(function["arguments"], str):
|
| 133 |
+
function["arguments"] = parse_tool_call_arguments(function["arguments"])
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _extract_max_dynamic_patch(request: ChatCompletionRequest):
|
| 137 |
+
img_vals = []
|
| 138 |
+
vid_vals = []
|
| 139 |
+
for msg in request.messages or []:
|
| 140 |
+
content = getattr(msg, "content", None)
|
| 141 |
+
if not isinstance(content, list):
|
| 142 |
+
continue
|
| 143 |
+
for part in content:
|
| 144 |
+
# pydantic object or dict type
|
| 145 |
+
if getattr(part, "type", None) == "image_url":
|
| 146 |
+
iu = getattr(part, "image_url", None)
|
| 147 |
+
mdp = getattr(iu, "max_dynamic_patch", None) if iu else None
|
| 148 |
+
if mdp is not None:
|
| 149 |
+
img_vals.append(int(mdp))
|
| 150 |
+
elif getattr(part, "type", None) == "video_url":
|
| 151 |
+
vu = getattr(part, "video_url", None)
|
| 152 |
+
mdp = getattr(vu, "max_dynamic_patch", None) if vu else None
|
| 153 |
+
if mdp is not None:
|
| 154 |
+
vid_vals.append(int(mdp))
|
| 155 |
+
|
| 156 |
+
# TODO(yuan-luo): per-item max_dynamic_patch for both image and video
|
| 157 |
+
img_max_dynamic_patch = min(img_vals) if img_vals else None
|
| 158 |
+
vid_max_dynamic_patch = min(vid_vals) if vid_vals else None
|
| 159 |
+
return img_max_dynamic_patch, vid_max_dynamic_patch
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class OpenAIServingChat(OpenAIServingBase):
|
| 163 |
+
"""Handler for /v1/chat/completions requests"""
|
| 164 |
+
|
| 165 |
+
_default_sampling_params_logged = False
|
| 166 |
+
|
| 167 |
+
def __init__(
|
| 168 |
+
self,
|
| 169 |
+
tokenizer_manager: TokenizerManager,
|
| 170 |
+
template_manager: TemplateManager,
|
| 171 |
+
):
|
| 172 |
+
super().__init__(tokenizer_manager)
|
| 173 |
+
self.template_manager = template_manager
|
| 174 |
+
self.tool_call_parser = self.tokenizer_manager.server_args.tool_call_parser
|
| 175 |
+
self.reasoning_parser = self.tokenizer_manager.server_args.reasoning_parser
|
| 176 |
+
self.default_chat_template_kwargs = (
|
| 177 |
+
self.tokenizer_manager.server_args.default_chat_template_kwargs or {}
|
| 178 |
+
)
|
| 179 |
+
self._reasoning_detector = None
|
| 180 |
+
if self.reasoning_parser:
|
| 181 |
+
try:
|
| 182 |
+
rp = ReasoningParser(
|
| 183 |
+
model_type=self.reasoning_parser,
|
| 184 |
+
stream_reasoning=True,
|
| 185 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 186 |
+
)
|
| 187 |
+
self._reasoning_detector = rp.detector
|
| 188 |
+
except ValueError as e:
|
| 189 |
+
logger.warning(
|
| 190 |
+
"Failed to initialize reasoning detector for parser '%s': %s",
|
| 191 |
+
self.reasoning_parser,
|
| 192 |
+
e,
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
# Get default sampling parameters from model's generation config
|
| 196 |
+
self.default_sampling_params = (
|
| 197 |
+
self.tokenizer_manager.model_config.get_default_sampling_params()
|
| 198 |
+
)
|
| 199 |
+
if (
|
| 200 |
+
self.default_sampling_params
|
| 201 |
+
and not OpenAIServingChat._default_sampling_params_logged
|
| 202 |
+
):
|
| 203 |
+
logger.info(
|
| 204 |
+
f"Using default chat sampling params from model generation config: {self.default_sampling_params}",
|
| 205 |
+
)
|
| 206 |
+
OpenAIServingChat._default_sampling_params_logged = True
|
| 207 |
+
|
| 208 |
+
# Check if the model is a GPT-OSS model
|
| 209 |
+
self.is_gpt_oss = (
|
| 210 |
+
hasattr(self.tokenizer_manager.model_config, "hf_config")
|
| 211 |
+
and hasattr(self.tokenizer_manager.model_config.hf_config, "model_type")
|
| 212 |
+
and self.tokenizer_manager.model_config.hf_config.model_type == "gpt_oss"
|
| 213 |
+
)
|
| 214 |
+
self.is_gemma4 = (
|
| 215 |
+
hasattr(self.tokenizer_manager.model_config, "hf_config")
|
| 216 |
+
and hasattr(self.tokenizer_manager.model_config.hf_config, "model_type")
|
| 217 |
+
and self.tokenizer_manager.model_config.hf_config.model_type
|
| 218 |
+
in ("gemma4", "gemma4_unified")
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# Which Python-based chat encoder (if any) bypasses apply_chat_template.
|
| 222 |
+
# Values: "dsv32", "dsv4", or custom values set by subclass. None for default.
|
| 223 |
+
self.chat_encoding_spec = self._resolve_chat_encoding_spec()
|
| 224 |
+
|
| 225 |
+
# Resolve the env-configured Inkling effort default once: the env var is
|
| 226 |
+
# frozen for the server's lifetime, and a misconfigured value should
|
| 227 |
+
# fail at boot, not 400 every request.
|
| 228 |
+
self._inkling_default_reasoning_effort: Optional[float] = (
|
| 229 |
+
self._get_inkling_default_reasoning_effort()
|
| 230 |
+
if self.chat_encoding_spec == "inkling"
|
| 231 |
+
else None
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# Per-request response parser for custom decoding (set by _encode_messages)
|
| 235 |
+
self._response_parser: Optional[ResponseParserProtocol] = None
|
| 236 |
+
|
| 237 |
+
# Probe whether ``encode("")`` returns specials. If it does, we must
|
| 238 |
+
# keep ``add_special_tokens=False`` at the chat-template encode site
|
| 239 |
+
# to avoid double BOS; otherwise the kwarg is a no-op and dropping it
|
| 240 |
+
# lets slow tokenizers (e.g. Kimi's TikTokenTokenizer) stay on the
|
| 241 |
+
# fast internal path.
|
| 242 |
+
try:
|
| 243 |
+
self._tokenizer_auto_adds_specials = (
|
| 244 |
+
len(self.tokenizer_manager.tokenizer.encode("")) > 0
|
| 245 |
+
)
|
| 246 |
+
except Exception:
|
| 247 |
+
self._tokenizer_auto_adds_specials = True
|
| 248 |
+
|
| 249 |
+
def _handle_last_assistant_message(
|
| 250 |
+
self,
|
| 251 |
+
messages: List[Dict[str, Any]],
|
| 252 |
+
request: ChatCompletionRequest,
|
| 253 |
+
) -> tuple[List[Dict[str, Any]], Optional[str]]:
|
| 254 |
+
"""
|
| 255 |
+
Handle continue_final_message feature: separate final assistant message.
|
| 256 |
+
|
| 257 |
+
If continue_final_message is enabled and the last message is from assistant,
|
| 258 |
+
extract its content and remove it from the message list.
|
| 259 |
+
If continue_final_message is False and the last message is from assistant,
|
| 260 |
+
convert it to a user message to ensure the last message is always from user.
|
| 261 |
+
|
| 262 |
+
Only processes text-based content (strings), ignoring multimodal content (lists).
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
messages: List of message dictionaries
|
| 266 |
+
request: ChatCompletionRequest with continue_final_message flag
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
Tuple of (processed_messages, assistant_prefix)
|
| 270 |
+
- processed_messages: Messages with last assistant message handled appropriately
|
| 271 |
+
- assistant_prefix: Content of the last assistant message (string only), or None
|
| 272 |
+
"""
|
| 273 |
+
assistant_prefix = None
|
| 274 |
+
if messages and messages[-1].get("role") == "assistant":
|
| 275 |
+
last_content = messages[-1].get("content")
|
| 276 |
+
# Only process string content, ignore multimodal content (lists)
|
| 277 |
+
if isinstance(last_content, str):
|
| 278 |
+
if request.continue_final_message:
|
| 279 |
+
# Extract content and remove the assistant message
|
| 280 |
+
assistant_prefix = last_content
|
| 281 |
+
messages = messages[:-1]
|
| 282 |
+
else:
|
| 283 |
+
# Convert the last assistant message to user message
|
| 284 |
+
messages[-1] = {"role": "user", "content": last_content}
|
| 285 |
+
return messages, assistant_prefix
|
| 286 |
+
|
| 287 |
+
def _append_assistant_prefix_to_prompt_ids(
|
| 288 |
+
self, prompt_ids: List[int], assistant_prefix: str
|
| 289 |
+
) -> List[int]:
|
| 290 |
+
"""
|
| 291 |
+
Append assistant prefix to prompt_ids.
|
| 292 |
+
|
| 293 |
+
Args:
|
| 294 |
+
prompt_ids: Current prompt token IDs
|
| 295 |
+
assistant_prefix: Assistant message content to append
|
| 296 |
+
|
| 297 |
+
Returns:
|
| 298 |
+
Updated prompt_ids with assistant prefix appended
|
| 299 |
+
"""
|
| 300 |
+
encoded = self.tokenizer_manager.tokenizer.encode(assistant_prefix)
|
| 301 |
+
if encoded and encoded[0] == self.tokenizer_manager.tokenizer.bos_token_id:
|
| 302 |
+
encoded = encoded[1:]
|
| 303 |
+
return prompt_ids + encoded
|
| 304 |
+
|
| 305 |
+
def _resolve_chat_encoding_spec(self) -> Optional[str]:
|
| 306 |
+
"""Determine which chat encoding spec to use.
|
| 307 |
+
|
| 308 |
+
Override in subclass to add custom encoding specs.
|
| 309 |
+
"""
|
| 310 |
+
from sglang.srt.entrypoints.openai.chat_encoding import (
|
| 311 |
+
resolve_chat_encoding_spec,
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
return resolve_chat_encoding_spec(
|
| 315 |
+
hf_config=self.tokenizer_manager.model_config.hf_config,
|
| 316 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 317 |
+
tool_call_parser=self.tool_call_parser,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
def _request_id_prefix(self) -> str:
|
| 321 |
+
return "chatcmpl-"
|
| 322 |
+
|
| 323 |
+
def _encode_messages(
|
| 324 |
+
self,
|
| 325 |
+
messages: List[Dict[str, Any]],
|
| 326 |
+
request: ChatCompletionRequest,
|
| 327 |
+
thinking_mode: ThinkingMode,
|
| 328 |
+
tools: Optional[List[Dict]] = None,
|
| 329 |
+
) -> Optional[List[int]]:
|
| 330 |
+
"""Encode messages for custom chat_encoding_spec values.
|
| 331 |
+
|
| 332 |
+
Returns prompt_ids if handled, None to use default encoding.
|
| 333 |
+
"""
|
| 334 |
+
if self.chat_encoding_spec == "inkling":
|
| 335 |
+
# Inkling: render messages -> input_ids with framing tokens + ONE placeholder per
|
| 336 |
+
# media (encoding/expansion happens later in InklingMultimodalProcessor). The
|
| 337 |
+
# server's tokenizer is the base tiktoken backend; wrap it so encode_special
|
| 338 |
+
# supplies the framing-token overlay.
|
| 339 |
+
from sglang.srt.parser.inkling_renderer import render_inkling_messages
|
| 340 |
+
from sglang.srt.parser.inkling_tokenizer import (
|
| 341 |
+
CONTENT_TEXT,
|
| 342 |
+
MESSAGE_MODEL,
|
| 343 |
+
InklingTokenizer,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
inkling_tokenizer = InklingTokenizer(
|
| 347 |
+
tokenizer=self.tokenizer_manager.tokenizer
|
| 348 |
+
)
|
| 349 |
+
reasoning_effort = self._parse_inkling_reasoning_effort(
|
| 350 |
+
request.reasoning_effort
|
| 351 |
+
)
|
| 352 |
+
if reasoning_effort is None:
|
| 353 |
+
reasoning_effort = self._inkling_default_reasoning_effort
|
| 354 |
+
assistant_prefix = self._pop_inkling_assistant_prefix(messages, request)
|
| 355 |
+
prompt_ids = render_inkling_messages(
|
| 356 |
+
messages,
|
| 357 |
+
inkling_tokenizer,
|
| 358 |
+
add_generation_prompt=False,
|
| 359 |
+
tools=tools,
|
| 360 |
+
reasoning_effort=reasoning_effort,
|
| 361 |
+
)
|
| 362 |
+
if assistant_prefix is not None:
|
| 363 |
+
# Continue the final assistant message inside an OPEN model text
|
| 364 |
+
# block: header + payload, no <|end_message|> and no
|
| 365 |
+
# <|content_model_end_sampling|>, so the model resumes the turn.
|
| 366 |
+
prompt_ids += [
|
| 367 |
+
inkling_tokenizer.encode_special(MESSAGE_MODEL),
|
| 368 |
+
inkling_tokenizer.encode_special(CONTENT_TEXT),
|
| 369 |
+
*inkling_tokenizer.encode_text(assistant_prefix),
|
| 370 |
+
]
|
| 371 |
+
return prompt_ids
|
| 372 |
+
return None
|
| 373 |
+
|
| 374 |
+
@staticmethod
|
| 375 |
+
def _pop_inkling_assistant_prefix(
|
| 376 |
+
messages: List[Dict[str, Any]],
|
| 377 |
+
request: ChatCompletionRequest,
|
| 378 |
+
) -> Optional[str]:
|
| 379 |
+
"""Extract the trailing assistant text for ``continue_final_message``.
|
| 380 |
+
|
| 381 |
+
Only a plain-string assistant message with no tool calls and no
|
| 382 |
+
reasoning content can be continued; anything else renders as a closed
|
| 383 |
+
historical turn. Mutates ``messages`` in place (callers pass a copy).
|
| 384 |
+
"""
|
| 385 |
+
if not request.continue_final_message or not messages:
|
| 386 |
+
return None
|
| 387 |
+
last = messages[-1]
|
| 388 |
+
if (
|
| 389 |
+
last.get("role") != "assistant"
|
| 390 |
+
or not isinstance(last.get("content"), str)
|
| 391 |
+
or last.get("tool_calls")
|
| 392 |
+
or last.get("reasoning_content")
|
| 393 |
+
):
|
| 394 |
+
return None
|
| 395 |
+
messages.pop()
|
| 396 |
+
return last["content"]
|
| 397 |
+
|
| 398 |
+
@staticmethod
|
| 399 |
+
def _parse_inkling_reasoning_effort(
|
| 400 |
+
value: Optional[Union[str, float]],
|
| 401 |
+
) -> Optional[float]:
|
| 402 |
+
"""Convert an OpenAI-style reasoning_effort to an Inkling float."""
|
| 403 |
+
if value is None:
|
| 404 |
+
return None
|
| 405 |
+
if isinstance(value, bool):
|
| 406 |
+
raise ValueError("Inkling reasoning_effort must not be a boolean")
|
| 407 |
+
if isinstance(value, (int, float)):
|
| 408 |
+
parsed = float(value)
|
| 409 |
+
if not math.isfinite(parsed) or not 0.0 <= parsed <= 0.99:
|
| 410 |
+
raise ValueError("Inkling reasoning_effort must be in [0.0, 0.99]")
|
| 411 |
+
return parsed
|
| 412 |
+
_EFFORT_MAP = {
|
| 413 |
+
"none": 0.0,
|
| 414 |
+
"minimal": 0.1,
|
| 415 |
+
"low": 0.2,
|
| 416 |
+
"medium": 0.7,
|
| 417 |
+
"high": 0.9,
|
| 418 |
+
"xhigh": 0.99,
|
| 419 |
+
"max": 0.99,
|
| 420 |
+
}
|
| 421 |
+
if value in _EFFORT_MAP:
|
| 422 |
+
return _EFFORT_MAP[value]
|
| 423 |
+
try:
|
| 424 |
+
parsed = float(value)
|
| 425 |
+
except (ValueError, TypeError) as exc:
|
| 426 |
+
raise ValueError(f"invalid Inkling reasoning_effort: {value!r}") from exc
|
| 427 |
+
if not math.isfinite(parsed) or not 0.0 <= parsed <= 0.99:
|
| 428 |
+
raise ValueError("Inkling reasoning_effort must be in [0.0, 0.99]")
|
| 429 |
+
return parsed
|
| 430 |
+
|
| 431 |
+
@staticmethod
|
| 432 |
+
def _get_inkling_default_reasoning_effort() -> float:
|
| 433 |
+
"""Read the default Inkling reasoning effort from the environment."""
|
| 434 |
+
from sglang.srt.environ import envs
|
| 435 |
+
|
| 436 |
+
val = envs.SGLANG_INKLING_DEFAULT_REASONING_EFFORT.get()
|
| 437 |
+
if not val:
|
| 438 |
+
return 0.9
|
| 439 |
+
try:
|
| 440 |
+
parsed = float(val)
|
| 441 |
+
except (ValueError, TypeError) as exc:
|
| 442 |
+
raise ValueError(
|
| 443 |
+
"SGLANG_INKLING_DEFAULT_REASONING_EFFORT must be numeric"
|
| 444 |
+
) from exc
|
| 445 |
+
if not math.isfinite(parsed) or not 0.0 <= parsed <= 0.99:
|
| 446 |
+
raise ValueError(
|
| 447 |
+
"SGLANG_INKLING_DEFAULT_REASONING_EFFORT must be in [0.0, 0.99]"
|
| 448 |
+
)
|
| 449 |
+
return parsed
|
| 450 |
+
|
| 451 |
+
def _decode_response(self, ret_item: Dict[str, Any]) -> Union[str, ErrorResponse]:
|
| 452 |
+
"""Extract text from response."""
|
| 453 |
+
return ret_item["text"]
|
| 454 |
+
|
| 455 |
+
def _get_parsed_response_fields(
|
| 456 |
+
self,
|
| 457 |
+
reasoning_text: Optional[str],
|
| 458 |
+
tool_calls: Optional[List[Dict]],
|
| 459 |
+
) -> tuple[Optional[str], Optional[List[Dict]]]:
|
| 460 |
+
"""Post-process reasoning and tool_calls before building response."""
|
| 461 |
+
return reasoning_text, tool_calls
|
| 462 |
+
|
| 463 |
+
def _continuous_usage_cached_details(
|
| 464 |
+
self, content: Dict[str, Any]
|
| 465 |
+
) -> Optional[PromptTokensDetails]:
|
| 466 |
+
if not self.tokenizer_manager.server_args.enable_cache_report:
|
| 467 |
+
return None
|
| 468 |
+
return UsageProcessor._details_if_cached(
|
| 469 |
+
content["meta_info"].get("cached_tokens", 0)
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
async def _generate_stream_content(
|
| 473 |
+
self,
|
| 474 |
+
content: Dict[str, Any],
|
| 475 |
+
index: int,
|
| 476 |
+
request: ChatCompletionRequest,
|
| 477 |
+
stream_offsets: Dict[int, int],
|
| 478 |
+
reasoning_parser_dict: Dict,
|
| 479 |
+
parser_dict: Dict,
|
| 480 |
+
has_tool_calls: Dict[int, bool],
|
| 481 |
+
choice_logprobs: Optional[Dict],
|
| 482 |
+
finish_reason_type: Optional[str],
|
| 483 |
+
continuous_usage_stats: bool,
|
| 484 |
+
prompt_tokens: Dict[int, int],
|
| 485 |
+
reasoning_tokens: Dict[int, int],
|
| 486 |
+
completion_tokens: Dict[int, int],
|
| 487 |
+
) -> AsyncGenerator[str, None]:
|
| 488 |
+
"""Generate SSE chunks for streaming content."""
|
| 489 |
+
offset = stream_offsets.get(index, 0)
|
| 490 |
+
if self.tokenizer_manager.server_args.incremental_streaming_output:
|
| 491 |
+
delta = content["text"]
|
| 492 |
+
else:
|
| 493 |
+
delta = content["text"][offset:]
|
| 494 |
+
stream_offsets[index] = len(content["text"])
|
| 495 |
+
|
| 496 |
+
# Attach logprobs to the first chunk emitted this step (reasoning,
|
| 497 |
+
# tool-call, or content) so they aren't dropped when a parser is active
|
| 498 |
+
# nor duplicated across chunks; flush any leftover at the end.
|
| 499 |
+
remaining_logprobs = choice_logprobs
|
| 500 |
+
|
| 501 |
+
# Handle reasoning content
|
| 502 |
+
if self.reasoning_parser and request.separate_reasoning:
|
| 503 |
+
reasoning_text, delta = self._process_reasoning_stream(
|
| 504 |
+
index,
|
| 505 |
+
delta,
|
| 506 |
+
reasoning_parser_dict,
|
| 507 |
+
content,
|
| 508 |
+
request,
|
| 509 |
+
finish_reason_type,
|
| 510 |
+
)
|
| 511 |
+
if reasoning_text:
|
| 512 |
+
usage = None
|
| 513 |
+
if continuous_usage_stats:
|
| 514 |
+
usage = UsageProcessor.calculate_token_usage(
|
| 515 |
+
prompt_tokens=prompt_tokens.get(index, 0),
|
| 516 |
+
reasoning_tokens=reasoning_tokens.get(index, 0),
|
| 517 |
+
completion_tokens=completion_tokens.get(index, 0),
|
| 518 |
+
cached_tokens=self._continuous_usage_cached_details(content),
|
| 519 |
+
).model_dump()
|
| 520 |
+
|
| 521 |
+
yield build_sse_content(
|
| 522 |
+
chunk_id=content["meta_info"]["id"],
|
| 523 |
+
created=int(time.time()),
|
| 524 |
+
model=request.model,
|
| 525 |
+
index=index,
|
| 526 |
+
reasoning_content=reasoning_text,
|
| 527 |
+
logprobs=remaining_logprobs,
|
| 528 |
+
usage=usage,
|
| 529 |
+
)
|
| 530 |
+
remaining_logprobs = None
|
| 531 |
+
|
| 532 |
+
# Handle tool calls
|
| 533 |
+
if request.tool_choice != "none" and request.tools and self.tool_call_parser:
|
| 534 |
+
async for chunk in self._process_tool_call_stream(
|
| 535 |
+
index,
|
| 536 |
+
delta,
|
| 537 |
+
parser_dict,
|
| 538 |
+
content,
|
| 539 |
+
request,
|
| 540 |
+
has_tool_calls,
|
| 541 |
+
continuous_usage_stats,
|
| 542 |
+
):
|
| 543 |
+
if chunk:
|
| 544 |
+
yield chunk
|
| 545 |
+
|
| 546 |
+
# Send any remaining tool call arguments when generation finishes
|
| 547 |
+
if finish_reason_type is not None and index in parser_dict:
|
| 548 |
+
parser = parser_dict[index]
|
| 549 |
+
remaining_chunk = self._check_for_unstreamed_tool_args(
|
| 550 |
+
parser, content, request, index
|
| 551 |
+
)
|
| 552 |
+
if remaining_chunk:
|
| 553 |
+
yield remaining_chunk
|
| 554 |
+
|
| 555 |
+
else:
|
| 556 |
+
# Regular content
|
| 557 |
+
if delta:
|
| 558 |
+
usage = None
|
| 559 |
+
if continuous_usage_stats:
|
| 560 |
+
usage = UsageProcessor.calculate_token_usage(
|
| 561 |
+
prompt_tokens=prompt_tokens.get(index, 0),
|
| 562 |
+
reasoning_tokens=reasoning_tokens.get(index, 0),
|
| 563 |
+
completion_tokens=completion_tokens.get(index, 0),
|
| 564 |
+
cached_tokens=self._continuous_usage_cached_details(content),
|
| 565 |
+
).model_dump()
|
| 566 |
+
|
| 567 |
+
yield build_sse_content(
|
| 568 |
+
chunk_id=content["meta_info"]["id"],
|
| 569 |
+
created=int(time.time()),
|
| 570 |
+
model=request.model,
|
| 571 |
+
index=index,
|
| 572 |
+
content=delta,
|
| 573 |
+
logprobs=remaining_logprobs,
|
| 574 |
+
usage=usage,
|
| 575 |
+
)
|
| 576 |
+
remaining_logprobs = None
|
| 577 |
+
|
| 578 |
+
# Flush logprobs still unattached this step — only when a parser is
|
| 579 |
+
# active, since _process_tool_call_stream may consume the delta and emit
|
| 580 |
+
# no content chunk. On the plain path an empty-delta step has no chunk
|
| 581 |
+
# to attach to either way, and a standalone empty-delta logprobs chunk
|
| 582 |
+
# is not a shape clients expect.
|
| 583 |
+
if remaining_logprobs is not None and (
|
| 584 |
+
self.reasoning_parser or self.tool_call_parser
|
| 585 |
+
):
|
| 586 |
+
usage = None
|
| 587 |
+
if continuous_usage_stats:
|
| 588 |
+
usage = UsageProcessor.calculate_token_usage(
|
| 589 |
+
prompt_tokens=prompt_tokens.get(index, 0),
|
| 590 |
+
reasoning_tokens=reasoning_tokens.get(index, 0),
|
| 591 |
+
completion_tokens=completion_tokens.get(index, 0),
|
| 592 |
+
cached_tokens=self._continuous_usage_cached_details(content),
|
| 593 |
+
).model_dump()
|
| 594 |
+
|
| 595 |
+
yield build_sse_content(
|
| 596 |
+
chunk_id=content["meta_info"]["id"],
|
| 597 |
+
created=int(time.time()),
|
| 598 |
+
model=request.model,
|
| 599 |
+
index=index,
|
| 600 |
+
logprobs=remaining_logprobs,
|
| 601 |
+
usage=usage,
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
def _validate_request(self, request: ChatCompletionRequest) -> Optional[str]:
|
| 605 |
+
"""Validate that the input is valid."""
|
| 606 |
+
if not request.messages:
|
| 607 |
+
return "Messages cannot be empty."
|
| 608 |
+
|
| 609 |
+
if (
|
| 610 |
+
isinstance(request.tool_choice, str)
|
| 611 |
+
and request.tool_choice.lower() == "required"
|
| 612 |
+
and not request.tools
|
| 613 |
+
):
|
| 614 |
+
return "Tools cannot be empty if tool choice is set to required."
|
| 615 |
+
|
| 616 |
+
if request.tool_choice is not None and not isinstance(request.tool_choice, str):
|
| 617 |
+
if not request.tools:
|
| 618 |
+
return "Tools cannot be empty if tool choice is set to a specific tool."
|
| 619 |
+
tool_name = request.tool_choice.function.name
|
| 620 |
+
tool_exists = any(tool.function.name == tool_name for tool in request.tools)
|
| 621 |
+
if not tool_exists:
|
| 622 |
+
return f"Tool '{tool_name}' not found in tools list."
|
| 623 |
+
|
| 624 |
+
# Validate tool definitions
|
| 625 |
+
for i, tool in enumerate(request.tools or []):
|
| 626 |
+
if tool.function.parameters is None:
|
| 627 |
+
continue
|
| 628 |
+
try:
|
| 629 |
+
# Rewrite DB/ORM-style aliases (e.g. "varchar", "enum", "int")
|
| 630 |
+
# to standard JSON Schema types before validation. RecursionError
|
| 631 |
+
# guards against hand-crafted cyclic schemas so the request gets
|
| 632 |
+
# a 400 instead of crashing into a 500.
|
| 633 |
+
normalize_json_schema_types(tool.function.parameters)
|
| 634 |
+
Draft202012Validator.check_schema(tool.function.parameters)
|
| 635 |
+
except SchemaError as e:
|
| 636 |
+
return f"Tool {i} function has invalid 'parameters' schema: {str(e)}"
|
| 637 |
+
except RecursionError:
|
| 638 |
+
return (
|
| 639 |
+
f"Tool {i} function 'parameters' schema is too deeply nested "
|
| 640 |
+
"or contains a cycle."
|
| 641 |
+
)
|
| 642 |
+
|
| 643 |
+
max_output_tokens = request.max_completion_tokens or request.max_tokens
|
| 644 |
+
server_context_length = self.tokenizer_manager.server_args.context_length
|
| 645 |
+
if (
|
| 646 |
+
max_output_tokens
|
| 647 |
+
and server_context_length
|
| 648 |
+
and max_output_tokens > server_context_length
|
| 649 |
+
) and not self.tokenizer_manager.server_args.allow_auto_truncate:
|
| 650 |
+
return (
|
| 651 |
+
f"max_completion_tokens is too large: {max_output_tokens}."
|
| 652 |
+
f"This model supports at most {server_context_length} completion tokens."
|
| 653 |
+
)
|
| 654 |
+
|
| 655 |
+
if request.response_format and request.response_format.type == "json_schema":
|
| 656 |
+
schema = getattr(request.response_format.json_schema, "schema_", None)
|
| 657 |
+
if schema is None:
|
| 658 |
+
return "schema_ is required for json_schema response format request."
|
| 659 |
+
|
| 660 |
+
return None
|
| 661 |
+
|
| 662 |
+
def _convert_to_internal_request(
|
| 663 |
+
self,
|
| 664 |
+
request: ChatCompletionRequest,
|
| 665 |
+
raw_request: Request = None,
|
| 666 |
+
) -> tuple[GenerateReqInput, ChatCompletionRequest]:
|
| 667 |
+
reasoning_effort = (
|
| 668 |
+
request.chat_template_kwargs.pop("reasoning_effort", None)
|
| 669 |
+
if request.chat_template_kwargs
|
| 670 |
+
else None
|
| 671 |
+
)
|
| 672 |
+
if self.is_gpt_oss and reasoning_effort == "none":
|
| 673 |
+
raise ValueError(
|
| 674 |
+
f"Harmony does not support reasoning effort {reasoning_effort}"
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
if reasoning_effort is not None:
|
| 678 |
+
request.reasoning_effort = reasoning_effort
|
| 679 |
+
|
| 680 |
+
if request.stream:
|
| 681 |
+
if request.return_prompt_token_ids:
|
| 682 |
+
raise ValueError(
|
| 683 |
+
"return_prompt_token_ids is not supported with streaming. "
|
| 684 |
+
"Please set stream=false when using return_prompt_token_ids=true."
|
| 685 |
+
)
|
| 686 |
+
if request.return_meta_info:
|
| 687 |
+
raise ValueError(
|
| 688 |
+
"return_meta_info is not supported with streaming. "
|
| 689 |
+
"Please set stream=false when using return_meta_info=true."
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
is_multimodal = self.tokenizer_manager.model_config.is_multimodal
|
| 693 |
+
|
| 694 |
+
# Process messages and apply chat template
|
| 695 |
+
processed_messages = self._process_messages(request, is_multimodal)
|
| 696 |
+
# Build sampling parameters
|
| 697 |
+
sampling_params = request.to_sampling_params(
|
| 698 |
+
stop=processed_messages.stop,
|
| 699 |
+
model_generation_config=self.default_sampling_params,
|
| 700 |
+
tool_call_constraint=processed_messages.tool_call_constraint,
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
# Handle single vs multiple requests
|
| 704 |
+
if request.input_ids is not None:
|
| 705 |
+
prompt_kwargs = {"input_ids": processed_messages.prompt_ids}
|
| 706 |
+
elif is_multimodal:
|
| 707 |
+
# Standard VLMs render a text prompt (with placeholder strings) for the MM
|
| 708 |
+
# processor to tokenize. Inkling's custom encoder instead produces pre-rendered
|
| 709 |
+
# input_ids with single placeholders; pass those through so the MM processor
|
| 710 |
+
# expands them rather than re-tokenizing an empty prompt. Gated on the Inkling
|
| 711 |
+
# encoding spec so every other model keeps the standard text path.
|
| 712 |
+
if (
|
| 713 |
+
self.chat_encoding_spec == "inkling"
|
| 714 |
+
and isinstance(processed_messages.prompt_ids, list)
|
| 715 |
+
and processed_messages.prompt_ids
|
| 716 |
+
):
|
| 717 |
+
prompt_kwargs = {"input_ids": processed_messages.prompt_ids}
|
| 718 |
+
else:
|
| 719 |
+
prompt_kwargs = {"text": processed_messages.prompt}
|
| 720 |
+
else:
|
| 721 |
+
if isinstance(processed_messages.prompt_ids, str):
|
| 722 |
+
prompt_kwargs = {"text": processed_messages.prompt_ids}
|
| 723 |
+
else:
|
| 724 |
+
prompt_kwargs = {"input_ids": processed_messages.prompt_ids}
|
| 725 |
+
|
| 726 |
+
# Extract custom labels from raw request headers
|
| 727 |
+
custom_labels = self.extract_custom_labels(raw_request)
|
| 728 |
+
|
| 729 |
+
# Extract routed_dp_rank from header (has higher priority than body)
|
| 730 |
+
effective_routed_dp_rank = self.extract_routed_dp_rank_from_header(
|
| 731 |
+
raw_request, request.routed_dp_rank
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
# Resolve LoRA adapter from model parameter or explicit lora_path
|
| 735 |
+
lora_path = self._resolve_lora_path(request.model, request.lora_path)
|
| 736 |
+
img_max_dynamic_patch, vid_max_dynamic_patch = _extract_max_dynamic_patch(
|
| 737 |
+
request
|
| 738 |
+
)
|
| 739 |
+
require_reasoning = self._get_reasoning_from_request(request)
|
| 740 |
+
|
| 741 |
+
adapted_request = GenerateReqInput(
|
| 742 |
+
**prompt_kwargs,
|
| 743 |
+
image_data=processed_messages.image_data,
|
| 744 |
+
video_data=processed_messages.video_data,
|
| 745 |
+
audio_data=processed_messages.audio_data,
|
| 746 |
+
sampling_params=sampling_params,
|
| 747 |
+
return_logprob=request.logprobs,
|
| 748 |
+
logprob_start_len=-1,
|
| 749 |
+
top_logprobs_num=request.top_logprobs or 0,
|
| 750 |
+
stream=request.stream,
|
| 751 |
+
return_text_in_logprobs=True,
|
| 752 |
+
modalities=processed_messages.modalities,
|
| 753 |
+
lora_path=lora_path,
|
| 754 |
+
bootstrap_host=request.bootstrap_host,
|
| 755 |
+
bootstrap_port=request.bootstrap_port,
|
| 756 |
+
bootstrap_room=request.bootstrap_room,
|
| 757 |
+
routed_dp_rank=effective_routed_dp_rank,
|
| 758 |
+
disagg_prefill_dp_rank=request.disagg_prefill_dp_rank,
|
| 759 |
+
return_hidden_states=request.return_hidden_states,
|
| 760 |
+
return_routed_experts=request.return_routed_experts,
|
| 761 |
+
routed_experts_start_len=request.routed_experts_start_len,
|
| 762 |
+
rid=request.rid,
|
| 763 |
+
session_id=request.session_id,
|
| 764 |
+
extra_key=self._compute_extra_key(request),
|
| 765 |
+
require_reasoning=require_reasoning,
|
| 766 |
+
priority=request.priority,
|
| 767 |
+
routing_key=self.extract_routing_key(raw_request),
|
| 768 |
+
custom_labels=custom_labels,
|
| 769 |
+
custom_logit_processor=request.custom_logit_processor,
|
| 770 |
+
images_config=getattr(request, "images_config", None),
|
| 771 |
+
image_max_dynamic_patch=img_max_dynamic_patch,
|
| 772 |
+
video_max_dynamic_patch=vid_max_dynamic_patch,
|
| 773 |
+
max_dynamic_patch=getattr(request, "max_dynamic_patch", None),
|
| 774 |
+
use_audio_in_video=getattr(request, "use_audio_in_video", False),
|
| 775 |
+
return_prompt_token_ids=request.return_prompt_token_ids,
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
return adapted_request, request
|
| 779 |
+
|
| 780 |
+
def _process_messages(
|
| 781 |
+
self, request: ChatCompletionRequest, is_multimodal: bool
|
| 782 |
+
) -> MessageProcessingResult:
|
| 783 |
+
"""Process chat messages and apply chat template"""
|
| 784 |
+
if self.default_chat_template_kwargs:
|
| 785 |
+
ctk = dict(request.chat_template_kwargs or {})
|
| 786 |
+
for k, v in self.default_chat_template_kwargs.items():
|
| 787 |
+
ctk.setdefault(k, v)
|
| 788 |
+
request.chat_template_kwargs = ctk
|
| 789 |
+
effort = ctk.get("reasoning_effort")
|
| 790 |
+
if effort is not None and request.reasoning_effort is None:
|
| 791 |
+
request.reasoning_effort = effort
|
| 792 |
+
|
| 793 |
+
# GptOss model needs to keep special tokens for harmony parsing
|
| 794 |
+
if self.is_gpt_oss or self.is_gemma4:
|
| 795 |
+
request.skip_special_tokens = False
|
| 796 |
+
|
| 797 |
+
self._patch_reasoning_skip_special_tokens(request)
|
| 798 |
+
|
| 799 |
+
thinking_mode = self._get_reasoning_from_request(request)
|
| 800 |
+
# SGLang's ReasonerGrammarBackend owns the reasoning prefix
|
| 801 |
+
# when --reasoning-parser is configured, so builtin xgrammar
|
| 802 |
+
# tags must describe only the post-reasoning tool-call suffix.
|
| 803 |
+
xgrammar_reasoning = thinking_mode and (
|
| 804 |
+
self.tokenizer_manager.server_args.reasoning_parser is None
|
| 805 |
+
)
|
| 806 |
+
tool_call_constraint = None
|
| 807 |
+
|
| 808 |
+
# Apply chat template and its stop strings
|
| 809 |
+
tools = None
|
| 810 |
+
if request.tools and request.tool_choice != "none":
|
| 811 |
+
request.skip_special_tokens = False
|
| 812 |
+
if not isinstance(request.tool_choice, str):
|
| 813 |
+
tools = [
|
| 814 |
+
item.model_dump()
|
| 815 |
+
for item in request.tools
|
| 816 |
+
if item.function.name == request.tool_choice.function.name
|
| 817 |
+
]
|
| 818 |
+
else:
|
| 819 |
+
tools = [item.model_dump() for item in request.tools]
|
| 820 |
+
if self.tool_call_parser:
|
| 821 |
+
parser = FunctionCallParser(
|
| 822 |
+
request.tools,
|
| 823 |
+
self.tool_call_parser,
|
| 824 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 825 |
+
)
|
| 826 |
+
tool_call_constraint = parser.get_structure_constraint(
|
| 827 |
+
request.tool_choice,
|
| 828 |
+
parallel_tool_calls=request.parallel_tool_calls,
|
| 829 |
+
thinking_mode=xgrammar_reasoning,
|
| 830 |
+
)
|
| 831 |
+
# Fallback: use generic JSON schema for required/named tool choice
|
| 832 |
+
# only when no parser-specific constraint was set
|
| 833 |
+
if tool_call_constraint is None and (
|
| 834 |
+
request.tool_choice == "required"
|
| 835 |
+
or isinstance(request.tool_choice, ToolChoice)
|
| 836 |
+
):
|
| 837 |
+
json_schema = get_json_schema_constraint(
|
| 838 |
+
request.tools,
|
| 839 |
+
request.tool_choice,
|
| 840 |
+
parallel_tool_calls=request.parallel_tool_calls,
|
| 841 |
+
)
|
| 842 |
+
tool_call_constraint = ("json_schema", json_schema)
|
| 843 |
+
|
| 844 |
+
# When input_ids are provided, skip template tokenization entirely;
|
| 845 |
+
# only stop tokens and tool_call_constraint are needed.
|
| 846 |
+
if request.input_ids is not None:
|
| 847 |
+
result = MessageProcessingResult(
|
| 848 |
+
prompt="",
|
| 849 |
+
prompt_ids=request.input_ids,
|
| 850 |
+
image_data=None,
|
| 851 |
+
audio_data=None,
|
| 852 |
+
video_data=None,
|
| 853 |
+
modalities=[],
|
| 854 |
+
stop=request.stop or [],
|
| 855 |
+
)
|
| 856 |
+
elif self.template_manager.chat_template_name is None:
|
| 857 |
+
result = self._apply_jinja_template(request, tools, is_multimodal)
|
| 858 |
+
else:
|
| 859 |
+
result = self._apply_conversation_template(request, is_multimodal)
|
| 860 |
+
|
| 861 |
+
result.tool_call_constraint = tool_call_constraint
|
| 862 |
+
return result
|
| 863 |
+
|
| 864 |
+
def _apply_jinja_template(
|
| 865 |
+
self,
|
| 866 |
+
request: ChatCompletionRequest,
|
| 867 |
+
tools: Optional[List[Dict]],
|
| 868 |
+
is_multimodal: bool,
|
| 869 |
+
) -> MessageProcessingResult:
|
| 870 |
+
"""Apply Jinja chat template"""
|
| 871 |
+
prompt = ""
|
| 872 |
+
prompt_ids = []
|
| 873 |
+
openai_compatible_messages = []
|
| 874 |
+
image_data = []
|
| 875 |
+
video_data = []
|
| 876 |
+
audio_data = []
|
| 877 |
+
modalities = []
|
| 878 |
+
|
| 879 |
+
template_content_format = self.template_manager.jinja_template_content_format
|
| 880 |
+
|
| 881 |
+
# Try custom encoding first (override in subclass for custom renderers)
|
| 882 |
+
thinking_requested = (request.chat_template_kwargs or {}).get(
|
| 883 |
+
"thinking", envs.SGLANG_DEFAULT_THINKING.get()
|
| 884 |
+
)
|
| 885 |
+
thinking_mode = (
|
| 886 |
+
ThinkingMode.THINKING if thinking_requested else ThinkingMode.CHAT
|
| 887 |
+
)
|
| 888 |
+
messages = [msg.model_dump() for msg in request.messages]
|
| 889 |
+
for message in messages:
|
| 890 |
+
normalize_assistant_tool_call_arguments(message)
|
| 891 |
+
|
| 892 |
+
prompt_ids = self._encode_messages(
|
| 893 |
+
copy.deepcopy(messages),
|
| 894 |
+
request,
|
| 895 |
+
thinking_mode,
|
| 896 |
+
tools=tools,
|
| 897 |
+
)
|
| 898 |
+
|
| 899 |
+
if prompt_ids is not None:
|
| 900 |
+
# Custom encoding produced prompt_ids. Text-only encoders (dsv4/dsv32) need
|
| 901 |
+
# nothing more; Inkling is the only multimodal custom encoder and still needs the
|
| 902 |
+
# image/audio media harvested from the messages for the MM processor.
|
| 903 |
+
if self.chat_encoding_spec == "inkling":
|
| 904 |
+
for message in request.messages:
|
| 905 |
+
msg_dict = message.model_dump()
|
| 906 |
+
if msg_dict.get("content") is None:
|
| 907 |
+
msg_dict["content"] = ""
|
| 908 |
+
process_content_for_template_format(
|
| 909 |
+
msg_dict,
|
| 910 |
+
"openai",
|
| 911 |
+
image_data,
|
| 912 |
+
video_data,
|
| 913 |
+
audio_data,
|
| 914 |
+
modalities,
|
| 915 |
+
)
|
| 916 |
+
elif self.chat_encoding_spec is not None:
|
| 917 |
+
# dsv4/dsv32 encoding path
|
| 918 |
+
messages = copy.deepcopy(messages)
|
| 919 |
+
|
| 920 |
+
# dsv4/dsv32 are text-only and consume string content; flatten
|
| 921 |
+
# OpenAI parts-list content here so the encoder sees a plain string.
|
| 922 |
+
for i, msg in enumerate(messages):
|
| 923 |
+
if isinstance(msg.get("content"), list):
|
| 924 |
+
messages[i] = process_content_for_template_format(
|
| 925 |
+
msg, "string", [], [], [], []
|
| 926 |
+
)
|
| 927 |
+
|
| 928 |
+
for msg in messages:
|
| 929 |
+
if msg.get("content") is None:
|
| 930 |
+
msg["content"] = ""
|
| 931 |
+
processed_msg = process_content_for_template_format(
|
| 932 |
+
msg,
|
| 933 |
+
template_content_format,
|
| 934 |
+
image_data,
|
| 935 |
+
video_data,
|
| 936 |
+
audio_data,
|
| 937 |
+
modalities,
|
| 938 |
+
use_dpsk_v32_encoding=self.chat_encoding_spec == "dsv32",
|
| 939 |
+
)
|
| 940 |
+
msg.update(processed_msg)
|
| 941 |
+
|
| 942 |
+
# Handle continue_final_message: separate final assistant message
|
| 943 |
+
messages, assistant_prefix = self._handle_last_assistant_message(
|
| 944 |
+
messages, request
|
| 945 |
+
)
|
| 946 |
+
|
| 947 |
+
if messages[0]["role"] != "system":
|
| 948 |
+
# Insert a placeholder system message so tool schemas have
|
| 949 |
+
# somewhere to render. $AGNES_DEFAULT_SYSTEM_PROMPT gives it
|
| 950 |
+
# default content; unset (or empty) restores upstream behaviour
|
| 951 |
+
# of an empty system message. A system message supplied by the
|
| 952 |
+
# request is never overridden -- this branch only runs when the
|
| 953 |
+
# conversation does not open with one.
|
| 954 |
+
messages.insert(
|
| 955 |
+
0,
|
| 956 |
+
{
|
| 957 |
+
"role": "system",
|
| 958 |
+
"content": os.environ.get("AGNES_DEFAULT_SYSTEM_PROMPT", ""),
|
| 959 |
+
},
|
| 960 |
+
)
|
| 961 |
+
if request.tools:
|
| 962 |
+
messages[0]["tools"] = [tool.model_dump() for tool in request.tools]
|
| 963 |
+
|
| 964 |
+
# Default encoding (dsv4/dsv32)
|
| 965 |
+
if self.chat_encoding_spec == "dsv4":
|
| 966 |
+
# V4 encoder only accepts "max" / "high" / None.
|
| 967 |
+
# OpenAI protocol defaults to "medium" which V4 rejects; drop it.
|
| 968 |
+
# Fallback: if request didn't set it, try env SGLANG_DSV4_REASONING_EFFORT.
|
| 969 |
+
effort_source = request.reasoning_effort
|
| 970 |
+
if effort_source is None:
|
| 971 |
+
env_val = envs.SGLANG_DSV4_REASONING_EFFORT.get()
|
| 972 |
+
if env_val:
|
| 973 |
+
effort_source = env_val
|
| 974 |
+
v4_reasoning_effort = (
|
| 975 |
+
effort_source if effort_source in ("max", "high") else None
|
| 976 |
+
)
|
| 977 |
+
if request.task is not None:
|
| 978 |
+
encoding_dsv4.attach_task_to_last_user_message(
|
| 979 |
+
messages, request.task
|
| 980 |
+
)
|
| 981 |
+
real_input = encoding_dsv4.encode_messages(
|
| 982 |
+
messages,
|
| 983 |
+
thinking_mode=thinking_mode,
|
| 984 |
+
reasoning_effort=v4_reasoning_effort,
|
| 985 |
+
)
|
| 986 |
+
prompt_ids = self.tokenizer_manager.tokenizer.encode(real_input)
|
| 987 |
+
else:
|
| 988 |
+
real_input = encoding_dsv32.encode_messages(
|
| 989 |
+
messages, thinking_mode=thinking_mode
|
| 990 |
+
)
|
| 991 |
+
prompt_ids = self.tokenizer_manager.tokenizer.encode(real_input)
|
| 992 |
+
|
| 993 |
+
# Append assistant prefix if continue_final_message is enabled
|
| 994 |
+
if assistant_prefix:
|
| 995 |
+
prompt_ids = self._append_assistant_prefix_to_prompt_ids(
|
| 996 |
+
prompt_ids, assistant_prefix
|
| 997 |
+
)
|
| 998 |
+
else:
|
| 999 |
+
for msg_dict in copy.deepcopy(messages):
|
| 1000 |
+
if msg_dict.get("content") is None:
|
| 1001 |
+
msg_dict["content"] = ""
|
| 1002 |
+
|
| 1003 |
+
# Process content based on detected template format
|
| 1004 |
+
processed_msg = process_content_for_template_format(
|
| 1005 |
+
msg_dict,
|
| 1006 |
+
template_content_format,
|
| 1007 |
+
image_data,
|
| 1008 |
+
video_data,
|
| 1009 |
+
audio_data,
|
| 1010 |
+
modalities,
|
| 1011 |
+
)
|
| 1012 |
+
|
| 1013 |
+
processed_msg["content"] = normalize_tool_content(
|
| 1014 |
+
processed_msg["role"], processed_msg.get("content")
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
openai_compatible_messages.append(processed_msg)
|
| 1018 |
+
|
| 1019 |
+
# Handle continue_final_message: separate final assistant message
|
| 1020 |
+
openai_compatible_messages, assistant_prefix = (
|
| 1021 |
+
self._handle_last_assistant_message(openai_compatible_messages, request)
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
+
extra_template_kwargs = {}
|
| 1025 |
+
if request.reasoning_effort is not None:
|
| 1026 |
+
extra_template_kwargs["reasoning_effort"] = request.reasoning_effort
|
| 1027 |
+
if request.chat_template_kwargs:
|
| 1028 |
+
extra_template_kwargs.update(request.chat_template_kwargs)
|
| 1029 |
+
|
| 1030 |
+
rc = self.template_manager.reasoning_config
|
| 1031 |
+
if rc is not None and rc.effort_kwarg is not None:
|
| 1032 |
+
if request.reasoning_effort == "low":
|
| 1033 |
+
extra_template_kwargs.setdefault(rc.effort_kwarg, True)
|
| 1034 |
+
elif request.reasoning_effort in ("medium", "high", "max"):
|
| 1035 |
+
logger.warning(
|
| 1036 |
+
"Model '%s' supports only 'low' reasoning effort; "
|
| 1037 |
+
"requested '%s' treated as default thinking",
|
| 1038 |
+
self.tokenizer_manager.server_args.served_model_name,
|
| 1039 |
+
request.reasoning_effort,
|
| 1040 |
+
)
|
| 1041 |
+
|
| 1042 |
+
# Split apply_chat_template(tokenize=True) into render + encode so we
|
| 1043 |
+
# can skip add_special_tokens=False on tokenizers that don't auto-add
|
| 1044 |
+
# specials (Kimi-like, OpenAI-chat analogue of #25265). Chat
|
| 1045 |
+
# templates already include role/special tokens, so the encode must
|
| 1046 |
+
# avoid double BOS on tokenizers that would add it.
|
| 1047 |
+
encode_kwargs = (
|
| 1048 |
+
{"add_special_tokens": False}
|
| 1049 |
+
if self._tokenizer_auto_adds_specials
|
| 1050 |
+
else {}
|
| 1051 |
+
)
|
| 1052 |
+
try:
|
| 1053 |
+
rendered_prompt = self.tokenizer_manager.tokenizer.apply_chat_template(
|
| 1054 |
+
openai_compatible_messages,
|
| 1055 |
+
tokenize=False,
|
| 1056 |
+
add_generation_prompt=True,
|
| 1057 |
+
tools=tools,
|
| 1058 |
+
return_dict=False,
|
| 1059 |
+
**extra_template_kwargs,
|
| 1060 |
+
)
|
| 1061 |
+
prompt_ids = self.tokenizer_manager.tokenizer.encode(
|
| 1062 |
+
rendered_prompt, **encode_kwargs
|
| 1063 |
+
)
|
| 1064 |
+
except Exception:
|
| 1065 |
+
# If the first attempt fails, try with flat function-only format.
|
| 1066 |
+
# Some templates (e.g. Mistral) expect tools without the OpenAI wrapper.
|
| 1067 |
+
tools = (
|
| 1068 |
+
[t["function"] if "function" in t else t for t in tools]
|
| 1069 |
+
if tools
|
| 1070 |
+
else None
|
| 1071 |
+
)
|
| 1072 |
+
try:
|
| 1073 |
+
rendered_prompt = (
|
| 1074 |
+
self.tokenizer_manager.tokenizer.apply_chat_template(
|
| 1075 |
+
openai_compatible_messages,
|
| 1076 |
+
tokenize=False,
|
| 1077 |
+
add_generation_prompt=True,
|
| 1078 |
+
tools=tools,
|
| 1079 |
+
return_dict=False,
|
| 1080 |
+
**extra_template_kwargs,
|
| 1081 |
+
)
|
| 1082 |
+
)
|
| 1083 |
+
prompt_ids = self.tokenizer_manager.tokenizer.encode(
|
| 1084 |
+
rendered_prompt, **encode_kwargs
|
| 1085 |
+
)
|
| 1086 |
+
except (jinja2.TemplateError, TypeError) as template_error:
|
| 1087 |
+
# Template errors (e.g., from raise_exception in Jinja templates)
|
| 1088 |
+
# and TypeError (e.g., tojson filter on Jinja2 Undefined variables)
|
| 1089 |
+
# should be treated as client errors (400 BadRequest)
|
| 1090 |
+
raise ValueError(str(template_error)) from template_error
|
| 1091 |
+
|
| 1092 |
+
# Append assistant prefix if continue_final_message is enabled
|
| 1093 |
+
if assistant_prefix:
|
| 1094 |
+
prompt_ids = self._append_assistant_prefix_to_prompt_ids(
|
| 1095 |
+
prompt_ids, assistant_prefix
|
| 1096 |
+
)
|
| 1097 |
+
|
| 1098 |
+
if is_multimodal:
|
| 1099 |
+
prompt = self.tokenizer_manager.tokenizer.decode(prompt_ids)
|
| 1100 |
+
|
| 1101 |
+
stop = request.stop
|
| 1102 |
+
image_data = image_data if image_data else None
|
| 1103 |
+
audio_data = audio_data if audio_data else None
|
| 1104 |
+
video_data = video_data if video_data else None
|
| 1105 |
+
modalities = modalities if modalities else []
|
| 1106 |
+
return MessageProcessingResult(
|
| 1107 |
+
prompt=prompt,
|
| 1108 |
+
prompt_ids=prompt_ids,
|
| 1109 |
+
image_data=image_data,
|
| 1110 |
+
video_data=video_data,
|
| 1111 |
+
audio_data=audio_data,
|
| 1112 |
+
modalities=modalities,
|
| 1113 |
+
stop=stop,
|
| 1114 |
+
)
|
| 1115 |
+
|
| 1116 |
+
def _apply_conversation_template(
|
| 1117 |
+
self,
|
| 1118 |
+
request: ChatCompletionRequest,
|
| 1119 |
+
is_multimodal: bool,
|
| 1120 |
+
) -> MessageProcessingResult:
|
| 1121 |
+
"""Apply conversation template"""
|
| 1122 |
+
prompt = ""
|
| 1123 |
+
prompt_ids = []
|
| 1124 |
+
conv = generate_chat_conv(request, self.template_manager.chat_template_name)
|
| 1125 |
+
|
| 1126 |
+
# If we should continue the final assistant message, adjust the conversation.
|
| 1127 |
+
if (
|
| 1128 |
+
request.continue_final_message
|
| 1129 |
+
and request.messages
|
| 1130 |
+
and request.messages[-1].role == "assistant"
|
| 1131 |
+
):
|
| 1132 |
+
# Remove the auto-added blank assistant turn, if present.
|
| 1133 |
+
if conv.messages and conv.messages[-1][1] is None:
|
| 1134 |
+
conv.messages.pop()
|
| 1135 |
+
# Rebuild the prompt from the conversation.
|
| 1136 |
+
prompt = conv.get_prompt()
|
| 1137 |
+
# Strip trailing stop tokens or separators that indicate end-of-assistant.
|
| 1138 |
+
if isinstance(conv.stop_str, list):
|
| 1139 |
+
for stop_token in conv.stop_str:
|
| 1140 |
+
if prompt.endswith(stop_token):
|
| 1141 |
+
prompt = prompt[: -len(stop_token)]
|
| 1142 |
+
elif isinstance(conv.stop_str, str) and prompt.endswith(conv.stop_str):
|
| 1143 |
+
prompt = prompt[: -len(conv.stop_str)]
|
| 1144 |
+
if conv.sep and prompt.endswith(conv.sep):
|
| 1145 |
+
prompt = prompt[: -len(conv.sep)]
|
| 1146 |
+
if getattr(conv, "sep2", None) and prompt.endswith(conv.sep2):
|
| 1147 |
+
prompt = prompt[: -len(conv.sep2)]
|
| 1148 |
+
else:
|
| 1149 |
+
prompt = conv.get_prompt()
|
| 1150 |
+
if self._get_reasoning_from_request(request) and (
|
| 1151 |
+
self._reasoning_detector is None
|
| 1152 |
+
or not self._reasoning_detector.thinks_internally
|
| 1153 |
+
):
|
| 1154 |
+
# Models with thinks_internally=True think without a leading <think> token
|
| 1155 |
+
prompt += "<think>" # Note(Xinyuan): hard code thinking token
|
| 1156 |
+
|
| 1157 |
+
image_data = conv.image_data if conv.image_data else None
|
| 1158 |
+
video_data = conv.video_data if conv.video_data else None
|
| 1159 |
+
audio_data = conv.audio_data if conv.audio_data else None
|
| 1160 |
+
modalities = conv.modalities if conv.modalities else []
|
| 1161 |
+
stop = copy.copy(conv.stop_str or [] if not request.ignore_eos else [])
|
| 1162 |
+
|
| 1163 |
+
if request.stop:
|
| 1164 |
+
if isinstance(request.stop, str):
|
| 1165 |
+
stop.append(request.stop)
|
| 1166 |
+
else:
|
| 1167 |
+
stop.extend(request.stop)
|
| 1168 |
+
|
| 1169 |
+
if not is_multimodal:
|
| 1170 |
+
prompt_ids = self.tokenizer_manager.tokenizer.encode(prompt)
|
| 1171 |
+
|
| 1172 |
+
return MessageProcessingResult(
|
| 1173 |
+
prompt=prompt,
|
| 1174 |
+
prompt_ids=prompt_ids,
|
| 1175 |
+
image_data=image_data,
|
| 1176 |
+
video_data=video_data,
|
| 1177 |
+
audio_data=audio_data,
|
| 1178 |
+
modalities=modalities,
|
| 1179 |
+
stop=stop,
|
| 1180 |
+
)
|
| 1181 |
+
|
| 1182 |
+
async def _handle_streaming_request(
|
| 1183 |
+
self,
|
| 1184 |
+
adapted_request: GenerateReqInput,
|
| 1185 |
+
request: ChatCompletionRequest,
|
| 1186 |
+
raw_request: Request,
|
| 1187 |
+
) -> Union[StreamingResponse, ErrorResponse]:
|
| 1188 |
+
"""Handle streaming chat completion request"""
|
| 1189 |
+
generator = self._generate_chat_stream(adapted_request, request, raw_request)
|
| 1190 |
+
|
| 1191 |
+
# Kick-start the generator to trigger validation before HTTP 200 is sent.
|
| 1192 |
+
# If validation fails (e.g., context length exceeded), we can still return
|
| 1193 |
+
# a proper HTTP 400 error response instead of streaming it as SSE payload.
|
| 1194 |
+
try:
|
| 1195 |
+
first_chunk = await generator.__anext__()
|
| 1196 |
+
except ValueError as e:
|
| 1197 |
+
return self.create_error_response(str(e))
|
| 1198 |
+
|
| 1199 |
+
async def prepend_first_chunk():
|
| 1200 |
+
yield first_chunk
|
| 1201 |
+
async for chunk in generator:
|
| 1202 |
+
yield chunk
|
| 1203 |
+
|
| 1204 |
+
return StreamingResponse(
|
| 1205 |
+
prepend_first_chunk(),
|
| 1206 |
+
media_type="text/event-stream",
|
| 1207 |
+
background=self.tokenizer_manager.create_abort_task(adapted_request),
|
| 1208 |
+
)
|
| 1209 |
+
|
| 1210 |
+
async def _generate_chat_stream(
|
| 1211 |
+
self,
|
| 1212 |
+
adapted_request: GenerateReqInput,
|
| 1213 |
+
request: ChatCompletionRequest,
|
| 1214 |
+
raw_request: Request,
|
| 1215 |
+
) -> AsyncGenerator[str, None]:
|
| 1216 |
+
"""Generate streaming chat completion response"""
|
| 1217 |
+
# Parsers for tool calls and reasoning
|
| 1218 |
+
parser_dict = {}
|
| 1219 |
+
reasoning_parser_dict = {}
|
| 1220 |
+
|
| 1221 |
+
# State tracking for streaming
|
| 1222 |
+
is_firsts = {}
|
| 1223 |
+
stream_offsets = {}
|
| 1224 |
+
n_prev_tokens = {}
|
| 1225 |
+
has_tool_calls = {}
|
| 1226 |
+
finish_reasons = {}
|
| 1227 |
+
|
| 1228 |
+
# Usage tracking
|
| 1229 |
+
prompt_tokens = {}
|
| 1230 |
+
reasoning_tokens = {}
|
| 1231 |
+
completion_tokens = {}
|
| 1232 |
+
cached_tokens = {}
|
| 1233 |
+
hidden_states = {}
|
| 1234 |
+
routed_experts = {}
|
| 1235 |
+
cached_tokens_details = {}
|
| 1236 |
+
image_tokens = {}
|
| 1237 |
+
audio_tokens = {}
|
| 1238 |
+
video_tokens = {}
|
| 1239 |
+
|
| 1240 |
+
stream_started = False
|
| 1241 |
+
try:
|
| 1242 |
+
include_usage, continuous_usage_stats = should_include_usage(
|
| 1243 |
+
request.stream_options,
|
| 1244 |
+
self.tokenizer_manager.server_args.stream_response_default_include_usage,
|
| 1245 |
+
)
|
| 1246 |
+
|
| 1247 |
+
async for content in self.tokenizer_manager.generate_request(
|
| 1248 |
+
adapted_request, raw_request
|
| 1249 |
+
):
|
| 1250 |
+
index = content.get("index", 0)
|
| 1251 |
+
|
| 1252 |
+
prompt_tokens[index] = content["meta_info"].get("prompt_tokens", 0)
|
| 1253 |
+
completion_tokens[index] = content["meta_info"].get(
|
| 1254 |
+
"completion_tokens", 0
|
| 1255 |
+
)
|
| 1256 |
+
reasoning_tokens[index] = content["meta_info"].get(
|
| 1257 |
+
"reasoning_tokens", 0
|
| 1258 |
+
)
|
| 1259 |
+
cached_tokens[index] = content["meta_info"].get("cached_tokens", 0)
|
| 1260 |
+
hidden_states[index] = content["meta_info"].get("hidden_states", None)
|
| 1261 |
+
routed_experts[index] = content["meta_info"].get("routed_experts", None)
|
| 1262 |
+
cached_tokens_details[index] = content["meta_info"].get(
|
| 1263 |
+
"cached_tokens_details", None
|
| 1264 |
+
)
|
| 1265 |
+
image_tokens[index] = content["meta_info"].get("image_tokens", 0)
|
| 1266 |
+
audio_tokens[index] = content["meta_info"].get("audio_tokens", 0)
|
| 1267 |
+
video_tokens[index] = content["meta_info"].get("video_tokens", 0)
|
| 1268 |
+
|
| 1269 |
+
# Handle logprobs
|
| 1270 |
+
choice_logprobs = None
|
| 1271 |
+
if request.logprobs:
|
| 1272 |
+
n_prev_token = n_prev_tokens.get(index, 0)
|
| 1273 |
+
total_output_logprobs = content["meta_info"][
|
| 1274 |
+
"output_token_logprobs_length"
|
| 1275 |
+
]
|
| 1276 |
+
if n_prev_token < total_output_logprobs:
|
| 1277 |
+
choice_logprobs = self._process_streaming_logprobs(
|
| 1278 |
+
content, n_prev_token, total_output_logprobs
|
| 1279 |
+
).model_dump()
|
| 1280 |
+
n_prev_tokens[index] = total_output_logprobs
|
| 1281 |
+
|
| 1282 |
+
finish_reason = content["meta_info"].get("finish_reason", None)
|
| 1283 |
+
finish_reason_type = finish_reason["type"] if finish_reason else None
|
| 1284 |
+
|
| 1285 |
+
# Track finish_reason for each index
|
| 1286 |
+
if finish_reason_type:
|
| 1287 |
+
# Abort with an explicit error status_code is a system error
|
| 1288 |
+
# (timeout, OOM, validation): emit a streaming error chunk.
|
| 1289 |
+
# A graceful abort (no status_code, e.g. user-initiated via
|
| 1290 |
+
# /abort_request or session lifecycle cleanup) falls through
|
| 1291 |
+
# to the normal chunk path, matching the non-stream behavior
|
| 1292 |
+
# in tokenizer_manager._handle_abort_finish_reason.
|
| 1293 |
+
if finish_reason_type == "abort" and isinstance(
|
| 1294 |
+
finish_reason.get("status_code"), HTTPStatus
|
| 1295 |
+
):
|
| 1296 |
+
code = finish_reason["status_code"]
|
| 1297 |
+
error = self.create_streaming_error_response(
|
| 1298 |
+
finish_reason.get("message", "Generation aborted."),
|
| 1299 |
+
code.name,
|
| 1300 |
+
code.value,
|
| 1301 |
+
)
|
| 1302 |
+
yield f"data: {error}\n\n"
|
| 1303 |
+
break
|
| 1304 |
+
finish_reasons[index] = finish_reason
|
| 1305 |
+
|
| 1306 |
+
# First chunk with role
|
| 1307 |
+
if is_firsts.get(index, True):
|
| 1308 |
+
is_firsts[index] = False
|
| 1309 |
+
yield build_sse_content(
|
| 1310 |
+
chunk_id=content["meta_info"]["id"],
|
| 1311 |
+
created=int(time.time()),
|
| 1312 |
+
model=request.model,
|
| 1313 |
+
index=index,
|
| 1314 |
+
role="assistant",
|
| 1315 |
+
content="",
|
| 1316 |
+
)
|
| 1317 |
+
stream_started = True
|
| 1318 |
+
|
| 1319 |
+
# Generate streaming content (override in subclass for custom behavior)
|
| 1320 |
+
async for chunk in self._generate_stream_content(
|
| 1321 |
+
content=content,
|
| 1322 |
+
index=index,
|
| 1323 |
+
request=request,
|
| 1324 |
+
stream_offsets=stream_offsets,
|
| 1325 |
+
reasoning_parser_dict=reasoning_parser_dict,
|
| 1326 |
+
parser_dict=parser_dict,
|
| 1327 |
+
has_tool_calls=has_tool_calls,
|
| 1328 |
+
choice_logprobs=choice_logprobs,
|
| 1329 |
+
finish_reason_type=finish_reason_type,
|
| 1330 |
+
continuous_usage_stats=continuous_usage_stats,
|
| 1331 |
+
prompt_tokens=prompt_tokens,
|
| 1332 |
+
reasoning_tokens=reasoning_tokens,
|
| 1333 |
+
completion_tokens=completion_tokens,
|
| 1334 |
+
):
|
| 1335 |
+
yield chunk
|
| 1336 |
+
|
| 1337 |
+
# Send finish_reason chunks for each index that completed
|
| 1338 |
+
for idx, finish_reason_data in finish_reasons.items():
|
| 1339 |
+
finish_reason_type = finish_reason_data["type"]
|
| 1340 |
+
|
| 1341 |
+
# Change finish_reason to "tool_calls" if we had tool calls and stopped naturally
|
| 1342 |
+
final_finish_reason = finish_reason_type
|
| 1343 |
+
if has_tool_calls.get(idx, False) and finish_reason_type == "stop":
|
| 1344 |
+
final_finish_reason = "tool_calls"
|
| 1345 |
+
|
| 1346 |
+
matched_stop = finish_reason_data.get("matched")
|
| 1347 |
+
yield build_sse_content(
|
| 1348 |
+
chunk_id=content["meta_info"]["id"],
|
| 1349 |
+
created=int(time.time()),
|
| 1350 |
+
model=request.model,
|
| 1351 |
+
index=idx,
|
| 1352 |
+
finish_reason=final_finish_reason,
|
| 1353 |
+
matched_stop=matched_stop,
|
| 1354 |
+
)
|
| 1355 |
+
|
| 1356 |
+
# Send hidden states if requested
|
| 1357 |
+
if request.return_hidden_states and hidden_states:
|
| 1358 |
+
for index, choice_hidden_states in hidden_states.items():
|
| 1359 |
+
if choice_hidden_states:
|
| 1360 |
+
last_token_hidden_states = (
|
| 1361 |
+
choice_hidden_states[-1]
|
| 1362 |
+
if len(choice_hidden_states) > 1
|
| 1363 |
+
else []
|
| 1364 |
+
)
|
| 1365 |
+
hidden_states_chunk = ChatCompletionStreamResponse(
|
| 1366 |
+
id=content["meta_info"]["id"],
|
| 1367 |
+
created=int(time.time()),
|
| 1368 |
+
choices=[
|
| 1369 |
+
ChatCompletionResponseStreamChoice(
|
| 1370 |
+
index=index,
|
| 1371 |
+
delta=DeltaMessage(
|
| 1372 |
+
hidden_states=last_token_hidden_states
|
| 1373 |
+
),
|
| 1374 |
+
finish_reason=None, # Hidden states don't need finish_reason
|
| 1375 |
+
)
|
| 1376 |
+
],
|
| 1377 |
+
model=request.model,
|
| 1378 |
+
)
|
| 1379 |
+
yield f"data: {hidden_states_chunk.model_dump_json()}\n\n"
|
| 1380 |
+
|
| 1381 |
+
sglext_routed = None
|
| 1382 |
+
if request.return_routed_experts and routed_experts:
|
| 1383 |
+
sglext_routed = next(
|
| 1384 |
+
(v for v in routed_experts.values() if v is not None), None
|
| 1385 |
+
)
|
| 1386 |
+
|
| 1387 |
+
sglext_details = None
|
| 1388 |
+
if request.return_cached_tokens_details and cached_tokens_details:
|
| 1389 |
+
first_details = next(
|
| 1390 |
+
(v for v in cached_tokens_details.values() if v is not None), None
|
| 1391 |
+
)
|
| 1392 |
+
if first_details is not None:
|
| 1393 |
+
sglext_details = cached_tokens_details_from_dict(first_details)
|
| 1394 |
+
|
| 1395 |
+
if sglext_routed is not None or sglext_details is not None:
|
| 1396 |
+
sglext_chunk = ChatCompletionStreamResponse(
|
| 1397 |
+
id=content["meta_info"]["id"],
|
| 1398 |
+
created=int(time.time()),
|
| 1399 |
+
choices=[], # sglext is at response level
|
| 1400 |
+
model=request.model,
|
| 1401 |
+
sglext=SglExt(
|
| 1402 |
+
routed_experts=sglext_routed,
|
| 1403 |
+
cached_tokens_details=sglext_details,
|
| 1404 |
+
),
|
| 1405 |
+
)
|
| 1406 |
+
yield f"data: {sglext_chunk.model_dump_json()}\n\n"
|
| 1407 |
+
|
| 1408 |
+
# Additional usage chunk
|
| 1409 |
+
if include_usage:
|
| 1410 |
+
# Multimodal tokens are per-prompt (input side), so aggregate
|
| 1411 |
+
# once per prompt (first choice), matching prompt/cached semantics.
|
| 1412 |
+
total_image_tokens = sum(
|
| 1413 |
+
tok for idx, tok in image_tokens.items() if idx % request.n == 0
|
| 1414 |
+
)
|
| 1415 |
+
total_audio_tokens = sum(
|
| 1416 |
+
tok for idx, tok in audio_tokens.items() if idx % request.n == 0
|
| 1417 |
+
)
|
| 1418 |
+
total_video_tokens = sum(
|
| 1419 |
+
tok for idx, tok in video_tokens.items() if idx % request.n == 0
|
| 1420 |
+
)
|
| 1421 |
+
usage = UsageProcessor.calculate_streaming_usage(
|
| 1422 |
+
prompt_tokens,
|
| 1423 |
+
reasoning_tokens,
|
| 1424 |
+
completion_tokens,
|
| 1425 |
+
cached_tokens=cached_tokens,
|
| 1426 |
+
n_choices=request.n,
|
| 1427 |
+
enable_cache_report=self.tokenizer_manager.server_args.enable_cache_report,
|
| 1428 |
+
image_tokens=total_image_tokens,
|
| 1429 |
+
audio_tokens=total_audio_tokens,
|
| 1430 |
+
video_tokens=total_video_tokens,
|
| 1431 |
+
)
|
| 1432 |
+
usage_chunk = ChatCompletionStreamResponse(
|
| 1433 |
+
id=content["meta_info"]["id"],
|
| 1434 |
+
created=int(time.time()),
|
| 1435 |
+
choices=[], # Empty choices array as per OpenAI spec
|
| 1436 |
+
model=request.model,
|
| 1437 |
+
usage=usage,
|
| 1438 |
+
)
|
| 1439 |
+
yield f"data: {usage_chunk.model_dump_json()}\n\n"
|
| 1440 |
+
|
| 1441 |
+
except ValueError as e:
|
| 1442 |
+
if not stream_started:
|
| 1443 |
+
raise
|
| 1444 |
+
error = self.create_streaming_error_response(str(e))
|
| 1445 |
+
yield f"data: {error}\n\n"
|
| 1446 |
+
|
| 1447 |
+
yield "data: [DONE]\n\n"
|
| 1448 |
+
|
| 1449 |
+
async def _handle_non_streaming_request(
|
| 1450 |
+
self,
|
| 1451 |
+
adapted_request: GenerateReqInput,
|
| 1452 |
+
request: ChatCompletionRequest,
|
| 1453 |
+
raw_request: Request,
|
| 1454 |
+
) -> Union[ChatCompletionResponse, ErrorResponse, ORJSONResponse]:
|
| 1455 |
+
"""Handle non-streaming chat completion request"""
|
| 1456 |
+
try:
|
| 1457 |
+
ret = await self.tokenizer_manager.generate_request(
|
| 1458 |
+
adapted_request, raw_request
|
| 1459 |
+
).__anext__()
|
| 1460 |
+
except ValueError as e:
|
| 1461 |
+
return self.create_error_response(str(e))
|
| 1462 |
+
|
| 1463 |
+
if not isinstance(ret, list):
|
| 1464 |
+
ret = [ret]
|
| 1465 |
+
|
| 1466 |
+
response = self._build_chat_response(
|
| 1467 |
+
request,
|
| 1468 |
+
ret,
|
| 1469 |
+
int(time.time()),
|
| 1470 |
+
)
|
| 1471 |
+
|
| 1472 |
+
return response
|
| 1473 |
+
|
| 1474 |
+
def _build_chat_response(
|
| 1475 |
+
self,
|
| 1476 |
+
request: ChatCompletionRequest,
|
| 1477 |
+
ret: List[Dict[str, Any]],
|
| 1478 |
+
created: int,
|
| 1479 |
+
) -> Union[ChatCompletionResponse, ORJSONResponse]:
|
| 1480 |
+
"""Build chat completion response from generation results"""
|
| 1481 |
+
choices = []
|
| 1482 |
+
|
| 1483 |
+
# Build sglext at response level (from first ret_item, as these are per-request)
|
| 1484 |
+
first_ret = ret[0]
|
| 1485 |
+
routed_experts = process_routed_experts_from_ret(first_ret, request)
|
| 1486 |
+
cached_tokens_details = process_cached_tokens_details_from_ret(
|
| 1487 |
+
first_ret, request
|
| 1488 |
+
)
|
| 1489 |
+
response_sglext = None
|
| 1490 |
+
if routed_experts or cached_tokens_details:
|
| 1491 |
+
response_sglext = SglExt(
|
| 1492 |
+
routed_experts=routed_experts,
|
| 1493 |
+
cached_tokens_details=cached_tokens_details,
|
| 1494 |
+
)
|
| 1495 |
+
|
| 1496 |
+
for idx, ret_item in enumerate(ret):
|
| 1497 |
+
# Process logprobs
|
| 1498 |
+
choice_logprobs = None
|
| 1499 |
+
if request.logprobs:
|
| 1500 |
+
choice_logprobs = self._process_response_logprobs(ret_item)
|
| 1501 |
+
|
| 1502 |
+
# Handle hidden states
|
| 1503 |
+
hidden_states = process_hidden_states_from_ret(ret_item, request)
|
| 1504 |
+
|
| 1505 |
+
finish_reason = ret_item["meta_info"]["finish_reason"]
|
| 1506 |
+
|
| 1507 |
+
text = self._decode_response(ret_item)
|
| 1508 |
+
if isinstance(text, ErrorResponse):
|
| 1509 |
+
return ORJSONResponse(content=text.model_dump(), status_code=text.code)
|
| 1510 |
+
|
| 1511 |
+
# Handle reasoning content
|
| 1512 |
+
reasoning_text = None
|
| 1513 |
+
if self.reasoning_parser and request.separate_reasoning:
|
| 1514 |
+
force_reasoning = (
|
| 1515 |
+
self.template_manager.force_reasoning
|
| 1516 |
+
or self._get_reasoning_from_request(request)
|
| 1517 |
+
)
|
| 1518 |
+
try:
|
| 1519 |
+
parser = ReasoningParser(
|
| 1520 |
+
model_type=self.reasoning_parser,
|
| 1521 |
+
stream_reasoning=False,
|
| 1522 |
+
force_reasoning=force_reasoning,
|
| 1523 |
+
request=request,
|
| 1524 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 1525 |
+
)
|
| 1526 |
+
reasoning_text, text = parser.parse_non_stream(text)
|
| 1527 |
+
except Exception as e:
|
| 1528 |
+
logger.error(f"Reasoning parsing error: {e}")
|
| 1529 |
+
return self.create_error_response(
|
| 1530 |
+
"Failed to parse reasoning content",
|
| 1531 |
+
err_type="InternalServerError",
|
| 1532 |
+
status_code=500,
|
| 1533 |
+
)
|
| 1534 |
+
|
| 1535 |
+
# Handle tool calls
|
| 1536 |
+
tool_calls = None
|
| 1537 |
+
if (
|
| 1538 |
+
request.tool_choice != "none"
|
| 1539 |
+
and request.tools
|
| 1540 |
+
and self.tool_call_parser
|
| 1541 |
+
):
|
| 1542 |
+
history_tool_calls_cnt = self._get_history_tool_calls_cnt(request)
|
| 1543 |
+
tool_calls, text, finish_reason = self._process_tool_calls(
|
| 1544 |
+
text,
|
| 1545 |
+
request.tools,
|
| 1546 |
+
finish_reason,
|
| 1547 |
+
request.tool_choice,
|
| 1548 |
+
history_tool_calls_cnt,
|
| 1549 |
+
)
|
| 1550 |
+
|
| 1551 |
+
# Extract prompt_token_ids if requested
|
| 1552 |
+
choice_prompt_token_ids = (
|
| 1553 |
+
ret_item.get("prompt_token_ids")
|
| 1554 |
+
if request.return_prompt_token_ids
|
| 1555 |
+
else None
|
| 1556 |
+
)
|
| 1557 |
+
|
| 1558 |
+
choice_meta_info = (
|
| 1559 |
+
ret_item["meta_info"] if request.return_meta_info else None
|
| 1560 |
+
)
|
| 1561 |
+
# NOTE: content should not be None but empty string to make sure retokenize consistency.
|
| 1562 |
+
reasoning_text, tool_calls = self._get_parsed_response_fields(
|
| 1563 |
+
reasoning_text, tool_calls
|
| 1564 |
+
)
|
| 1565 |
+
|
| 1566 |
+
choice_data = ChatCompletionResponseChoice(
|
| 1567 |
+
index=idx,
|
| 1568 |
+
message=ChatMessage(
|
| 1569 |
+
role="assistant",
|
| 1570 |
+
content=text if text else "",
|
| 1571 |
+
tool_calls=tool_calls,
|
| 1572 |
+
reasoning_content=reasoning_text if reasoning_text else None,
|
| 1573 |
+
),
|
| 1574 |
+
logprobs=choice_logprobs,
|
| 1575 |
+
finish_reason=finish_reason["type"] if finish_reason else None,
|
| 1576 |
+
matched_stop=(
|
| 1577 |
+
finish_reason["matched"]
|
| 1578 |
+
if finish_reason and "matched" in finish_reason
|
| 1579 |
+
else None
|
| 1580 |
+
),
|
| 1581 |
+
hidden_states=hidden_states,
|
| 1582 |
+
prompt_token_ids=choice_prompt_token_ids,
|
| 1583 |
+
meta_info=choice_meta_info,
|
| 1584 |
+
)
|
| 1585 |
+
choices.append(choice_data)
|
| 1586 |
+
|
| 1587 |
+
# Calculate usage. Multimodal tokens are per-prompt (input side), so
|
| 1588 |
+
# aggregate once per prompt (stride by n), matching prompt/cached semantics.
|
| 1589 |
+
image_tokens = sum(
|
| 1590 |
+
ret[i]["meta_info"].get("image_tokens", 0)
|
| 1591 |
+
for i in range(0, len(ret), request.n)
|
| 1592 |
+
)
|
| 1593 |
+
audio_tokens = sum(
|
| 1594 |
+
ret[i]["meta_info"].get("audio_tokens", 0)
|
| 1595 |
+
for i in range(0, len(ret), request.n)
|
| 1596 |
+
)
|
| 1597 |
+
video_tokens = sum(
|
| 1598 |
+
ret[i]["meta_info"].get("video_tokens", 0)
|
| 1599 |
+
for i in range(0, len(ret), request.n)
|
| 1600 |
+
)
|
| 1601 |
+
usage = UsageProcessor.calculate_response_usage(
|
| 1602 |
+
ret,
|
| 1603 |
+
n_choices=request.n,
|
| 1604 |
+
enable_cache_report=self.tokenizer_manager.server_args.enable_cache_report,
|
| 1605 |
+
image_tokens=image_tokens,
|
| 1606 |
+
audio_tokens=audio_tokens,
|
| 1607 |
+
video_tokens=video_tokens,
|
| 1608 |
+
)
|
| 1609 |
+
|
| 1610 |
+
return ChatCompletionResponse(
|
| 1611 |
+
id=ret[0]["meta_info"]["id"],
|
| 1612 |
+
created=created,
|
| 1613 |
+
model=request.model,
|
| 1614 |
+
choices=choices,
|
| 1615 |
+
usage=usage,
|
| 1616 |
+
metadata={"weight_version": ret[0]["meta_info"]["weight_version"]},
|
| 1617 |
+
sglext=response_sglext,
|
| 1618 |
+
)
|
| 1619 |
+
|
| 1620 |
+
def _process_logprobs_tokens(
|
| 1621 |
+
self, logprobs: LogProbs, use_token_index: bool = False
|
| 1622 |
+
) -> List[ChatCompletionTokenLogprob]:
|
| 1623 |
+
"""Common helper to process logprobs tokens for both streaming and non-streaming
|
| 1624 |
+
|
| 1625 |
+
Args:
|
| 1626 |
+
logprobs: LogProbs data from model
|
| 1627 |
+
use_token_index: True for non-streaming (use token_idx), False for streaming (use index 0)
|
| 1628 |
+
"""
|
| 1629 |
+
token_logprobs = []
|
| 1630 |
+
|
| 1631 |
+
for token_idx, (token, logprob) in enumerate(
|
| 1632 |
+
zip(logprobs.tokens, logprobs.token_logprobs)
|
| 1633 |
+
):
|
| 1634 |
+
token_bytes = list(token.encode("utf-8"))
|
| 1635 |
+
top_logprobs = []
|
| 1636 |
+
if logprobs.top_logprobs:
|
| 1637 |
+
# - Non-streaming (use_token_index=True): uses token_idx for full data
|
| 1638 |
+
# - Streaming (use_token_index=False): uses index 0 for pre-sliced data
|
| 1639 |
+
top_logprobs_idx = token_idx if use_token_index else 0
|
| 1640 |
+
for top_token, top_logprob in logprobs.top_logprobs[
|
| 1641 |
+
top_logprobs_idx
|
| 1642 |
+
].items():
|
| 1643 |
+
top_token_bytes = list(top_token.encode("utf-8"))
|
| 1644 |
+
top_logprobs.append(
|
| 1645 |
+
TopLogprob(
|
| 1646 |
+
token=top_token,
|
| 1647 |
+
bytes=top_token_bytes,
|
| 1648 |
+
logprob=top_logprob,
|
| 1649 |
+
)
|
| 1650 |
+
)
|
| 1651 |
+
token_logprobs.append(
|
| 1652 |
+
ChatCompletionTokenLogprob(
|
| 1653 |
+
token=token,
|
| 1654 |
+
bytes=token_bytes,
|
| 1655 |
+
logprob=logprob,
|
| 1656 |
+
top_logprobs=top_logprobs,
|
| 1657 |
+
)
|
| 1658 |
+
)
|
| 1659 |
+
|
| 1660 |
+
return token_logprobs
|
| 1661 |
+
|
| 1662 |
+
def _process_response_logprobs(self, ret_item: Dict[str, Any]) -> ChoiceLogprobs:
|
| 1663 |
+
"""Process logprobs for non-streaming response"""
|
| 1664 |
+
logprobs = to_openai_style_logprobs(
|
| 1665 |
+
output_token_logprobs=ret_item["meta_info"]["output_token_logprobs"],
|
| 1666 |
+
output_top_logprobs=ret_item["meta_info"].get("output_top_logprobs", None),
|
| 1667 |
+
)
|
| 1668 |
+
|
| 1669 |
+
token_logprobs = self._process_logprobs_tokens(logprobs, use_token_index=True)
|
| 1670 |
+
return ChoiceLogprobs(content=token_logprobs)
|
| 1671 |
+
|
| 1672 |
+
def _process_tool_call_id(
|
| 1673 |
+
self,
|
| 1674 |
+
call_item: ToolCallItem,
|
| 1675 |
+
history_tool_calls_cnt: int,
|
| 1676 |
+
) -> str:
|
| 1677 |
+
"""Process for generating a new and unique `tool_call_id`"""
|
| 1678 |
+
if self.tool_call_parser != "kimi_k2":
|
| 1679 |
+
# A simple uuid is sufficient for all models except for Kimi-K2.
|
| 1680 |
+
tool_call_id = f"call_{uuid.uuid4().hex[:24]}"
|
| 1681 |
+
return tool_call_id
|
| 1682 |
+
else:
|
| 1683 |
+
# Align with Kimi-K2 format: functions.{name}:{index}
|
| 1684 |
+
# Kimi-K2 allows multiple tool_calls in one message; SGLang sets call_item.tool_index to the *local* position inside that message.
|
| 1685 |
+
# Therefore, the index must be corrected by using `history_tool_calls_cnt + call_item.tool_index` to ensure globally unique and properly ordered.
|
| 1686 |
+
tool_call_id = f"functions.{call_item.name}:{history_tool_calls_cnt+call_item.tool_index}"
|
| 1687 |
+
logger.debug(
|
| 1688 |
+
f"Process tool call idx, parser: {self.tool_call_parser}, tool_call_id: {tool_call_id}, history_cnt: {history_tool_calls_cnt}"
|
| 1689 |
+
)
|
| 1690 |
+
return tool_call_id
|
| 1691 |
+
|
| 1692 |
+
def _process_tool_calls(
|
| 1693 |
+
self,
|
| 1694 |
+
text: str,
|
| 1695 |
+
tools: List[Any],
|
| 1696 |
+
finish_reason: Dict[str, Any],
|
| 1697 |
+
tool_choice: Optional[Union[str, ToolChoice]] = None,
|
| 1698 |
+
history_tool_calls_cnt: int = 0,
|
| 1699 |
+
) -> ToolCallProcessingResult:
|
| 1700 |
+
"""Process tool calls in the response"""
|
| 1701 |
+
|
| 1702 |
+
is_required = tool_choice == "required" or isinstance(tool_choice, ToolChoice)
|
| 1703 |
+
|
| 1704 |
+
# Try model-specific parser when output is in native format.
|
| 1705 |
+
# For required/named: only use parser when structural_tag was used
|
| 1706 |
+
# as constraint (mirrors the streaming path). For auto: always try.
|
| 1707 |
+
if self.tool_call_parser:
|
| 1708 |
+
parser = FunctionCallParser(
|
| 1709 |
+
tools, self.tool_call_parser, tokenizer=self.tokenizer_manager.tokenizer
|
| 1710 |
+
)
|
| 1711 |
+
should_try_parser = (
|
| 1712 |
+
not is_required or parser.detector.supports_structural_tag()
|
| 1713 |
+
)
|
| 1714 |
+
if should_try_parser and parser.has_tool_call(text):
|
| 1715 |
+
try:
|
| 1716 |
+
text, call_info_list = parser.parse_non_stream(text)
|
| 1717 |
+
if not call_info_list:
|
| 1718 |
+
return ToolCallProcessingResult(None, text, finish_reason)
|
| 1719 |
+
|
| 1720 |
+
tool_calls = []
|
| 1721 |
+
for call_info in call_info_list:
|
| 1722 |
+
tool_id = self._process_tool_call_id(
|
| 1723 |
+
call_info, history_tool_calls_cnt
|
| 1724 |
+
)
|
| 1725 |
+
tool_calls.append(
|
| 1726 |
+
ToolCall(
|
| 1727 |
+
id=tool_id,
|
| 1728 |
+
index=getattr(call_info, "tool_index", None),
|
| 1729 |
+
function=FunctionResponse(
|
| 1730 |
+
name=call_info.name,
|
| 1731 |
+
arguments=call_info.parameters,
|
| 1732 |
+
),
|
| 1733 |
+
)
|
| 1734 |
+
)
|
| 1735 |
+
if finish_reason["type"] == "stop":
|
| 1736 |
+
finish_reason["type"] = "tool_calls"
|
| 1737 |
+
finish_reason["matched"] = None
|
| 1738 |
+
return ToolCallProcessingResult(tool_calls, text, finish_reason)
|
| 1739 |
+
except Exception as e:
|
| 1740 |
+
logger.error(f"Tool call parsing error: {e}")
|
| 1741 |
+
return ToolCallProcessingResult(None, text, finish_reason)
|
| 1742 |
+
|
| 1743 |
+
# json_schema constraint → JSON array output for required/named
|
| 1744 |
+
if is_required:
|
| 1745 |
+
original_finish_type = finish_reason["type"]
|
| 1746 |
+
if finish_reason["type"] == "stop":
|
| 1747 |
+
finish_reason["type"] = "tool_calls"
|
| 1748 |
+
finish_reason["matched"] = None
|
| 1749 |
+
try:
|
| 1750 |
+
tool_call_data = orjson.loads(text)
|
| 1751 |
+
tool_calls = []
|
| 1752 |
+
for i, tool in enumerate(tool_call_data):
|
| 1753 |
+
call_info = ToolCallItem(
|
| 1754 |
+
tool_index=i,
|
| 1755 |
+
name=tool["name"],
|
| 1756 |
+
parameters=json.dumps(tool["parameters"], ensure_ascii=False),
|
| 1757 |
+
)
|
| 1758 |
+
tool_id = self._process_tool_call_id(
|
| 1759 |
+
call_info, history_tool_calls_cnt
|
| 1760 |
+
)
|
| 1761 |
+
tool_calls.append(
|
| 1762 |
+
ToolCall(
|
| 1763 |
+
id=tool_id,
|
| 1764 |
+
index=i,
|
| 1765 |
+
function=FunctionResponse(
|
| 1766 |
+
name=tool["name"],
|
| 1767 |
+
arguments=json.dumps(
|
| 1768 |
+
tool["parameters"], ensure_ascii=False
|
| 1769 |
+
),
|
| 1770 |
+
),
|
| 1771 |
+
)
|
| 1772 |
+
)
|
| 1773 |
+
return ToolCallProcessingResult(tool_calls, "", finish_reason)
|
| 1774 |
+
except Exception as e:
|
| 1775 |
+
logger.error(f"Tool call parsing error: {e}")
|
| 1776 |
+
finish_reason["type"] = original_finish_type
|
| 1777 |
+
return ToolCallProcessingResult(None, text, finish_reason)
|
| 1778 |
+
|
| 1779 |
+
return ToolCallProcessingResult(None, text, finish_reason)
|
| 1780 |
+
|
| 1781 |
+
def _process_streaming_logprobs(
|
| 1782 |
+
self,
|
| 1783 |
+
content: Dict[str, Any],
|
| 1784 |
+
n_prev_token: int,
|
| 1785 |
+
total_output_logprobs: int,
|
| 1786 |
+
) -> ChoiceLogprobs:
|
| 1787 |
+
"""Process logprobs for streaming response"""
|
| 1788 |
+
output_token_logprobs = content["meta_info"]["output_token_logprobs"]
|
| 1789 |
+
output_top_logprobs = content["meta_info"].get("output_top_logprobs", [])
|
| 1790 |
+
if not self.tokenizer_manager.server_args.incremental_streaming_output:
|
| 1791 |
+
output_token_logprobs = output_token_logprobs[
|
| 1792 |
+
n_prev_token:total_output_logprobs
|
| 1793 |
+
]
|
| 1794 |
+
output_top_logprobs = output_top_logprobs[
|
| 1795 |
+
n_prev_token:total_output_logprobs
|
| 1796 |
+
]
|
| 1797 |
+
logprobs = to_openai_style_logprobs(
|
| 1798 |
+
output_token_logprobs=output_token_logprobs,
|
| 1799 |
+
output_top_logprobs=output_top_logprobs,
|
| 1800 |
+
)
|
| 1801 |
+
|
| 1802 |
+
token_logprobs = self._process_logprobs_tokens(logprobs, use_token_index=False)
|
| 1803 |
+
return ChoiceLogprobs(content=token_logprobs)
|
| 1804 |
+
|
| 1805 |
+
def _process_reasoning_stream(
|
| 1806 |
+
self,
|
| 1807 |
+
index: int,
|
| 1808 |
+
delta: str,
|
| 1809 |
+
reasoning_parser_dict: Dict[int, ReasoningParser],
|
| 1810 |
+
content: Dict[str, Any],
|
| 1811 |
+
request: ChatCompletionRequest,
|
| 1812 |
+
finish_reason_type: Optional[str] = None,
|
| 1813 |
+
) -> tuple[Optional[str], str]:
|
| 1814 |
+
"""Process reasoning content in streaming response"""
|
| 1815 |
+
if index not in reasoning_parser_dict:
|
| 1816 |
+
is_force_reasoning = (
|
| 1817 |
+
self.template_manager.force_reasoning
|
| 1818 |
+
or self._get_reasoning_from_request(request)
|
| 1819 |
+
)
|
| 1820 |
+
reasoning_parser_dict[index] = ReasoningParser(
|
| 1821 |
+
self.reasoning_parser,
|
| 1822 |
+
request.stream_reasoning,
|
| 1823 |
+
is_force_reasoning,
|
| 1824 |
+
request,
|
| 1825 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 1826 |
+
)
|
| 1827 |
+
reasoning_parser = reasoning_parser_dict[index]
|
| 1828 |
+
reasoning_text, normal_text = reasoning_parser.parse_stream_chunk(delta)
|
| 1829 |
+
if finish_reason_type is not None and finish_reason_type != "abort":
|
| 1830 |
+
end_reasoning_text, end_normal_text = reasoning_parser.parse_stream_end()
|
| 1831 |
+
if end_reasoning_text:
|
| 1832 |
+
reasoning_text = (reasoning_text or "") + end_reasoning_text
|
| 1833 |
+
if end_normal_text:
|
| 1834 |
+
normal_text = (normal_text or "") + end_normal_text
|
| 1835 |
+
return reasoning_text, normal_text
|
| 1836 |
+
|
| 1837 |
+
def _get_history_tool_calls_cnt(self, request: ChatCompletionRequest) -> int:
|
| 1838 |
+
"""Counts the number of tool calls in the request's message history.
|
| 1839 |
+
|
| 1840 |
+
NOTE: This method is only useful for models that include self-increasing
|
| 1841 |
+
history tool call idx in tool calls id, such as kimi-k2
|
| 1842 |
+
|
| 1843 |
+
Args:
|
| 1844 |
+
request: The chat completion request object.
|
| 1845 |
+
|
| 1846 |
+
Returns:
|
| 1847 |
+
The total number of tool calls in the history, or 0 if not applicable.
|
| 1848 |
+
"""
|
| 1849 |
+
messages = getattr(request, "messages", [])
|
| 1850 |
+
idx = 0
|
| 1851 |
+
for msg in messages:
|
| 1852 |
+
if msg.role == "assistant":
|
| 1853 |
+
tool_calls = getattr(msg, "tool_calls", None)
|
| 1854 |
+
idx += len(list(tool_calls)) if tool_calls is not None else 0 # noqa
|
| 1855 |
+
return idx
|
| 1856 |
+
|
| 1857 |
+
def _patch_reasoning_skip_special_tokens(
|
| 1858 |
+
self, request: ChatCompletionRequest
|
| 1859 |
+
) -> None:
|
| 1860 |
+
"""Keep parser-specific reasoning markers in the decoded text.
|
| 1861 |
+
|
| 1862 |
+
Some reasoning parsers rely on special-token delimiters that would be
|
| 1863 |
+
removed during detokenization when ``skip_special_tokens=True``.
|
| 1864 |
+
"""
|
| 1865 |
+
if self.reasoning_parser == "apertus2509":
|
| 1866 |
+
request.skip_special_tokens = False
|
| 1867 |
+
|
| 1868 |
+
if (
|
| 1869 |
+
self.reasoning_parser in ["mistral"]
|
| 1870 |
+
and request.reasoning_effort is not None
|
| 1871 |
+
and request.reasoning_effort != "none"
|
| 1872 |
+
):
|
| 1873 |
+
request.skip_special_tokens = False
|
| 1874 |
+
elif self.reasoning_parser == "inkling":
|
| 1875 |
+
request.skip_special_tokens = False
|
| 1876 |
+
|
| 1877 |
+
def wrap_reasoning_history(self, reasoning_text: str) -> str:
|
| 1878 |
+
"""Wrap prior-turn reasoning in the detector's own start/end tokens.
|
| 1879 |
+
|
| 1880 |
+
Pulling the delimiters from the detector keeps adapters in lockstep
|
| 1881 |
+
with any future parser that ships non-``<think>`` markers — Mistral's
|
| 1882 |
+
``[THINK]``, Gemma4's ``think_start_self_label = "thought\\n"``, etc.
|
| 1883 |
+
Falling back to a plain string is unsafe: it would let prior
|
| 1884 |
+
thinking text reach a non-reasoning model as ordinary assistant
|
| 1885 |
+
content, so the caller must surface this state, not paper over it.
|
| 1886 |
+
"""
|
| 1887 |
+
if self._reasoning_detector is None:
|
| 1888 |
+
raise ValueError(
|
| 1889 |
+
"Cannot rewrap thinking history: no reasoning detector is "
|
| 1890 |
+
"configured for this model"
|
| 1891 |
+
)
|
| 1892 |
+
d = self._reasoning_detector
|
| 1893 |
+
return (
|
| 1894 |
+
f"{d.think_start_token}{d.think_start_self_label}"
|
| 1895 |
+
f"{reasoning_text}\n{d.think_end_token}"
|
| 1896 |
+
)
|
| 1897 |
+
|
| 1898 |
+
def _reasoning_default_mode(self) -> Optional[str]:
|
| 1899 |
+
if self._reasoning_detector is None:
|
| 1900 |
+
return None
|
| 1901 |
+
return self._reasoning_detector.reasoning_default
|
| 1902 |
+
|
| 1903 |
+
def _get_reasoning_toggle_param(self) -> Optional[str]:
|
| 1904 |
+
"""Resolve the chat-template kwarg that toggles reasoning, if any."""
|
| 1905 |
+
config = self.template_manager.reasoning_config
|
| 1906 |
+
if config is not None:
|
| 1907 |
+
return config.toggle_param
|
| 1908 |
+
|
| 1909 |
+
mode = self._reasoning_default_mode()
|
| 1910 |
+
if mode in ("thinking", "enable_thinking"):
|
| 1911 |
+
return mode
|
| 1912 |
+
if mode in ("explicit_thinking", "explicit_enable_thinking"):
|
| 1913 |
+
return mode.replace("explicit_", "")
|
| 1914 |
+
return None
|
| 1915 |
+
|
| 1916 |
+
def apply_reasoning_enabled(
|
| 1917 |
+
self, request: ChatCompletionRequest, enabled: bool
|
| 1918 |
+
) -> None:
|
| 1919 |
+
"""Force the request into the requested reasoning-on/off mode.
|
| 1920 |
+
|
| 1921 |
+
Mirrors the read-side logic in ``_get_reasoning_from_request``;
|
| 1922 |
+
the two must stay in sync. Always-on models cannot be disabled,
|
| 1923 |
+
so explicit ``enabled=False`` raises rather than silently leaving
|
| 1924 |
+
reasoning on.
|
| 1925 |
+
"""
|
| 1926 |
+
if not self.reasoning_parser:
|
| 1927 |
+
if enabled:
|
| 1928 |
+
raise ValueError(
|
| 1929 |
+
"Anthropic thinking is not supported for models without "
|
| 1930 |
+
"a reasoning parser"
|
| 1931 |
+
)
|
| 1932 |
+
return
|
| 1933 |
+
|
| 1934 |
+
if self.reasoning_parser == "hunyuan":
|
| 1935 |
+
request.reasoning_effort = "medium" if enabled else "no_think"
|
| 1936 |
+
return
|
| 1937 |
+
|
| 1938 |
+
config = self.template_manager.reasoning_config
|
| 1939 |
+
is_mistral = (config is not None and config.special_case == "mistral") or (
|
| 1940 |
+
config is None and self._reasoning_default_mode() == "mistral"
|
| 1941 |
+
)
|
| 1942 |
+
if is_mistral:
|
| 1943 |
+
request.reasoning_effort = "medium" if enabled else "none"
|
| 1944 |
+
return
|
| 1945 |
+
|
| 1946 |
+
is_always_on = (config is not None and config.special_case == "always") or (
|
| 1947 |
+
config is None and self._reasoning_default_mode() == "always"
|
| 1948 |
+
)
|
| 1949 |
+
if is_always_on:
|
| 1950 |
+
if not enabled:
|
| 1951 |
+
raise ValueError(
|
| 1952 |
+
f"Reasoning parser '{self.reasoning_parser}' is always-on "
|
| 1953 |
+
f"and cannot be disabled via Anthropic thinking"
|
| 1954 |
+
)
|
| 1955 |
+
return
|
| 1956 |
+
|
| 1957 |
+
toggle_param = self._get_reasoning_toggle_param()
|
| 1958 |
+
# The read side (``_get_reasoning_from_request``) returns False
|
| 1959 |
+
# whenever ``config.toggle_param is None`` OR
|
| 1960 |
+
# ``config.default_enabled is None``. The write side must mirror
|
| 1961 |
+
# both conditions: if ``default_enabled`` is unset we cannot
|
| 1962 |
+
# actually honor an ``enabled=True`` request even when the toggle
|
| 1963 |
+
# name itself is resolvable, so writing the kwarg would set up the
|
| 1964 |
+
# template to emit reasoning tokens while the parser ignores them
|
| 1965 |
+
# (literal ``<think>`` markers leak into the assistant text).
|
| 1966 |
+
config = self.template_manager.reasoning_config
|
| 1967 |
+
read_side_supported = toggle_param is not None and (
|
| 1968 |
+
config is None or config.default_enabled is not None
|
| 1969 |
+
)
|
| 1970 |
+
if not read_side_supported:
|
| 1971 |
+
if not enabled:
|
| 1972 |
+
return
|
| 1973 |
+
raise ValueError(
|
| 1974 |
+
f"Anthropic thinking is not supported for reasoning parser "
|
| 1975 |
+
f"'{self.reasoning_parser}'"
|
| 1976 |
+
)
|
| 1977 |
+
|
| 1978 |
+
chat_template_kwargs = dict(request.chat_template_kwargs or {})
|
| 1979 |
+
chat_template_kwargs[toggle_param] = enabled
|
| 1980 |
+
request.chat_template_kwargs = chat_template_kwargs
|
| 1981 |
+
|
| 1982 |
+
def _get_reasoning_from_request(self, request: ChatCompletionRequest) -> bool:
|
| 1983 |
+
"""Determine whether reasoning mode should be enabled for this request.
|
| 1984 |
+
|
| 1985 |
+
NOTE: This is predefined based on model's chat template
|
| 1986 |
+
"""
|
| 1987 |
+
if not self.reasoning_parser:
|
| 1988 |
+
return False
|
| 1989 |
+
|
| 1990 |
+
if self.reasoning_parser == "minimax-m3":
|
| 1991 |
+
# M3 template prefills <mm:think> for thinking_mode=enabled, so it never
|
| 1992 |
+
# appears in output and reasoning must be forced. Mirrors reasoning_parser.py.
|
| 1993 |
+
return (request.chat_template_kwargs or {}).get(
|
| 1994 |
+
"thinking_mode"
|
| 1995 |
+
) == "enabled"
|
| 1996 |
+
|
| 1997 |
+
if self.reasoning_parser == "hunyuan":
|
| 1998 |
+
# Hy3-preview template emits no <think> when reasoning_effort is
|
| 1999 |
+
# "no_think" / "none" / unset; forcing reasoning would route all
|
| 2000 |
+
# output into reasoning_content.
|
| 2001 |
+
return request.reasoning_effort not in (None, "none", "no_think")
|
| 2002 |
+
|
| 2003 |
+
config = self.template_manager.reasoning_config
|
| 2004 |
+
if config is None:
|
| 2005 |
+
# Fallback to parser-level defaults when template toggle config
|
| 2006 |
+
# cannot be inferred (e.g., parser-only <think> templates).
|
| 2007 |
+
mode = (
|
| 2008 |
+
self._reasoning_detector.reasoning_default
|
| 2009 |
+
if self._reasoning_detector is not None
|
| 2010 |
+
else None
|
| 2011 |
+
)
|
| 2012 |
+
if mode is None:
|
| 2013 |
+
return False
|
| 2014 |
+
if mode == "always":
|
| 2015 |
+
return True
|
| 2016 |
+
if mode == "mistral":
|
| 2017 |
+
return (
|
| 2018 |
+
request.reasoning_effort is not None
|
| 2019 |
+
and request.reasoning_effort != "none"
|
| 2020 |
+
)
|
| 2021 |
+
if mode in ("thinking", "enable_thinking"):
|
| 2022 |
+
return (
|
| 2023 |
+
not request.chat_template_kwargs
|
| 2024 |
+
or request.chat_template_kwargs.get(mode) is not False
|
| 2025 |
+
)
|
| 2026 |
+
if mode in ("explicit_thinking", "explicit_enable_thinking"):
|
| 2027 |
+
toggle = mode.replace("explicit_", "")
|
| 2028 |
+
return (
|
| 2029 |
+
request.chat_template_kwargs is not None
|
| 2030 |
+
and request.chat_template_kwargs.get(toggle) is True
|
| 2031 |
+
)
|
| 2032 |
+
logger.warning(
|
| 2033 |
+
"Unknown reasoning_default mode '%s', defaulting to reasoning disabled",
|
| 2034 |
+
mode,
|
| 2035 |
+
)
|
| 2036 |
+
return False
|
| 2037 |
+
|
| 2038 |
+
if config.special_case == "always":
|
| 2039 |
+
return True
|
| 2040 |
+
|
| 2041 |
+
if config.special_case == "mistral":
|
| 2042 |
+
return (
|
| 2043 |
+
request.reasoning_effort is not None
|
| 2044 |
+
and request.reasoning_effort != "none"
|
| 2045 |
+
)
|
| 2046 |
+
|
| 2047 |
+
if config.toggle_param is None or config.default_enabled is None:
|
| 2048 |
+
return False
|
| 2049 |
+
|
| 2050 |
+
if config.default_enabled:
|
| 2051 |
+
return (
|
| 2052 |
+
not request.chat_template_kwargs
|
| 2053 |
+
or request.chat_template_kwargs.get(config.toggle_param) is not False
|
| 2054 |
+
)
|
| 2055 |
+
return (
|
| 2056 |
+
request.chat_template_kwargs is not None
|
| 2057 |
+
and request.chat_template_kwargs.get(config.toggle_param) is True
|
| 2058 |
+
)
|
| 2059 |
+
|
| 2060 |
+
async def _process_tool_call_stream(
|
| 2061 |
+
self,
|
| 2062 |
+
index: int,
|
| 2063 |
+
delta: str,
|
| 2064 |
+
parser_dict: Dict[int, FunctionCallParser],
|
| 2065 |
+
content: Dict[str, Any],
|
| 2066 |
+
request: ChatCompletionRequest,
|
| 2067 |
+
has_tool_calls: Dict[int, bool],
|
| 2068 |
+
continuous_usage_stats: bool = False,
|
| 2069 |
+
):
|
| 2070 |
+
"""Process tool calls in streaming response"""
|
| 2071 |
+
if index not in parser_dict:
|
| 2072 |
+
is_required = request.tool_choice == "required" or isinstance(
|
| 2073 |
+
request.tool_choice, ToolChoice
|
| 2074 |
+
)
|
| 2075 |
+
# For required/named tool choice: use JsonArrayParser when the
|
| 2076 |
+
# constrained output is plain JSON (detector doesn't support
|
| 2077 |
+
# structural_tag or no parser configured). Use FunctionCallParser
|
| 2078 |
+
# only when the detector supports structural_tag and will produce
|
| 2079 |
+
# native format output.
|
| 2080 |
+
if is_required:
|
| 2081 |
+
use_native_parser = False
|
| 2082 |
+
if self.tool_call_parser:
|
| 2083 |
+
probe = FunctionCallParser(
|
| 2084 |
+
tools=request.tools,
|
| 2085 |
+
tool_call_parser=self.tool_call_parser,
|
| 2086 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 2087 |
+
)
|
| 2088 |
+
use_native_parser = probe.detector.supports_structural_tag()
|
| 2089 |
+
if use_native_parser:
|
| 2090 |
+
parser_dict[index] = probe
|
| 2091 |
+
else:
|
| 2092 |
+
parser_dict[index] = JsonArrayParser()
|
| 2093 |
+
else:
|
| 2094 |
+
parser_dict[index] = FunctionCallParser(
|
| 2095 |
+
tools=request.tools,
|
| 2096 |
+
tool_call_parser=self.tool_call_parser,
|
| 2097 |
+
tokenizer=self.tokenizer_manager.tokenizer,
|
| 2098 |
+
)
|
| 2099 |
+
|
| 2100 |
+
parser = parser_dict[index]
|
| 2101 |
+
|
| 2102 |
+
# Handle both FunctionCallParser and JsonArrayParser
|
| 2103 |
+
if isinstance(parser, JsonArrayParser):
|
| 2104 |
+
result = parser.parse_streaming_increment(delta, request.tools)
|
| 2105 |
+
normal_text, calls = result.normal_text, result.calls
|
| 2106 |
+
else:
|
| 2107 |
+
normal_text, calls = parser.parse_stream_chunk(delta)
|
| 2108 |
+
|
| 2109 |
+
# Yield normal text
|
| 2110 |
+
if normal_text:
|
| 2111 |
+
choice_data = ChatCompletionResponseStreamChoice(
|
| 2112 |
+
index=index,
|
| 2113 |
+
delta=DeltaMessage(content=normal_text),
|
| 2114 |
+
finish_reason=None,
|
| 2115 |
+
)
|
| 2116 |
+
chunk = ChatCompletionStreamResponse(
|
| 2117 |
+
id=content["meta_info"]["id"],
|
| 2118 |
+
created=int(time.time()),
|
| 2119 |
+
choices=[choice_data],
|
| 2120 |
+
model=request.model,
|
| 2121 |
+
)
|
| 2122 |
+
|
| 2123 |
+
# Add usage stats if continuous_usage_stats is enabled
|
| 2124 |
+
if continuous_usage_stats:
|
| 2125 |
+
prompt_tokens = content["meta_info"].get("prompt_tokens", 0)
|
| 2126 |
+
completion_tokens = content["meta_info"].get("completion_tokens", 0)
|
| 2127 |
+
reasoning_tokens = content["meta_info"].get("reasoning_tokens", 0)
|
| 2128 |
+
chunk.usage = UsageProcessor.calculate_token_usage(
|
| 2129 |
+
prompt_tokens=prompt_tokens,
|
| 2130 |
+
completion_tokens=completion_tokens,
|
| 2131 |
+
reasoning_tokens=reasoning_tokens,
|
| 2132 |
+
cached_tokens=self._continuous_usage_cached_details(content),
|
| 2133 |
+
)
|
| 2134 |
+
|
| 2135 |
+
yield f"data: {chunk.model_dump_json()}\n\n"
|
| 2136 |
+
|
| 2137 |
+
# Yield tool calls
|
| 2138 |
+
history_tool_calls_cnt = self._get_history_tool_calls_cnt(request)
|
| 2139 |
+
for call_item in calls:
|
| 2140 |
+
# Mark that this choice has tool calls
|
| 2141 |
+
has_tool_calls[index] = True
|
| 2142 |
+
|
| 2143 |
+
# Tool call ID should be generated only once per tool call
|
| 2144 |
+
if call_item.name:
|
| 2145 |
+
# First chunk: include ID and function name
|
| 2146 |
+
tool_call_id = self._process_tool_call_id(
|
| 2147 |
+
call_item, history_tool_calls_cnt
|
| 2148 |
+
)
|
| 2149 |
+
function_name = call_item.name
|
| 2150 |
+
else:
|
| 2151 |
+
# Subsequent chunks: null ID and name for argument deltas
|
| 2152 |
+
tool_call_id = None
|
| 2153 |
+
function_name = None
|
| 2154 |
+
|
| 2155 |
+
tool_call = ToolCall(
|
| 2156 |
+
id=tool_call_id,
|
| 2157 |
+
index=call_item.tool_index,
|
| 2158 |
+
function=FunctionResponse(
|
| 2159 |
+
name=function_name,
|
| 2160 |
+
arguments=call_item.parameters,
|
| 2161 |
+
),
|
| 2162 |
+
)
|
| 2163 |
+
|
| 2164 |
+
choice_data = ChatCompletionResponseStreamChoice(
|
| 2165 |
+
index=index,
|
| 2166 |
+
delta=DeltaMessage(tool_calls=[tool_call]),
|
| 2167 |
+
finish_reason=None,
|
| 2168 |
+
)
|
| 2169 |
+
chunk = ChatCompletionStreamResponse(
|
| 2170 |
+
id=content["meta_info"]["id"],
|
| 2171 |
+
created=int(time.time()),
|
| 2172 |
+
choices=[choice_data],
|
| 2173 |
+
model=request.model,
|
| 2174 |
+
)
|
| 2175 |
+
|
| 2176 |
+
# Add usage stats if continuous_usage_stats is enabled
|
| 2177 |
+
if continuous_usage_stats:
|
| 2178 |
+
prompt_tokens = content["meta_info"].get("prompt_tokens", 0)
|
| 2179 |
+
completion_tokens = content["meta_info"].get("completion_tokens", 0)
|
| 2180 |
+
reasoning_tokens = content["meta_info"].get("reasoning_tokens", 0)
|
| 2181 |
+
chunk.usage = UsageProcessor.calculate_token_usage(
|
| 2182 |
+
prompt_tokens=prompt_tokens,
|
| 2183 |
+
completion_tokens=completion_tokens,
|
| 2184 |
+
reasoning_tokens=reasoning_tokens,
|
| 2185 |
+
cached_tokens=self._continuous_usage_cached_details(content),
|
| 2186 |
+
)
|
| 2187 |
+
|
| 2188 |
+
yield f"data: {chunk.model_dump_json()}\n\n"
|
| 2189 |
+
|
| 2190 |
+
def _check_for_unstreamed_tool_args(
|
| 2191 |
+
self,
|
| 2192 |
+
parser: Union[FunctionCallParser, JsonArrayParser],
|
| 2193 |
+
content: Dict[str, Any],
|
| 2194 |
+
request: ChatCompletionRequest,
|
| 2195 |
+
index: int,
|
| 2196 |
+
) -> Optional[str]:
|
| 2197 |
+
"""
|
| 2198 |
+
Check for any remaining tool call arguments that need to be streamed
|
| 2199 |
+
when generation finishes. This ensures tool calls are properly completed
|
| 2200 |
+
even if the model generates the final arguments in the last chunk.
|
| 2201 |
+
"""
|
| 2202 |
+
# Get the detector - either from FunctionCallParser or directly if json detector
|
| 2203 |
+
detector = parser.detector if hasattr(parser, "detector") else parser
|
| 2204 |
+
|
| 2205 |
+
# Only check if we have tool calls and the detector has tracked data
|
| 2206 |
+
if (
|
| 2207 |
+
not hasattr(detector, "prev_tool_call_arr")
|
| 2208 |
+
or not detector.prev_tool_call_arr
|
| 2209 |
+
):
|
| 2210 |
+
return None
|
| 2211 |
+
|
| 2212 |
+
if (
|
| 2213 |
+
not hasattr(detector, "streamed_args_for_tool")
|
| 2214 |
+
or not detector.streamed_args_for_tool
|
| 2215 |
+
):
|
| 2216 |
+
return None
|
| 2217 |
+
|
| 2218 |
+
# Get the last tool call that was being processed
|
| 2219 |
+
tool_index = len(detector.prev_tool_call_arr) - 1
|
| 2220 |
+
if tool_index < 0 or tool_index >= len(detector.streamed_args_for_tool):
|
| 2221 |
+
return None
|
| 2222 |
+
|
| 2223 |
+
# Get expected vs actual arguments
|
| 2224 |
+
expected_args = detector.prev_tool_call_arr[tool_index].get("arguments", {})
|
| 2225 |
+
if isinstance(expected_args, str):
|
| 2226 |
+
expected_call = expected_args
|
| 2227 |
+
else:
|
| 2228 |
+
expected_call = json.dumps(expected_args, ensure_ascii=False)
|
| 2229 |
+
actual_call = detector.streamed_args_for_tool[tool_index]
|
| 2230 |
+
|
| 2231 |
+
# Check if there are remaining arguments to send
|
| 2232 |
+
remaining_call = (
|
| 2233 |
+
expected_call[len(actual_call) :]
|
| 2234 |
+
if expected_call.startswith(actual_call)
|
| 2235 |
+
else ""
|
| 2236 |
+
)
|
| 2237 |
+
|
| 2238 |
+
if remaining_call:
|
| 2239 |
+
# Create tool call chunk with remaining arguments
|
| 2240 |
+
tool_call = ToolCall(
|
| 2241 |
+
id=None, # No ID for argument deltas
|
| 2242 |
+
index=tool_index,
|
| 2243 |
+
function=FunctionResponse(
|
| 2244 |
+
name=None, # No name for argument deltas
|
| 2245 |
+
arguments=remaining_call,
|
| 2246 |
+
),
|
| 2247 |
+
)
|
| 2248 |
+
|
| 2249 |
+
choice_data = ChatCompletionResponseStreamChoice(
|
| 2250 |
+
index=index,
|
| 2251 |
+
delta=DeltaMessage(tool_calls=[tool_call]),
|
| 2252 |
+
finish_reason=None, # Don't send finish_reason with this chunk
|
| 2253 |
+
)
|
| 2254 |
+
|
| 2255 |
+
chunk = ChatCompletionStreamResponse(
|
| 2256 |
+
id=content["meta_info"]["id"],
|
| 2257 |
+
created=int(time.time()),
|
| 2258 |
+
choices=[choice_data],
|
| 2259 |
+
model=request.model,
|
| 2260 |
+
)
|
| 2261 |
+
|
| 2262 |
+
return f"data: {chunk.model_dump_json()}\n\n"
|
| 2263 |
+
|
| 2264 |
+
return None
|
sglang_patch/srt/function_call/function_call_parser.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import logging
|
| 3 |
+
from typing import Dict, List, Literal, Optional, Set, Tuple, Type, Union
|
| 4 |
+
|
| 5 |
+
from sglang.srt.entrypoints.openai.protocol import (
|
| 6 |
+
LegacyStructuralTagResponseFormat,
|
| 7 |
+
StructuralTagResponseFormat,
|
| 8 |
+
StructuresResponseFormat,
|
| 9 |
+
Tool,
|
| 10 |
+
ToolCallConstraint,
|
| 11 |
+
ToolChoice,
|
| 12 |
+
)
|
| 13 |
+
from sglang.srt.environ import ToolStrictLevel, envs
|
| 14 |
+
from sglang.srt.function_call.apertus2509_detector import Apertus2509Detector
|
| 15 |
+
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
| 16 |
+
from sglang.srt.function_call.cohere_command4_detector import CohereCommand4Detector
|
| 17 |
+
from sglang.srt.function_call.core_types import ToolCallItem
|
| 18 |
+
from sglang.srt.function_call.deepseekv3_detector import DeepSeekV3Detector
|
| 19 |
+
from sglang.srt.function_call.deepseekv4_detector import DeepSeekV4Detector
|
| 20 |
+
from sglang.srt.function_call.deepseekv31_detector import DeepSeekV31Detector
|
| 21 |
+
from sglang.srt.function_call.deepseekv32_detector import DeepSeekV32Detector
|
| 22 |
+
from sglang.srt.function_call.gemma4_detector import Gemma4Detector
|
| 23 |
+
from sglang.srt.function_call.gigachat3_detector import GigaChat3Detector
|
| 24 |
+
from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
|
| 25 |
+
from sglang.srt.function_call.glm47_moe_detector import Glm47MoeDetector
|
| 26 |
+
from sglang.srt.function_call.gpt_oss_detector import GptOssDetector
|
| 27 |
+
from sglang.srt.function_call.hermes_detector import HermesDetector
|
| 28 |
+
from sglang.srt.function_call.hunyuan_detector import HunyuanDetector
|
| 29 |
+
from sglang.srt.function_call.inkling_detector import InklingDetector
|
| 30 |
+
from sglang.srt.function_call.internlm_detector import InternlmDetector
|
| 31 |
+
from sglang.srt.function_call.kimik2_detector import KimiK2Detector
|
| 32 |
+
from sglang.srt.function_call.lfm2_detector import Lfm2Detector
|
| 33 |
+
from sglang.srt.function_call.llama32_detector import Llama32Detector
|
| 34 |
+
from sglang.srt.function_call.mimo_detector import MiMoDetector
|
| 35 |
+
from sglang.srt.function_call.minicpm5_detector import MiniCPM5Detector
|
| 36 |
+
from sglang.srt.function_call.minimax_m2 import MinimaxM2Detector
|
| 37 |
+
from sglang.srt.function_call.minimax_m3 import MinimaxM3Detector
|
| 38 |
+
from sglang.srt.function_call.mistral_detector import MistralDetector
|
| 39 |
+
from sglang.srt.function_call.poolside_v1_detector import PoolsideV1Detector
|
| 40 |
+
from sglang.srt.function_call.pythonic_detector import PythonicDetector
|
| 41 |
+
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
|
| 42 |
+
from sglang.srt.function_call.qwen25_detector import Qwen25Detector
|
| 43 |
+
from sglang.srt.function_call.step3_detector import Step3Detector
|
| 44 |
+
from sglang.srt.function_call.trinity_detector import TrinityDetector
|
| 45 |
+
from sglang.srt.function_call.utils import (
|
| 46 |
+
_get_tool_schema_defs,
|
| 47 |
+
get_json_schema_constraint,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
logger = logging.getLogger(__name__)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class FunctionCallParser:
|
| 54 |
+
"""
|
| 55 |
+
Parser for function/tool calls in model outputs.
|
| 56 |
+
|
| 57 |
+
This class handles both streaming and non-streaming parsing of function calls using a detector.
|
| 58 |
+
In streaming scenarios, each time new_text is received, it calls detector.parse_streaming_increment
|
| 59 |
+
and returns the resulting normal_text and calls to the upper layer (or SSE).
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
ToolCallParserEnum: Dict[str, Type[BaseFormatDetector]] = {
|
| 63 |
+
"apertus2509": Apertus2509Detector,
|
| 64 |
+
"cohere_command4": CohereCommand4Detector,
|
| 65 |
+
"deepseekv3": DeepSeekV3Detector,
|
| 66 |
+
"deepseekv31": DeepSeekV31Detector,
|
| 67 |
+
"deepseekv32": DeepSeekV32Detector,
|
| 68 |
+
"agnes": DeepSeekV4Detector, # alias: Agnes uses the deepseekv4 format
|
| 69 |
+
"deepseekv4": DeepSeekV4Detector,
|
| 70 |
+
"glm": Glm4MoeDetector,
|
| 71 |
+
"glm45": Glm4MoeDetector,
|
| 72 |
+
"glm47": Glm47MoeDetector,
|
| 73 |
+
"gpt-oss": GptOssDetector,
|
| 74 |
+
"kimi_k2": KimiK2Detector,
|
| 75 |
+
"lfm2": Lfm2Detector,
|
| 76 |
+
"llama3": Llama32Detector,
|
| 77 |
+
"mimo": MiMoDetector,
|
| 78 |
+
"minicpm5": MiniCPM5Detector,
|
| 79 |
+
"mistral": MistralDetector,
|
| 80 |
+
"poolside_v1": PoolsideV1Detector,
|
| 81 |
+
"pythonic": PythonicDetector,
|
| 82 |
+
"qwen": Qwen25Detector,
|
| 83 |
+
"qwen25": Qwen25Detector,
|
| 84 |
+
"qwen3_coder": Qwen3CoderDetector,
|
| 85 |
+
"step3": Step3Detector,
|
| 86 |
+
"step3p5": Qwen3CoderDetector,
|
| 87 |
+
"minimax-m2": MinimaxM2Detector,
|
| 88 |
+
"minimax-m3": MinimaxM3Detector,
|
| 89 |
+
"trinity": TrinityDetector,
|
| 90 |
+
"interns1": InternlmDetector,
|
| 91 |
+
"hermes": HermesDetector,
|
| 92 |
+
"hunyuan": HunyuanDetector,
|
| 93 |
+
"gigachat3": GigaChat3Detector,
|
| 94 |
+
"gemma4": Gemma4Detector,
|
| 95 |
+
"inkling": InklingDetector,
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
def __init__(self, tools: List[Tool], tool_call_parser: str, tokenizer=None):
|
| 99 |
+
detector_class = self.ToolCallParserEnum.get(tool_call_parser)
|
| 100 |
+
if detector_class:
|
| 101 |
+
kwargs = {}
|
| 102 |
+
if tokenizer is not None:
|
| 103 |
+
sig = inspect.signature(detector_class)
|
| 104 |
+
if "tokenizer" in sig.parameters:
|
| 105 |
+
kwargs["tokenizer"] = tokenizer
|
| 106 |
+
detector = detector_class(**kwargs)
|
| 107 |
+
else:
|
| 108 |
+
raise ValueError(f"Unsupported tool_call_parser: {tool_call_parser}")
|
| 109 |
+
|
| 110 |
+
self.detector = detector
|
| 111 |
+
self.tools = tools
|
| 112 |
+
self.tool_strict_level = envs.SGLANG_TOOL_STRICT_LEVEL.get()
|
| 113 |
+
|
| 114 |
+
def has_tool_call(self, text: str) -> bool:
|
| 115 |
+
"""
|
| 116 |
+
Check if the given text contains a tool call in the format supported by this parser.
|
| 117 |
+
This delegates to the detector's implementation.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
text: The text to check for tool calls
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
True if the text contains a tool call, False otherwise
|
| 124 |
+
"""
|
| 125 |
+
if not self.tools:
|
| 126 |
+
return False
|
| 127 |
+
return self.detector.has_tool_call(text)
|
| 128 |
+
|
| 129 |
+
def parse_non_stream(self, full_text: str) -> Tuple[str, list[ToolCallItem]]:
|
| 130 |
+
"""
|
| 131 |
+
One-time parsing of the full text to extract tool calls.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
full_text: The complete text to parse
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
A tuple containing:
|
| 138 |
+
- The remaining text after parsing that was not consumed by the detector (can be treated as normal text)
|
| 139 |
+
- A list of tool calls parsed from the text
|
| 140 |
+
"""
|
| 141 |
+
if not self.tools:
|
| 142 |
+
return full_text, []
|
| 143 |
+
has_tool_call = self.detector.has_tool_call(full_text)
|
| 144 |
+
parsed_result = self.detector.detect_and_parse(full_text, self.tools)
|
| 145 |
+
tool_call_list = parsed_result.calls
|
| 146 |
+
if tool_call_list or has_tool_call:
|
| 147 |
+
return parsed_result.normal_text, tool_call_list
|
| 148 |
+
else:
|
| 149 |
+
return full_text, []
|
| 150 |
+
|
| 151 |
+
def parse_stream_chunk(self, chunk_text: str) -> Tuple[str, list[ToolCallItem]]:
|
| 152 |
+
"""
|
| 153 |
+
Streaming incremental parsing of chunks of text as they arrive.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
chunk_text: The new chunk of text to parse
|
| 157 |
+
|
| 158 |
+
Returns:
|
| 159 |
+
A tuple containing:
|
| 160 |
+
- The normal text that should be displayed to the user
|
| 161 |
+
- A list of tool calls parsed from the chunk
|
| 162 |
+
"""
|
| 163 |
+
if not self.tools:
|
| 164 |
+
return chunk_text, []
|
| 165 |
+
final_normal_text = ""
|
| 166 |
+
final_calls = []
|
| 167 |
+
|
| 168 |
+
sp_result = self.detector.parse_streaming_increment(chunk_text, self.tools)
|
| 169 |
+
if sp_result.normal_text:
|
| 170 |
+
final_normal_text = sp_result.normal_text
|
| 171 |
+
if sp_result.calls:
|
| 172 |
+
final_calls.extend(sp_result.calls)
|
| 173 |
+
final_normal_text = sp_result.normal_text
|
| 174 |
+
|
| 175 |
+
return final_normal_text, final_calls
|
| 176 |
+
|
| 177 |
+
def get_legacy_structural_tag(
|
| 178 |
+
self, at_least_one: bool = False
|
| 179 |
+
) -> StructuralTagResponseFormat:
|
| 180 |
+
"""
|
| 181 |
+
Generate a structural tag response format for all available tools.
|
| 182 |
+
|
| 183 |
+
This creates the necessary structural tags that guide the model's output format.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
at_least_one: If True, the grammar forces at least one tool call
|
| 187 |
+
(no free text allowed). Used for required/named tool_choice.
|
| 188 |
+
|
| 189 |
+
Raises:
|
| 190 |
+
ValueError: If tools have conflicting $defs schemas.
|
| 191 |
+
"""
|
| 192 |
+
# Validate $defs consistency before building structural tags
|
| 193 |
+
_get_tool_schema_defs(self.tools)
|
| 194 |
+
|
| 195 |
+
tool_structures: List[StructuresResponseFormat] = list()
|
| 196 |
+
tool_trigger_set: Set[str] = set()
|
| 197 |
+
|
| 198 |
+
get_structure_info = self.detector.structure_info()
|
| 199 |
+
for tool in self.tools:
|
| 200 |
+
function = tool.function
|
| 201 |
+
name = function.name
|
| 202 |
+
assert name is not None
|
| 203 |
+
info = get_structure_info(name)
|
| 204 |
+
|
| 205 |
+
# accept all if not strict, otherwise only accept the schema
|
| 206 |
+
is_strict = (
|
| 207 |
+
function.strict or self.tool_strict_level >= ToolStrictLevel.PARAMETER
|
| 208 |
+
)
|
| 209 |
+
schema = function.parameters if is_strict else {}
|
| 210 |
+
|
| 211 |
+
tool_structures.append(
|
| 212 |
+
StructuresResponseFormat(
|
| 213 |
+
begin=info.begin,
|
| 214 |
+
schema=schema or {}, # type: ignore
|
| 215 |
+
end=info.end,
|
| 216 |
+
)
|
| 217 |
+
)
|
| 218 |
+
tool_trigger_set.add(info.trigger)
|
| 219 |
+
|
| 220 |
+
# TODO(dark): move this into new structural tag format
|
| 221 |
+
# This requires all grammar backend support the new format
|
| 222 |
+
return LegacyStructuralTagResponseFormat(
|
| 223 |
+
type="structural_tag",
|
| 224 |
+
structures=tool_structures,
|
| 225 |
+
triggers=list(tool_trigger_set),
|
| 226 |
+
at_least_one=at_least_one,
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
def get_structure_constraint(
|
| 230 |
+
self,
|
| 231 |
+
tool_choice: Union[ToolChoice, Literal["auto", "required"]],
|
| 232 |
+
parallel_tool_calls: bool = True,
|
| 233 |
+
thinking_mode: bool = False,
|
| 234 |
+
) -> Optional[ToolCallConstraint]:
|
| 235 |
+
"""
|
| 236 |
+
Returns the appropriate structure constraint for tool calls based on the tool_choice.
|
| 237 |
+
The constraint is used to guide the model's output format.
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
tool_choice: The tool choice setting from the request
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
A tuple of (constraint_type, constraint_value) to be added to sampling parameters,
|
| 244 |
+
or None if no constraint applies.
|
| 245 |
+
"""
|
| 246 |
+
is_required = tool_choice == "required" or isinstance(tool_choice, ToolChoice)
|
| 247 |
+
should_constrain_auto = tool_choice == "auto" and (
|
| 248 |
+
any(tool.function.strict for tool in self.tools)
|
| 249 |
+
or self.tool_strict_level >= ToolStrictLevel.FUNCTION
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
# Highest priority: model-native structural_tag when available.
|
| 253 |
+
try:
|
| 254 |
+
if tool_choice == "auto" and not should_constrain_auto:
|
| 255 |
+
structural_tag = self.detector.get_auto_tool_call_structural_tag(
|
| 256 |
+
tools=self.tools
|
| 257 |
+
)
|
| 258 |
+
if structural_tag is not None:
|
| 259 |
+
return ("structural_tag", structural_tag)
|
| 260 |
+
|
| 261 |
+
if is_required or should_constrain_auto:
|
| 262 |
+
structural_tag = self.detector.get_structural_tag(
|
| 263 |
+
tools=self.tools,
|
| 264 |
+
thinking_mode=thinking_mode,
|
| 265 |
+
tool_choice=tool_choice,
|
| 266 |
+
)
|
| 267 |
+
if structural_tag is not None:
|
| 268 |
+
return ("structural_tag", structural_tag)
|
| 269 |
+
|
| 270 |
+
# Fallback to legacy structural tag if model-native tag is not supported.
|
| 271 |
+
if self.detector.supports_structural_tag():
|
| 272 |
+
# For "required"/named: always use structural_tag to preserve the
|
| 273 |
+
# model's native tool call format. Schema is only included when
|
| 274 |
+
# strict=True, per OpenAI protocol semantics.
|
| 275 |
+
# For "auto": only constrain when strict is enabled.
|
| 276 |
+
tag = self.get_legacy_structural_tag(at_least_one=is_required)
|
| 277 |
+
return ("structural_tag", tag)
|
| 278 |
+
|
| 279 |
+
if tool_choice == "required" or isinstance(tool_choice, ToolChoice):
|
| 280 |
+
json_schema = get_json_schema_constraint(
|
| 281 |
+
self.tools, tool_choice, parallel_tool_calls=parallel_tool_calls
|
| 282 |
+
)
|
| 283 |
+
return ("json_schema", json_schema)
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logger.error(f"Error getting structure constraint: {e}")
|
| 286 |
+
return None
|
sglang_patch/srt/layers/attention/dsv4/compressor.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, List, Literal, NamedTuple, Optional, Union
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
|
| 8 |
+
from sglang.jit_kernel.dsv4 import linear_bf16_fp32, triton_create_paged_compress_data
|
| 9 |
+
from sglang.jit_kernel.dsv4.compress_old import (
|
| 10 |
+
CompressorDecodePlan,
|
| 11 |
+
CompressorPrefillPlan,
|
| 12 |
+
compress_forward,
|
| 13 |
+
compress_fused_norm_rope_inplace,
|
| 14 |
+
)
|
| 15 |
+
from sglang.kernels.ops.attention.dsa.triton_kernel import act_quant
|
| 16 |
+
from sglang.kernels.ops.attention.dsv4.quant_k_cache import (
|
| 17 |
+
quant_to_nope_fp8_rope_bf16_pack_triton,
|
| 18 |
+
)
|
| 19 |
+
from sglang.srt.configs.agnes import AgnesConfig
|
| 20 |
+
from sglang.srt.environ import envs
|
| 21 |
+
from sglang.srt.layers.attention.dsa.utils import dsa_use_prefill_cp
|
| 22 |
+
from sglang.srt.layers.layernorm import RMSNorm
|
| 23 |
+
from sglang.srt.layers.linear import ReplicatedLinear
|
| 24 |
+
from sglang.srt.layers.utils.cp_utils import cp_all_gather_rerange_output
|
| 25 |
+
from sglang.srt.layers.utils.multi_platform import MultiPlatformOp
|
| 26 |
+
from sglang.srt.mem_cache.deepseek_v4_compress_state import (
|
| 27 |
+
CompressStatePool,
|
| 28 |
+
)
|
| 29 |
+
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
| 30 |
+
from sglang.srt.model_executor.forward_context import get_attn_backend
|
| 31 |
+
from sglang.srt.models.deepseek_v2 import _is_hip
|
| 32 |
+
from sglang.srt.runtime_context import get_parallel
|
| 33 |
+
from sglang.srt.utils import add_prefix, is_npu, set_weight_attrs
|
| 34 |
+
|
| 35 |
+
_is_npu = is_npu()
|
| 36 |
+
|
| 37 |
+
if TYPE_CHECKING:
|
| 38 |
+
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
| 39 |
+
from sglang.srt.layers.attention.deepseek_v4_backend import DeepseekV4AttnBackend
|
| 40 |
+
from sglang.srt.layers.rotary_embedding import RotaryEmbedding
|
| 41 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class FusedCompressMetadata(NamedTuple):
|
| 45 |
+
write_loc: torch.Tensor
|
| 46 |
+
extra_data: Optional[torch.Tensor]
|
| 47 |
+
plan: Union[CompressorDecodePlan, CompressorPrefillPlan]
|
| 48 |
+
|
| 49 |
+
def copy_(self, other: FusedCompressMetadata) -> None:
|
| 50 |
+
from .metadata import maybe_copy_inplace
|
| 51 |
+
|
| 52 |
+
self.write_loc.copy_(other.write_loc)
|
| 53 |
+
maybe_copy_inplace(self.extra_data, src=other.extra_data)
|
| 54 |
+
self.plan.copy_(other.plan)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class CompressorBackendMixin:
|
| 58 |
+
def get_paged_compress_metadata(self, compress_ratio: int) -> FusedCompressMetadata:
|
| 59 |
+
attr_name = f"c{compress_ratio}_compress_metadata"
|
| 60 |
+
metadata = getattr(self.forward_metadata, attr_name)
|
| 61 |
+
assert isinstance(metadata, FusedCompressMetadata)
|
| 62 |
+
return metadata
|
| 63 |
+
|
| 64 |
+
def forward_compress(
|
| 65 |
+
self,
|
| 66 |
+
*,
|
| 67 |
+
kv_score_buffer: torch.Tensor,
|
| 68 |
+
kv_score_input: torch.Tensor,
|
| 69 |
+
ape: torch.Tensor,
|
| 70 |
+
head_dim: int,
|
| 71 |
+
norm: RMSNorm,
|
| 72 |
+
freqs_cis_cache: torch.Tensor,
|
| 73 |
+
rotate: bool,
|
| 74 |
+
forward_batch: ForwardBatch,
|
| 75 |
+
compress_ratio: int,
|
| 76 |
+
is_paged: bool = False,
|
| 77 |
+
) -> torch.Tensor:
|
| 78 |
+
from sglang.srt.layers.attention.dsa.dsa_indexer import rotate_activation
|
| 79 |
+
|
| 80 |
+
assert compress_ratio in (
|
| 81 |
+
4,
|
| 82 |
+
128,
|
| 83 |
+
), f"DSV4 supports CSA(4x) and HCA(128x) only, got {compress_ratio=}"
|
| 84 |
+
if is_paged:
|
| 85 |
+
metadata = self.get_paged_compress_metadata(compress_ratio)
|
| 86 |
+
coff = 2 if is_overlap_compress(compress_ratio) else 1
|
| 87 |
+
if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
| 88 |
+
kv_score_buffer = kv_score_buffer.view(-1, 1, head_dim * 3)
|
| 89 |
+
else:
|
| 90 |
+
last_dim = 2 * head_dim * coff
|
| 91 |
+
assert kv_score_buffer.shape[-1] == last_dim
|
| 92 |
+
kv_score_buffer = kv_score_buffer.view(-1, compress_ratio, last_dim)
|
| 93 |
+
else:
|
| 94 |
+
plan = make_compressor_plan(compress_ratio, forward_batch)
|
| 95 |
+
metadata = (forward_batch.req_pool_indices.to(torch.int32), None, plan)
|
| 96 |
+
indices, extra_data, plan = metadata
|
| 97 |
+
|
| 98 |
+
if _is_hip:
|
| 99 |
+
if not is_paged:
|
| 100 |
+
raise NotImplementedError("HIP fused compressor expects paged metadata")
|
| 101 |
+
|
| 102 |
+
from sglang.kernels.ops.attention.dsv4.fused_compress_triton import (
|
| 103 |
+
hip_compress_forward,
|
| 104 |
+
hip_compress_fused_norm_rope_hadamard_inplace,
|
| 105 |
+
hip_compress_fused_norm_rope_inplace,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
kv_compressed = hip_compress_forward(
|
| 109 |
+
kv_score_buffer=kv_score_buffer,
|
| 110 |
+
kv_score_input=kv_score_input,
|
| 111 |
+
ape=ape,
|
| 112 |
+
indices=indices,
|
| 113 |
+
plan=plan,
|
| 114 |
+
compress_ratio=compress_ratio,
|
| 115 |
+
head_dim=head_dim,
|
| 116 |
+
extra_data=extra_data,
|
| 117 |
+
)
|
| 118 |
+
norm_eps = (
|
| 119 |
+
norm.variance_epsilon if hasattr(norm, "variance_epsilon") else norm.eps
|
| 120 |
+
)
|
| 121 |
+
if rotate:
|
| 122 |
+
hip_compress_fused_norm_rope_hadamard_inplace(
|
| 123 |
+
kv_compressed,
|
| 124 |
+
norm.weight,
|
| 125 |
+
norm_eps,
|
| 126 |
+
freqs_cis_cache,
|
| 127 |
+
plan,
|
| 128 |
+
head_dim,
|
| 129 |
+
)
|
| 130 |
+
else:
|
| 131 |
+
hip_compress_fused_norm_rope_inplace(
|
| 132 |
+
kv_compressed,
|
| 133 |
+
norm.weight,
|
| 134 |
+
norm_eps,
|
| 135 |
+
freqs_cis_cache,
|
| 136 |
+
plan,
|
| 137 |
+
)
|
| 138 |
+
return kv_compressed
|
| 139 |
+
|
| 140 |
+
kv_compressed = compress_forward(
|
| 141 |
+
kv_score_buffer=kv_score_buffer,
|
| 142 |
+
kv_score_input=kv_score_input,
|
| 143 |
+
ape=ape,
|
| 144 |
+
indices=indices,
|
| 145 |
+
plan=plan,
|
| 146 |
+
compress_ratio=compress_ratio,
|
| 147 |
+
head_dim=head_dim,
|
| 148 |
+
extra_data=extra_data,
|
| 149 |
+
)
|
| 150 |
+
compress_fused_norm_rope_inplace(
|
| 151 |
+
kv_compressed,
|
| 152 |
+
norm.weight,
|
| 153 |
+
norm.variance_epsilon,
|
| 154 |
+
freqs_cis_cache,
|
| 155 |
+
plan,
|
| 156 |
+
)
|
| 157 |
+
return rotate_activation(kv_compressed) if rotate else kv_compressed
|
| 158 |
+
|
| 159 |
+
def forward_core_compressor(
|
| 160 |
+
self,
|
| 161 |
+
x: torch.Tensor,
|
| 162 |
+
forward_batch: ForwardBatch,
|
| 163 |
+
layer_id: int,
|
| 164 |
+
compressor: Compressor,
|
| 165 |
+
) -> None:
|
| 166 |
+
if forward_batch.forward_mode.is_idle():
|
| 167 |
+
return
|
| 168 |
+
token_to_kv_pool = self.token_to_kv_pool
|
| 169 |
+
if TYPE_CHECKING:
|
| 170 |
+
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
| 171 |
+
|
| 172 |
+
new_compressed_kv = compressor(x, forward_batch, attn_backend=self)
|
| 173 |
+
core_metadata = self.forward_metadata.core_metadata
|
| 174 |
+
out_loc = (
|
| 175 |
+
core_metadata.c4_out_loc
|
| 176 |
+
if compressor.ratio == 4
|
| 177 |
+
else core_metadata.c128_out_loc
|
| 178 |
+
)
|
| 179 |
+
if out_loc.shape[0] > new_compressed_kv.shape[0]:
|
| 180 |
+
out_loc = out_loc[: new_compressed_kv.shape[0]]
|
| 181 |
+
if envs.SGLANG_OPT_USE_FUSED_STORE_CACHE.get():
|
| 182 |
+
token_to_kv_pool.set_extra_key_buffer_fused(
|
| 183 |
+
layer_id=layer_id,
|
| 184 |
+
loc=out_loc,
|
| 185 |
+
cache_k=new_compressed_kv,
|
| 186 |
+
)
|
| 187 |
+
else:
|
| 188 |
+
pack = quant_to_nope_fp8_rope_bf16_pack_triton(new_compressed_kv.bfloat16())
|
| 189 |
+
token_to_kv_pool.set_extra_key_buffer(layer_id, out_loc, pack)
|
| 190 |
+
|
| 191 |
+
def forward_indexer_compressor(
|
| 192 |
+
self,
|
| 193 |
+
x: torch.Tensor,
|
| 194 |
+
forward_batch: ForwardBatch,
|
| 195 |
+
layer_id: int,
|
| 196 |
+
compressor: Compressor,
|
| 197 |
+
) -> None:
|
| 198 |
+
assert is_overlap_compress(compressor.ratio)
|
| 199 |
+
token_to_kv_pool = self.token_to_kv_pool
|
| 200 |
+
if TYPE_CHECKING:
|
| 201 |
+
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
| 202 |
+
|
| 203 |
+
new_compressed_kv = compressor(x, forward_batch, attn_backend=self)
|
| 204 |
+
out_loc = self.forward_metadata.core_metadata.c4_out_loc
|
| 205 |
+
if out_loc.shape[0] > new_compressed_kv.shape[0]:
|
| 206 |
+
out_loc = out_loc[: new_compressed_kv.shape[0]]
|
| 207 |
+
if self.enable_deepseek_v4_fp4_indexer:
|
| 208 |
+
token_to_kv_pool.set_index_k_fp4(
|
| 209 |
+
layer_id=layer_id,
|
| 210 |
+
loc=out_loc,
|
| 211 |
+
cache_k=new_compressed_kv,
|
| 212 |
+
)
|
| 213 |
+
elif envs.SGLANG_OPT_USE_FUSED_STORE_CACHE.get():
|
| 214 |
+
token_to_kv_pool.set_index_k_fused(
|
| 215 |
+
layer_id=layer_id,
|
| 216 |
+
loc=out_loc,
|
| 217 |
+
cache_k=new_compressed_kv,
|
| 218 |
+
)
|
| 219 |
+
else:
|
| 220 |
+
new_compressed_kv_fp8, new_compressed_kv_scale = act_quant(
|
| 221 |
+
new_compressed_kv
|
| 222 |
+
)
|
| 223 |
+
token_to_kv_pool.set_index_k_scale_buffer(
|
| 224 |
+
layer_id=layer_id,
|
| 225 |
+
loc=out_loc,
|
| 226 |
+
index_k=new_compressed_kv_fp8,
|
| 227 |
+
index_k_scale=new_compressed_kv_scale,
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def is_overlap_compress(compress_ratio: int) -> bool:
|
| 232 |
+
return compress_ratio == 4
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def make_compressor_plan(
|
| 236 |
+
compress_ratio: Literal[4, 128],
|
| 237 |
+
forward_batch: ForwardBatch,
|
| 238 |
+
) -> Union[CompressorDecodePlan, CompressorPrefillPlan]:
|
| 239 |
+
if forward_batch.forward_mode.is_decode():
|
| 240 |
+
seq_lens_32 = forward_batch.seq_lens.to(torch.int32)
|
| 241 |
+
return CompressorDecodePlan(compress_ratio, seq_lens_32)
|
| 242 |
+
if forward_batch.forward_mode.is_prefill():
|
| 243 |
+
assert not forward_batch.forward_mode.is_target_verify()
|
| 244 |
+
extend_lens_list = forward_batch.extend_seq_lens_cpu
|
| 245 |
+
seq_lens_cpu = forward_batch.seq_lens_cpu
|
| 246 |
+
assert extend_lens_list is not None and seq_lens_cpu is not None
|
| 247 |
+
return CompressorPrefillPlan.generate(
|
| 248 |
+
compress_ratio=compress_ratio,
|
| 249 |
+
num_q_tokens=sum(extend_lens_list),
|
| 250 |
+
seq_lens=seq_lens_cpu,
|
| 251 |
+
extend_lens=torch.tensor(extend_lens_list),
|
| 252 |
+
device=forward_batch.seq_lens.device,
|
| 253 |
+
)
|
| 254 |
+
elif forward_batch.forward_mode.is_target_verify():
|
| 255 |
+
raise NotImplementedError("target verify mode to be implemented")
|
| 256 |
+
else:
|
| 257 |
+
raise NotImplementedError(f"unsupported mode {forward_batch.forward_mode=}")
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def create_paged_compressor_data(
|
| 261 |
+
compress_ratio: Literal[4, 128],
|
| 262 |
+
*,
|
| 263 |
+
is_prefill: bool,
|
| 264 |
+
token_to_kv_pool: DeepSeekV4TokenToKVPool,
|
| 265 |
+
req_to_token: torch.Tensor,
|
| 266 |
+
req_pool_indices: torch.Tensor,
|
| 267 |
+
seq_lens: torch.Tensor,
|
| 268 |
+
extend_lens: Optional[torch.Tensor] = None,
|
| 269 |
+
seq_lens_cpu: Optional[List[int]] = None,
|
| 270 |
+
extend_lens_cpu: Optional[List[int]] = None,
|
| 271 |
+
use_prefill_cuda_graph: bool = False,
|
| 272 |
+
num_q_tokens: Optional[int] = None,
|
| 273 |
+
) -> FusedCompressMetadata:
|
| 274 |
+
swa_page_size = token_to_kv_pool.swa_page_size
|
| 275 |
+
ring_size = token_to_kv_pool.get_ring_size(compress_ratio=compress_ratio)
|
| 276 |
+
# assert ring_size % compress_ratio == 0
|
| 277 |
+
|
| 278 |
+
def clip_down(positions: torch.Tensor) -> torch.Tensor:
|
| 279 |
+
return positions // compress_ratio * compress_ratio
|
| 280 |
+
|
| 281 |
+
def get_raw_loc(positions: torch.Tensor) -> torch.Tensor:
|
| 282 |
+
positions = positions.masked_fill(positions < 0, 0)
|
| 283 |
+
if compress_ratio == 128:
|
| 284 |
+
state_loc = req_pool_indices * ring_size + positions % ring_size
|
| 285 |
+
else:
|
| 286 |
+
loc = req_to_token[req_pool_indices, positions]
|
| 287 |
+
swa_loc = token_to_kv_pool.translate_loc_from_full_to_swa(loc)
|
| 288 |
+
swa_pages = swa_loc // swa_page_size
|
| 289 |
+
state_loc = swa_pages * ring_size + swa_loc % ring_size
|
| 290 |
+
return (state_loc // compress_ratio).to(torch.int32)
|
| 291 |
+
|
| 292 |
+
is_overlap = is_overlap_compress(compress_ratio)
|
| 293 |
+
|
| 294 |
+
if is_prefill:
|
| 295 |
+
assert extend_lens is not None
|
| 296 |
+
write_loc, extra_data = triton_create_paged_compress_data(
|
| 297 |
+
compress_ratio=compress_ratio,
|
| 298 |
+
is_overlap=is_overlap,
|
| 299 |
+
swa_page_size=swa_page_size,
|
| 300 |
+
ring_size=ring_size,
|
| 301 |
+
req_pool_indices=req_pool_indices,
|
| 302 |
+
seq_lens=seq_lens,
|
| 303 |
+
extend_seq_lens=extend_lens,
|
| 304 |
+
req_to_token=req_to_token,
|
| 305 |
+
full_to_swa_index_mapping=token_to_kv_pool.full_to_swa_index_mapping,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
plan_kwargs: dict
|
| 309 |
+
if seq_lens_cpu is None:
|
| 310 |
+
assert num_q_tokens is not None
|
| 311 |
+
plan_kwargs = dict(
|
| 312 |
+
num_q_tokens=num_q_tokens,
|
| 313 |
+
seq_lens=seq_lens,
|
| 314 |
+
extend_lens=extend_lens,
|
| 315 |
+
)
|
| 316 |
+
else:
|
| 317 |
+
assert extend_lens_cpu is not None
|
| 318 |
+
plan_kwargs = dict(
|
| 319 |
+
num_q_tokens=sum(extend_lens_cpu),
|
| 320 |
+
seq_lens=torch.tensor(seq_lens_cpu),
|
| 321 |
+
extend_lens=torch.tensor(extend_lens_cpu),
|
| 322 |
+
)
|
| 323 |
+
plan = CompressorPrefillPlan.generate(
|
| 324 |
+
compress_ratio=compress_ratio,
|
| 325 |
+
device=seq_lens.device,
|
| 326 |
+
use_cuda_graph=use_prefill_cuda_graph,
|
| 327 |
+
**plan_kwargs,
|
| 328 |
+
)
|
| 329 |
+
else:
|
| 330 |
+
write_positions = clip_down(seq_lens - 1)
|
| 331 |
+
write_loc = get_raw_loc(write_positions)
|
| 332 |
+
if is_overlap:
|
| 333 |
+
write_overlap_loc = get_raw_loc(write_positions - compress_ratio)
|
| 334 |
+
extra_data = write_overlap_loc.view(-1, 1)
|
| 335 |
+
elif _is_hip:
|
| 336 |
+
extra_data = get_raw_loc(write_positions - compress_ratio)
|
| 337 |
+
else:
|
| 338 |
+
extra_data = None
|
| 339 |
+
plan = CompressorDecodePlan(compress_ratio, seq_lens.to(torch.int32))
|
| 340 |
+
|
| 341 |
+
return FusedCompressMetadata(write_loc=write_loc, extra_data=extra_data, plan=plan)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class Compressor(MultiPlatformOp):
|
| 345 |
+
def __init__(
|
| 346 |
+
self,
|
| 347 |
+
config: AgnesConfig,
|
| 348 |
+
layer_id: int,
|
| 349 |
+
is_in_indexer: bool,
|
| 350 |
+
freqs_cis: torch.Tensor,
|
| 351 |
+
compress_ratio: Literal[0, 4, 128],
|
| 352 |
+
head_dim: int,
|
| 353 |
+
rotate: bool = False,
|
| 354 |
+
prefix: str = "",
|
| 355 |
+
rotary_emb: Optional[RotaryEmbedding] = None,
|
| 356 |
+
) -> None:
|
| 357 |
+
super().__init__()
|
| 358 |
+
self.layer_id = layer_id
|
| 359 |
+
self.is_in_indexer = is_in_indexer
|
| 360 |
+
self.dim = config.hidden_size
|
| 361 |
+
self.head_dim = head_dim
|
| 362 |
+
self.rope_head_dim = getattr(config, "qk_rope_head_dim", 64)
|
| 363 |
+
assert compress_ratio != 0, "compress_ratio should not be 0"
|
| 364 |
+
self.ratio = compress_ratio
|
| 365 |
+
self.overlap = self.ratio == 4
|
| 366 |
+
self.rotate = rotate
|
| 367 |
+
self.coff = coff = 1 + self.overlap
|
| 368 |
+
|
| 369 |
+
self.ape = nn.Parameter(
|
| 370 |
+
torch.empty(self.ratio, coff * self.head_dim, dtype=torch.float32)
|
| 371 |
+
)
|
| 372 |
+
set_weight_attrs(self.ape, {"weight_loader": self.load_ape_weight})
|
| 373 |
+
wkv_gate_dtype = torch.bfloat16
|
| 374 |
+
self.wkv_gate = ReplicatedLinear(
|
| 375 |
+
self.dim,
|
| 376 |
+
2 * coff * self.head_dim,
|
| 377 |
+
bias=False,
|
| 378 |
+
quant_config=None,
|
| 379 |
+
prefix=add_prefix("wkv_gate", prefix),
|
| 380 |
+
params_dtype=wkv_gate_dtype,
|
| 381 |
+
)
|
| 382 |
+
self.norm = RMSNorm(
|
| 383 |
+
self.head_dim, eps=config.rms_norm_eps, weight_dtype=torch.float32
|
| 384 |
+
)
|
| 385 |
+
self.rotary_emb = rotary_emb
|
| 386 |
+
self.freqs_cis = freqs_cis
|
| 387 |
+
|
| 388 |
+
self.ape_converted = False
|
| 389 |
+
|
| 390 |
+
def _apply_ape_hotfix(self):
|
| 391 |
+
self.ape_converted = True
|
| 392 |
+
|
| 393 |
+
if _is_npu:
|
| 394 |
+
return
|
| 395 |
+
|
| 396 |
+
if self.overlap:
|
| 397 |
+
ape = torch.chunk(self.ape.data, 2, dim=-1)
|
| 398 |
+
ape = torch.cat([ape[0], ape[1]], dim=0)
|
| 399 |
+
self.ape.data.copy_(ape.view(self.ratio, -1))
|
| 400 |
+
|
| 401 |
+
def apply_ape_hotfix(self):
|
| 402 |
+
assert not self.ape_converted
|
| 403 |
+
self._apply_ape_hotfix()
|
| 404 |
+
|
| 405 |
+
def load_ape_weight(self, param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
|
| 406 |
+
assert param is self.ape
|
| 407 |
+
assert loaded_weight.shape == param.shape
|
| 408 |
+
param.data.copy_(loaded_weight)
|
| 409 |
+
self._apply_ape_hotfix()
|
| 410 |
+
|
| 411 |
+
def get_state_pool(self, attn_backend: AttentionBackend) -> CompressStatePool:
|
| 412 |
+
token_to_kv_pool = attn_backend.token_to_kv_pool
|
| 413 |
+
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
| 414 |
+
if self.is_in_indexer:
|
| 415 |
+
ret = token_to_kv_pool.get_indexer_compress_states(self.layer_id)
|
| 416 |
+
else:
|
| 417 |
+
ret = token_to_kv_pool.get_attention_compress_states(self.layer_id)
|
| 418 |
+
assert isinstance(ret, CompressStatePool)
|
| 419 |
+
return ret
|
| 420 |
+
|
| 421 |
+
def compute_kv_score(self, x: torch.Tensor, forward_batch: ForwardBatch):
|
| 422 |
+
kv_score = linear_bf16_fp32(x, self.wkv_gate.weight)
|
| 423 |
+
|
| 424 |
+
# CUDA path: delegate to backend
|
| 425 |
+
if dsa_use_prefill_cp(forward_batch):
|
| 426 |
+
kv_score = cp_all_gather_rerange_output(
|
| 427 |
+
kv_score,
|
| 428 |
+
get_parallel().attn_cp_size,
|
| 429 |
+
forward_batch,
|
| 430 |
+
torch.cuda.current_stream(),
|
| 431 |
+
)
|
| 432 |
+
return kv_score
|
| 433 |
+
|
| 434 |
+
def forward_native(
|
| 435 |
+
self,
|
| 436 |
+
x: torch.Tensor,
|
| 437 |
+
forward_batch: ForwardBatch,
|
| 438 |
+
attn_backend: Optional[AttentionBackend] = None,
|
| 439 |
+
) -> torch.Tensor:
|
| 440 |
+
if forward_batch.forward_mode.is_idle():
|
| 441 |
+
assert x.shape[0] == 0
|
| 442 |
+
return x.new_empty(0, self.head_dim)
|
| 443 |
+
|
| 444 |
+
kv_score = self.compute_kv_score(x, forward_batch)
|
| 445 |
+
|
| 446 |
+
if TYPE_CHECKING:
|
| 447 |
+
assert isinstance(attn_backend, DeepseekV4AttnBackend)
|
| 448 |
+
kv_score_buffer = self.get_state_pool(attn_backend).kv_score_buffer.kv_score
|
| 449 |
+
return attn_backend.forward_compress(
|
| 450 |
+
kv_score_buffer=kv_score_buffer,
|
| 451 |
+
kv_score_input=kv_score,
|
| 452 |
+
ape=self.ape.view(-1, self.head_dim),
|
| 453 |
+
head_dim=self.head_dim,
|
| 454 |
+
norm=self.norm,
|
| 455 |
+
freqs_cis_cache=self.freqs_cis,
|
| 456 |
+
rotate=self.rotate,
|
| 457 |
+
compress_ratio=self.ratio,
|
| 458 |
+
forward_batch=forward_batch,
|
| 459 |
+
is_paged=True,
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
def forward_npu(
|
| 463 |
+
self,
|
| 464 |
+
x: torch.Tensor,
|
| 465 |
+
forward_batch: ForwardBatch,
|
| 466 |
+
attn_backend: Optional[AttentionBackend] = None,
|
| 467 |
+
) -> torch.Tensor:
|
| 468 |
+
if forward_batch.forward_mode.is_idle():
|
| 469 |
+
assert x.shape[0] == 0
|
| 470 |
+
return x.new_empty(0, self.head_dim)
|
| 471 |
+
|
| 472 |
+
if dsa_use_prefill_cp(forward_batch):
|
| 473 |
+
x = cp_all_gather_rerange_output(
|
| 474 |
+
x,
|
| 475 |
+
get_parallel().attn_cp_size,
|
| 476 |
+
forward_batch,
|
| 477 |
+
torch.cuda.current_stream(),
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
return get_attn_backend().forward_compress(self, x, forward_batch)
|
sglang_patch/srt/layers/attention/dsv4/indexer.py
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, TypeAlias, Union
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
from sglang.jit_kernel.dsv4 import (
|
| 10 |
+
fused_q_indexer_rope_hadamard_fp4_quant,
|
| 11 |
+
fused_q_indexer_rope_hadamard_quant,
|
| 12 |
+
topk_transform_512,
|
| 13 |
+
topk_transform_512_v2,
|
| 14 |
+
)
|
| 15 |
+
from sglang.kernels.ops.quantization.fp8_kernel import is_fp8_fnuz
|
| 16 |
+
from sglang.srt.configs.agnes import AgnesConfig
|
| 17 |
+
from sglang.srt.environ import envs
|
| 18 |
+
from sglang.srt.layers.attention.dsv4.compressor import Compressor
|
| 19 |
+
from sglang.srt.layers.attention.dsv4.metadata import (
|
| 20 |
+
NonPagedIndexerPlan,
|
| 21 |
+
PagedIndexerMetadata,
|
| 22 |
+
)
|
| 23 |
+
from sglang.srt.layers.linear import ReplicatedLinear
|
| 24 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
| 25 |
+
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import (
|
| 26 |
+
is_in_breakable_cuda_graph,
|
| 27 |
+
)
|
| 28 |
+
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
|
| 29 |
+
is_in_tc_piecewise_cuda_graph,
|
| 30 |
+
)
|
| 31 |
+
from sglang.srt.runtime_context import get_parallel
|
| 32 |
+
from sglang.srt.state_capturer.indexer_topk import get_global_indexer_capturer
|
| 33 |
+
from sglang.srt.utils import add_prefix, is_cuda, is_hip, is_xpu
|
| 34 |
+
from sglang.srt.utils.common import is_sm120_supported
|
| 35 |
+
|
| 36 |
+
if TYPE_CHECKING:
|
| 37 |
+
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
| 38 |
+
from sglang.srt.layers.attention.dsv4.compressor import (
|
| 39 |
+
CompressorBackendMixin,
|
| 40 |
+
)
|
| 41 |
+
from sglang.srt.layers.quantization import QuantizationConfig
|
| 42 |
+
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
| 43 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
FP8_DTYPE = torch.float8_e4m3fnuz if is_fp8_fnuz() else torch.float8_e4m3fn
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
IndexerQuery: TypeAlias = Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
_arange_cache = {}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def fp8_paged_mqa_logits_torch(
|
| 56 |
+
q_fp8: torch.Tensor,
|
| 57 |
+
kvcache_fp8: torch.Tensor,
|
| 58 |
+
weight: torch.Tensor,
|
| 59 |
+
seq_lens: torch.Tensor,
|
| 60 |
+
page_table: torch.Tensor,
|
| 61 |
+
deep_gemm_metadata: Any,
|
| 62 |
+
max_seq_len: int,
|
| 63 |
+
clean_logits: bool = True,
|
| 64 |
+
) -> torch.Tensor:
|
| 65 |
+
"""Vectorized implementation compatible with CUDA graph capture."""
|
| 66 |
+
_ = deep_gemm_metadata
|
| 67 |
+
batch_size, _, num_heads, head_dim = q_fp8.shape
|
| 68 |
+
block_size = kvcache_fp8.shape[1]
|
| 69 |
+
|
| 70 |
+
assert head_dim == 128
|
| 71 |
+
assert block_size == 64
|
| 72 |
+
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
|
| 73 |
+
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
|
| 74 |
+
assert weight.shape == (batch_size, num_heads)
|
| 75 |
+
assert seq_lens.shape == (batch_size,)
|
| 76 |
+
assert page_table.shape[0] == batch_size
|
| 77 |
+
assert clean_logits == False
|
| 78 |
+
|
| 79 |
+
max_num_pages = page_table.shape[1]
|
| 80 |
+
SCALE_OFFSET = block_size * head_dim
|
| 81 |
+
total_dim = block_size * (head_dim + 4)
|
| 82 |
+
|
| 83 |
+
kvcache_flat = kvcache_fp8.view(-1, total_dim)
|
| 84 |
+
|
| 85 |
+
pages_clamped = page_table.clamp(min=0)
|
| 86 |
+
kvcache_gathered = kvcache_flat[pages_clamped]
|
| 87 |
+
|
| 88 |
+
kv_values_raw = kvcache_gathered[..., :SCALE_OFFSET].contiguous()
|
| 89 |
+
kv_values_fp8 = kv_values_raw.view(dtype=FP8_DTYPE)
|
| 90 |
+
kv_values = kv_values_fp8.to(torch.bfloat16)
|
| 91 |
+
kv_values = kv_values.reshape(batch_size, max_num_pages * block_size, head_dim)
|
| 92 |
+
|
| 93 |
+
kv_scales_raw = kvcache_gathered[..., SCALE_OFFSET:].contiguous()
|
| 94 |
+
kv_scales = kv_scales_raw.view(dtype=torch.float32)
|
| 95 |
+
kv_scales = kv_scales.reshape(batch_size, max_num_pages * block_size)
|
| 96 |
+
|
| 97 |
+
q_float = q_fp8[:, 0].to(torch.bfloat16)
|
| 98 |
+
scores = torch.bmm(kv_values, q_float.transpose(1, 2))
|
| 99 |
+
scores = F.relu(scores)
|
| 100 |
+
scores = scores * weight.unsqueeze(1)
|
| 101 |
+
scores = scores.sum(dim=2)
|
| 102 |
+
scores = scores * kv_scales
|
| 103 |
+
|
| 104 |
+
padded_seq_len = max_num_pages * block_size
|
| 105 |
+
cache = _arange_cache
|
| 106 |
+
arange_key = f"arange_{padded_seq_len}_{scores.device}"
|
| 107 |
+
if arange_key not in cache:
|
| 108 |
+
cache[arange_key] = torch.arange(padded_seq_len, device=scores.device)
|
| 109 |
+
positions = cache[arange_key].unsqueeze(0)
|
| 110 |
+
valid_mask = positions < seq_lens.unsqueeze(1)
|
| 111 |
+
scores = scores.masked_fill(~valid_mask, 0.0)
|
| 112 |
+
|
| 113 |
+
if padded_seq_len < max_seq_len:
|
| 114 |
+
scores = F.pad(scores, (0, max_seq_len - padded_seq_len), value=0.0)
|
| 115 |
+
else:
|
| 116 |
+
scores = scores[:, :max_seq_len]
|
| 117 |
+
|
| 118 |
+
return scores
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _aiter_fp8_paged_mqa_logits(
|
| 122 |
+
q_fp8: torch.Tensor,
|
| 123 |
+
kvcache_fp8: torch.Tensor,
|
| 124 |
+
weight: torch.Tensor,
|
| 125 |
+
seq_lens: torch.Tensor,
|
| 126 |
+
page_table: torch.Tensor,
|
| 127 |
+
deep_gemm_metadata: Any,
|
| 128 |
+
max_seq_len: int,
|
| 129 |
+
clean_logits: bool = False,
|
| 130 |
+
) -> torch.Tensor:
|
| 131 |
+
"""Wrapper adapting aiter's deepgemm_fp8_paged_mqa_logits to SGLang's interface."""
|
| 132 |
+
from aiter.ops.triton.attention.pa_mqa_logits import (
|
| 133 |
+
deepgemm_fp8_paged_mqa_logits,
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
batch_size = q_fp8.shape[0]
|
| 137 |
+
next_n = q_fp8.shape[1]
|
| 138 |
+
total_tokens = batch_size * next_n
|
| 139 |
+
_sl = seq_lens.squeeze(-1) if seq_lens.dim() == 2 else seq_lens
|
| 140 |
+
kv_block_size = kvcache_fp8.shape[1]
|
| 141 |
+
logits = torch.empty(
|
| 142 |
+
total_tokens,
|
| 143 |
+
max_seq_len,
|
| 144 |
+
dtype=torch.float32,
|
| 145 |
+
device=q_fp8.device,
|
| 146 |
+
)
|
| 147 |
+
deepgemm_fp8_paged_mqa_logits(
|
| 148 |
+
q_fp8,
|
| 149 |
+
kvcache_fp8,
|
| 150 |
+
weight,
|
| 151 |
+
logits,
|
| 152 |
+
_sl.to(torch.int32),
|
| 153 |
+
page_table.to(torch.int32),
|
| 154 |
+
max_seq_len,
|
| 155 |
+
KVBlockSize=kv_block_size,
|
| 156 |
+
Preshuffle=True,
|
| 157 |
+
)
|
| 158 |
+
return logits
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def fp8_paged_mqa_logits_torch_sm120(
|
| 162 |
+
q_fp8: torch.Tensor,
|
| 163 |
+
kvcache_fp8: torch.Tensor,
|
| 164 |
+
weight: torch.Tensor,
|
| 165 |
+
seq_lens: torch.Tensor,
|
| 166 |
+
page_table: torch.Tensor,
|
| 167 |
+
deep_gemm_metadata: Any,
|
| 168 |
+
max_seq_len: int,
|
| 169 |
+
clean_logits: bool = True,
|
| 170 |
+
) -> torch.Tensor:
|
| 171 |
+
"""CUDA-graph-compatible FP8 paged MQA logits for SM120 (vectorized, no .item())."""
|
| 172 |
+
_ = deep_gemm_metadata
|
| 173 |
+
batch_size, _, num_heads, head_dim = q_fp8.shape
|
| 174 |
+
block_size = kvcache_fp8.shape[1]
|
| 175 |
+
device = q_fp8.device
|
| 176 |
+
|
| 177 |
+
_QUERY_CHUNK = 1024
|
| 178 |
+
if batch_size > _QUERY_CHUNK:
|
| 179 |
+
return torch.cat(
|
| 180 |
+
[
|
| 181 |
+
fp8_paged_mqa_logits_torch_sm120(
|
| 182 |
+
q_fp8[start : start + _QUERY_CHUNK],
|
| 183 |
+
kvcache_fp8,
|
| 184 |
+
weight[start : start + _QUERY_CHUNK],
|
| 185 |
+
seq_lens[start : start + _QUERY_CHUNK],
|
| 186 |
+
page_table[start : start + _QUERY_CHUNK],
|
| 187 |
+
deep_gemm_metadata,
|
| 188 |
+
max_seq_len,
|
| 189 |
+
clean_logits=clean_logits,
|
| 190 |
+
)
|
| 191 |
+
for start in range(0, batch_size, _QUERY_CHUNK)
|
| 192 |
+
],
|
| 193 |
+
dim=0,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
assert head_dim == 128, "Vectorized torch impl hardcodes DSV4 indexer head_dim=128"
|
| 197 |
+
assert (
|
| 198 |
+
block_size == 64
|
| 199 |
+
), "Vectorized torch impl hardcodes block_size=64 cache layout"
|
| 200 |
+
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
|
| 201 |
+
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
|
| 202 |
+
assert weight.shape == (batch_size, num_heads)
|
| 203 |
+
if seq_lens.dim() > 1:
|
| 204 |
+
seq_lens = seq_lens.squeeze(-1)
|
| 205 |
+
assert seq_lens.shape == (batch_size,)
|
| 206 |
+
assert page_table.shape[0] == batch_size
|
| 207 |
+
assert clean_logits == False
|
| 208 |
+
|
| 209 |
+
max_pages = (max_seq_len + block_size - 1) // block_size
|
| 210 |
+
max_padded_seq = max_pages * block_size
|
| 211 |
+
|
| 212 |
+
kvcache_flat = kvcache_fp8.view(-1, block_size * (head_dim + 4))
|
| 213 |
+
SCALE_OFFSET = block_size * head_dim
|
| 214 |
+
|
| 215 |
+
page_ids = page_table[:, :max_pages]
|
| 216 |
+
kvcache_gathered = kvcache_flat[page_ids]
|
| 217 |
+
|
| 218 |
+
kv_value_raw = kvcache_gathered[..., :SCALE_OFFSET]
|
| 219 |
+
kv_scale_raw = kvcache_gathered[..., SCALE_OFFSET:]
|
| 220 |
+
|
| 221 |
+
kv_value = kv_value_raw.contiguous().view(dtype=FP8_DTYPE).to(torch.bfloat16)
|
| 222 |
+
kv_value = kv_value.view(batch_size, max_padded_seq, head_dim)
|
| 223 |
+
|
| 224 |
+
kv_scale = kv_scale_raw.contiguous().view(dtype=torch.float32)
|
| 225 |
+
kv_scale = kv_scale.view(batch_size, max_padded_seq)
|
| 226 |
+
|
| 227 |
+
q = q_fp8[:, 0].to(torch.bfloat16)
|
| 228 |
+
|
| 229 |
+
score = torch.bmm(kv_value, q.transpose(1, 2))
|
| 230 |
+
|
| 231 |
+
score = F.relu(score)
|
| 232 |
+
score = score * weight.unsqueeze(1)
|
| 233 |
+
score = score.sum(dim=2)
|
| 234 |
+
|
| 235 |
+
score = score * kv_scale
|
| 236 |
+
|
| 237 |
+
out_width = min(max_padded_seq, max_seq_len)
|
| 238 |
+
logits = score.new_full((batch_size, max_seq_len), float("-inf"))
|
| 239 |
+
logits[:, :out_width] = score[:, :out_width]
|
| 240 |
+
|
| 241 |
+
positions = torch.arange(max_seq_len, device=device)
|
| 242 |
+
invalid_mask = positions.unsqueeze(0) >= seq_lens.unsqueeze(1)
|
| 243 |
+
logits.masked_fill_(invalid_mask, float("-inf"))
|
| 244 |
+
|
| 245 |
+
return logits
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def topk_transform_512_pytorch_vectorized(
|
| 249 |
+
scores: torch.Tensor,
|
| 250 |
+
seq_lens: torch.Tensor,
|
| 251 |
+
page_tables: torch.Tensor,
|
| 252 |
+
out_page_indices: torch.Tensor,
|
| 253 |
+
page_size: int,
|
| 254 |
+
out_raw_indices: Optional[torch.Tensor] = None,
|
| 255 |
+
) -> None:
|
| 256 |
+
"""Vectorized PyTorch fallback for topk_transform_512.
|
| 257 |
+
All helper tensors (arange, zeros) are cached to avoid device-tensor
|
| 258 |
+
creation during HIP/CUDA graph capture."""
|
| 259 |
+
|
| 260 |
+
TOPK = out_page_indices.shape[1]
|
| 261 |
+
batch_size = scores.shape[0]
|
| 262 |
+
max_seq_len = scores.shape[1]
|
| 263 |
+
device = scores.device
|
| 264 |
+
|
| 265 |
+
page_bits = (page_size - 1).bit_length() if page_size > 1 else 0
|
| 266 |
+
page_mask = page_size - 1
|
| 267 |
+
|
| 268 |
+
cache = _arange_cache
|
| 269 |
+
key_seq = f"arange_{max_seq_len}_{device}"
|
| 270 |
+
key_topk = f"arange_{TOPK}_{device}"
|
| 271 |
+
key_bs = f"arange_{batch_size}_{device}"
|
| 272 |
+
if key_seq not in cache:
|
| 273 |
+
cache[key_seq] = torch.arange(max_seq_len, device=device)
|
| 274 |
+
if key_topk not in cache:
|
| 275 |
+
cache[key_topk] = torch.arange(TOPK, device=device, dtype=torch.int32)
|
| 276 |
+
if key_bs not in cache:
|
| 277 |
+
cache[key_bs] = torch.arange(batch_size, device=device)
|
| 278 |
+
|
| 279 |
+
positions = cache[key_seq].unsqueeze(0).expand(batch_size, -1)
|
| 280 |
+
valid_mask = positions < seq_lens.unsqueeze(1)
|
| 281 |
+
|
| 282 |
+
masked_scores = scores.clone()
|
| 283 |
+
masked_scores.masked_fill_(~valid_mask, float("-inf"))
|
| 284 |
+
|
| 285 |
+
actual_k = min(TOPK, max_seq_len)
|
| 286 |
+
_, raw_indices = torch.topk(
|
| 287 |
+
masked_scores, k=actual_k, dim=1, largest=True, sorted=False
|
| 288 |
+
)
|
| 289 |
+
raw_indices = raw_indices.to(torch.int32)
|
| 290 |
+
|
| 291 |
+
if actual_k < TOPK:
|
| 292 |
+
raw_indices = F.pad(raw_indices, (0, TOPK - actual_k), value=0)
|
| 293 |
+
|
| 294 |
+
batch_indices = cache[key_bs].unsqueeze(1).expand(-1, TOPK)
|
| 295 |
+
gathered_scores = scores[
|
| 296 |
+
batch_indices.flatten(), raw_indices.clamp(min=0).flatten()
|
| 297 |
+
].view(batch_size, TOPK)
|
| 298 |
+
|
| 299 |
+
valid_topk = gathered_scores != float("-inf")
|
| 300 |
+
if actual_k < TOPK:
|
| 301 |
+
pad_mask = cache[key_topk].unsqueeze(0) >= actual_k
|
| 302 |
+
valid_topk = valid_topk & ~pad_mask
|
| 303 |
+
|
| 304 |
+
needs_sequential = seq_lens <= TOPK
|
| 305 |
+
sequential_indices = cache[key_topk].unsqueeze(0).expand(batch_size, -1)
|
| 306 |
+
sequential_valid = sequential_indices < seq_lens.unsqueeze(1)
|
| 307 |
+
|
| 308 |
+
seq_indices_or_neg1 = sequential_indices.clone()
|
| 309 |
+
seq_indices_or_neg1.masked_fill_(~sequential_valid, -1)
|
| 310 |
+
|
| 311 |
+
needs_seq_mask = needs_sequential.unsqueeze(1).expand(-1, TOPK)
|
| 312 |
+
raw_indices = torch.where(needs_seq_mask, seq_indices_or_neg1, raw_indices)
|
| 313 |
+
valid_topk = torch.where(needs_seq_mask, sequential_valid, valid_topk)
|
| 314 |
+
|
| 315 |
+
page_idx = raw_indices >> page_bits
|
| 316 |
+
offset_in_page = raw_indices & page_mask
|
| 317 |
+
|
| 318 |
+
page_idx_clamped = torch.clamp(page_idx, min=0)
|
| 319 |
+
physical_pages = torch.gather(page_tables, dim=1, index=page_idx_clamped.long())
|
| 320 |
+
|
| 321 |
+
page_indices = (physical_pages << page_bits) | offset_in_page
|
| 322 |
+
page_indices = page_indices.to(torch.int32)
|
| 323 |
+
page_indices.masked_fill_(~valid_topk, -1)
|
| 324 |
+
|
| 325 |
+
out_page_indices.copy_(page_indices)
|
| 326 |
+
|
| 327 |
+
if out_raw_indices is not None:
|
| 328 |
+
raw_indices = raw_indices.clone()
|
| 329 |
+
raw_indices.masked_fill_(~valid_topk, -1)
|
| 330 |
+
out_raw_indices.copy_(raw_indices)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
class C4IndexerBackendMixin:
|
| 334 |
+
def __init__(self):
|
| 335 |
+
super().__init__()
|
| 336 |
+
self.debug_use_external_c4_sparse_indices: bool = False
|
| 337 |
+
|
| 338 |
+
def _forward_prepare_multi_stream(
|
| 339 |
+
self,
|
| 340 |
+
x: torch.Tensor,
|
| 341 |
+
q_lora: torch.Tensor,
|
| 342 |
+
c4_indexer: C4Indexer,
|
| 343 |
+
positions: torch.Tensor,
|
| 344 |
+
forward_batch: ForwardBatch,
|
| 345 |
+
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
| 346 |
+
q_lora_ready: Optional[torch.cuda.Event] = None,
|
| 347 |
+
) -> Tuple[IndexerQuery, torch.Tensor]:
|
| 348 |
+
if TYPE_CHECKING:
|
| 349 |
+
assert isinstance(self, CompressorBackendMixin)
|
| 350 |
+
|
| 351 |
+
assert alt_streams is not None
|
| 352 |
+
assert len(alt_streams) >= 2
|
| 353 |
+
current_stream = torch.cuda.current_stream()
|
| 354 |
+
stream_q = alt_streams[0]
|
| 355 |
+
stream_weights = alt_streams[1]
|
| 356 |
+
|
| 357 |
+
stream_q.wait_stream(current_stream)
|
| 358 |
+
stream_weights.wait_stream(current_stream)
|
| 359 |
+
|
| 360 |
+
self.forward_indexer_compressor(
|
| 361 |
+
x=x,
|
| 362 |
+
forward_batch=forward_batch,
|
| 363 |
+
layer_id=c4_indexer.layer_id,
|
| 364 |
+
compressor=c4_indexer.compressor,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
# The weight projection is small and fast; compute it on its own
|
| 368 |
+
# stream, then have the Q stream wait on it before launching the big
|
| 369 |
+
# fused Q kernel (which folds rope, hadamard, quantization, and
|
| 370 |
+
# weight scaling into one pass).
|
| 371 |
+
with torch.cuda.stream(stream_weights):
|
| 372 |
+
weights = c4_indexer.compute_weights(x, skip_scale=True)
|
| 373 |
+
weights_ready = stream_weights.record_event()
|
| 374 |
+
|
| 375 |
+
with torch.cuda.stream(stream_q):
|
| 376 |
+
if q_lora_ready is not None:
|
| 377 |
+
stream_q.wait_event(q_lora_ready)
|
| 378 |
+
stream_q.wait_event(weights_ready)
|
| 379 |
+
q, weights = c4_indexer.compute_q(q_lora, positions, weights)
|
| 380 |
+
|
| 381 |
+
current_stream.wait_stream(stream_q)
|
| 382 |
+
return q, weights
|
| 383 |
+
|
| 384 |
+
def _forward_prepare_normal(
|
| 385 |
+
self,
|
| 386 |
+
x: torch.Tensor,
|
| 387 |
+
q_lora: torch.Tensor,
|
| 388 |
+
c4_indexer: C4Indexer,
|
| 389 |
+
positions: torch.Tensor,
|
| 390 |
+
forward_batch: ForwardBatch,
|
| 391 |
+
skip_compressor: bool = False,
|
| 392 |
+
) -> Tuple[IndexerQuery, torch.Tensor]:
|
| 393 |
+
if TYPE_CHECKING:
|
| 394 |
+
assert isinstance(self, CompressorBackendMixin)
|
| 395 |
+
|
| 396 |
+
weights = c4_indexer.compute_weights(x, skip_scale=True)
|
| 397 |
+
q, weights = c4_indexer.compute_q(q_lora, positions, weights)
|
| 398 |
+
if not skip_compressor:
|
| 399 |
+
self.forward_indexer_compressor(
|
| 400 |
+
x=x,
|
| 401 |
+
forward_batch=forward_batch,
|
| 402 |
+
layer_id=c4_indexer.layer_id,
|
| 403 |
+
compressor=c4_indexer.compressor,
|
| 404 |
+
)
|
| 405 |
+
return q, weights
|
| 406 |
+
|
| 407 |
+
def _can_use_nonpaged_indexer(
|
| 408 |
+
self,
|
| 409 |
+
*,
|
| 410 |
+
c4_indexer: C4Indexer,
|
| 411 |
+
forward_batch: ForwardBatch,
|
| 412 |
+
indexer_metadata: PagedIndexerMetadata,
|
| 413 |
+
) -> bool:
|
| 414 |
+
if not envs.SGLANG_OPT_DSV4_NONPAGED_INDEXER.get():
|
| 415 |
+
return False
|
| 416 |
+
# This path calls CUDA DeepGEMM and assumes the CUDA FP8+FP32 packed
|
| 417 |
+
# indexer cache layout. Explicitly reject HIP, NPU, and other devices.
|
| 418 |
+
if not is_cuda() or is_hip():
|
| 419 |
+
return False
|
| 420 |
+
# The gather plan is built from eager, child-local ForwardBatch metadata.
|
| 421 |
+
# Rewritten, TBO-split, and graph-backed batches must use the paged path.
|
| 422 |
+
if (
|
| 423 |
+
forward_batch.forward_mode != ForwardMode.EXTEND
|
| 424 |
+
or forward_batch._original_forward_mode is not None
|
| 425 |
+
or forward_batch.tbo_parent_token_range is not None
|
| 426 |
+
or forward_batch.batch_size != 1
|
| 427 |
+
or indexer_metadata.use_prefill_cuda_graph
|
| 428 |
+
):
|
| 429 |
+
return False
|
| 430 |
+
if (
|
| 431 |
+
c4_indexer.use_fp4_indexer
|
| 432 |
+
or envs.SGLANG_OPT_USE_TILELANG_INDEXER.get()
|
| 433 |
+
or envs.SGLANG_OPT_USE_AITER_INDEXER.get()
|
| 434 |
+
or envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get()
|
| 435 |
+
):
|
| 436 |
+
return False
|
| 437 |
+
if (
|
| 438 |
+
get_parallel().attn_cp_size != 1
|
| 439 |
+
or self.hisparse_coordinator is not None
|
| 440 |
+
or is_in_tc_piecewise_cuda_graph()
|
| 441 |
+
or is_in_breakable_cuda_graph()
|
| 442 |
+
):
|
| 443 |
+
return False
|
| 444 |
+
return not torch.cuda.is_current_stream_capturing()
|
| 445 |
+
|
| 446 |
+
def _get_nonpaged_indexer_plan(
|
| 447 |
+
self,
|
| 448 |
+
*,
|
| 449 |
+
c4_indexer: C4Indexer,
|
| 450 |
+
forward_batch: ForwardBatch,
|
| 451 |
+
indexer_metadata: PagedIndexerMetadata,
|
| 452 |
+
page_table: torch.Tensor,
|
| 453 |
+
c4_seq_lens: torch.Tensor,
|
| 454 |
+
query_rows: int,
|
| 455 |
+
) -> Optional[NonPagedIndexerPlan]:
|
| 456 |
+
if query_rows < envs.SGLANG_OPT_DSV4_NONPAGED_INDEXER_MIN_QUERY_TOKENS.get():
|
| 457 |
+
return None
|
| 458 |
+
if not self._can_use_nonpaged_indexer(
|
| 459 |
+
c4_indexer=c4_indexer,
|
| 460 |
+
forward_batch=forward_batch,
|
| 461 |
+
indexer_metadata=indexer_metadata,
|
| 462 |
+
):
|
| 463 |
+
return None
|
| 464 |
+
if indexer_metadata.nonpaged_plan is not None:
|
| 465 |
+
return indexer_metadata.nonpaged_plan
|
| 466 |
+
|
| 467 |
+
if (
|
| 468 |
+
forward_batch.seq_lens is None
|
| 469 |
+
or forward_batch.seq_lens_cpu is None
|
| 470 |
+
or forward_batch.extend_seq_lens_cpu is None
|
| 471 |
+
or forward_batch.extend_seq_lens is None
|
| 472 |
+
or forward_batch.extend_start_loc is None
|
| 473 |
+
or forward_batch.extend_num_tokens is None
|
| 474 |
+
):
|
| 475 |
+
return None
|
| 476 |
+
|
| 477 |
+
def to_cpu_int_list(values) -> Optional[List[int]]:
|
| 478 |
+
if isinstance(values, torch.Tensor):
|
| 479 |
+
if values.device.type != "cpu":
|
| 480 |
+
return None
|
| 481 |
+
values = values.tolist()
|
| 482 |
+
return [int(value) for value in values]
|
| 483 |
+
|
| 484 |
+
extend_lens_cpu = to_cpu_int_list(forward_batch.extend_seq_lens_cpu)
|
| 485 |
+
seq_lens_cpu = to_cpu_int_list(forward_batch.seq_lens_cpu)
|
| 486 |
+
if (
|
| 487 |
+
extend_lens_cpu is None
|
| 488 |
+
or seq_lens_cpu is None
|
| 489 |
+
or len(extend_lens_cpu) != 1
|
| 490 |
+
or len(seq_lens_cpu) != 1
|
| 491 |
+
or extend_lens_cpu[0] <= 0
|
| 492 |
+
):
|
| 493 |
+
return None
|
| 494 |
+
|
| 495 |
+
actual_queries = extend_lens_cpu[0]
|
| 496 |
+
if (
|
| 497 |
+
actual_queries != query_rows
|
| 498 |
+
or int(forward_batch.extend_num_tokens) != query_rows
|
| 499 |
+
or forward_batch.seq_lens.numel() != 1
|
| 500 |
+
or forward_batch.extend_seq_lens.numel() != 1
|
| 501 |
+
or forward_batch.extend_start_loc.numel() != 1
|
| 502 |
+
or page_table.dim() != 2
|
| 503 |
+
or page_table.shape[0] < query_rows
|
| 504 |
+
or c4_seq_lens.numel() < query_rows
|
| 505 |
+
):
|
| 506 |
+
return None
|
| 507 |
+
|
| 508 |
+
final_c4_len = seq_lens_cpu[0] // 4
|
| 509 |
+
if final_c4_len <= 0:
|
| 510 |
+
return None
|
| 511 |
+
|
| 512 |
+
request_page_table = page_table[:1].contiguous()
|
| 513 |
+
ke = c4_seq_lens[:query_rows].reshape(-1).to(torch.int32).contiguous()
|
| 514 |
+
gather_seq_lens = ke[-1:]
|
| 515 |
+
ks = torch.zeros_like(ke)
|
| 516 |
+
c4_page_size = indexer_metadata.c4_page_size
|
| 517 |
+
max_seqlen_k = (final_c4_len + c4_page_size - 1) // c4_page_size * c4_page_size
|
| 518 |
+
plan = NonPagedIndexerPlan(
|
| 519 |
+
page_table=request_page_table,
|
| 520 |
+
gather_seq_lens=gather_seq_lens,
|
| 521 |
+
ks=ks,
|
| 522 |
+
ke=ke,
|
| 523 |
+
seq_len_sum=final_c4_len,
|
| 524 |
+
max_seq_len=final_c4_len,
|
| 525 |
+
max_seqlen_k=max_seqlen_k,
|
| 526 |
+
query_rows=query_rows,
|
| 527 |
+
)
|
| 528 |
+
indexer_metadata.nonpaged_plan = plan
|
| 529 |
+
return plan
|
| 530 |
+
|
| 531 |
+
@staticmethod
|
| 532 |
+
def _forward_nonpaged_indexer(
|
| 533 |
+
*,
|
| 534 |
+
q_indexer: torch.Tensor,
|
| 535 |
+
weights: torch.Tensor,
|
| 536 |
+
c4_indexer: C4Indexer,
|
| 537 |
+
token_to_kv_pool: DeepSeekV4TokenToKVPool,
|
| 538 |
+
plan: NonPagedIndexerPlan,
|
| 539 |
+
) -> torch.Tensor:
|
| 540 |
+
import deep_gemm
|
| 541 |
+
|
| 542 |
+
k_u8, scale_u8 = token_to_kv_pool.get_index_k_scale_buffer(
|
| 543 |
+
layer_id=c4_indexer.layer_id,
|
| 544 |
+
seq_len_tensor=plan.gather_seq_lens,
|
| 545 |
+
page_indices=plan.page_table,
|
| 546 |
+
seq_len_sum=plan.seq_len_sum,
|
| 547 |
+
max_seq_len=plan.max_seq_len,
|
| 548 |
+
)
|
| 549 |
+
k_fp8 = k_u8.view(FP8_DTYPE)
|
| 550 |
+
k_scale = scale_u8.view(torch.float32).squeeze(-1)
|
| 551 |
+
return deep_gemm.fp8_mqa_logits(
|
| 552 |
+
q_indexer[: plan.query_rows],
|
| 553 |
+
(k_fp8, k_scale),
|
| 554 |
+
weights[: plan.query_rows],
|
| 555 |
+
plan.ks,
|
| 556 |
+
plan.ke,
|
| 557 |
+
clean_logits=False,
|
| 558 |
+
max_seqlen_k=plan.max_seqlen_k,
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
def forward_c4_indexer(
|
| 562 |
+
self,
|
| 563 |
+
x: torch.Tensor,
|
| 564 |
+
q_lora: torch.Tensor,
|
| 565 |
+
c4_indexer: C4Indexer,
|
| 566 |
+
forward_batch: ForwardBatch,
|
| 567 |
+
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
| 568 |
+
enable_multi_stream: bool = False,
|
| 569 |
+
q_lora_ready: Optional[torch.cuda.Event] = None,
|
| 570 |
+
skip_compressor: bool = False,
|
| 571 |
+
) -> None:
|
| 572 |
+
if forward_batch.forward_mode.is_idle():
|
| 573 |
+
return
|
| 574 |
+
token_to_kv_pool = self.token_to_kv_pool
|
| 575 |
+
|
| 576 |
+
if TYPE_CHECKING:
|
| 577 |
+
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
| 578 |
+
assert isinstance(self, CompressorBackendMixin)
|
| 579 |
+
|
| 580 |
+
metadata = self.forward_metadata
|
| 581 |
+
indexer_metadata = metadata.indexer_metadata
|
| 582 |
+
core_metadata = metadata.core_metadata
|
| 583 |
+
|
| 584 |
+
assert isinstance(indexer_metadata, PagedIndexerMetadata)
|
| 585 |
+
|
| 586 |
+
positions = core_metadata.positions
|
| 587 |
+
num_queries = min(x.shape[0], q_lora.shape[0], positions.shape[0])
|
| 588 |
+
if x.shape[0] != num_queries:
|
| 589 |
+
x = x[:num_queries]
|
| 590 |
+
if q_lora.shape[0] != num_queries:
|
| 591 |
+
q_lora = q_lora[:num_queries]
|
| 592 |
+
if positions.shape[0] != num_queries:
|
| 593 |
+
positions = positions[:num_queries]
|
| 594 |
+
|
| 595 |
+
if enable_multi_stream:
|
| 596 |
+
q_indexer, weights = self._forward_prepare_multi_stream(
|
| 597 |
+
x=x,
|
| 598 |
+
q_lora=q_lora,
|
| 599 |
+
c4_indexer=c4_indexer,
|
| 600 |
+
positions=positions,
|
| 601 |
+
forward_batch=forward_batch,
|
| 602 |
+
alt_streams=alt_streams,
|
| 603 |
+
q_lora_ready=q_lora_ready,
|
| 604 |
+
)
|
| 605 |
+
else:
|
| 606 |
+
assert q_lora_ready is None
|
| 607 |
+
q_indexer, weights = self._forward_prepare_normal(
|
| 608 |
+
x=x,
|
| 609 |
+
q_lora=q_lora,
|
| 610 |
+
c4_indexer=c4_indexer,
|
| 611 |
+
positions=positions,
|
| 612 |
+
forward_batch=forward_batch,
|
| 613 |
+
skip_compressor=skip_compressor,
|
| 614 |
+
)
|
| 615 |
+
|
| 616 |
+
use_fp4_indexer = c4_indexer.use_fp4_indexer
|
| 617 |
+
|
| 618 |
+
if use_fp4_indexer:
|
| 619 |
+
q_fp4, q_sf = q_indexer
|
| 620 |
+
assert len(q_fp4.shape) == 3
|
| 621 |
+
assert len(q_sf.shape) == 2
|
| 622 |
+
q = (q_fp4.unsqueeze(1), q_sf.unsqueeze(1))
|
| 623 |
+
else:
|
| 624 |
+
assert len(q_indexer.shape) == 3
|
| 625 |
+
q = q_indexer.unsqueeze(1)
|
| 626 |
+
|
| 627 |
+
assert len(weights.shape) == 3
|
| 628 |
+
weights = weights.squeeze(2)
|
| 629 |
+
if use_fp4_indexer:
|
| 630 |
+
weights = weights.float()
|
| 631 |
+
if envs.SGLANG_OPT_USE_TILELANG_INDEXER.get():
|
| 632 |
+
raise RuntimeError("DeepSeek V4 FP4 indexer requires DeepGEMM indexer.")
|
| 633 |
+
from deep_gemm import fp8_fp4_paged_mqa_logits as fn
|
| 634 |
+
elif envs.SGLANG_OPT_USE_TILELANG_INDEXER.get():
|
| 635 |
+
from sglang.kernels.ops.attention.dsa.tilelang_kernel import (
|
| 636 |
+
tilelang_fp8_paged_mqa_logits as fn,
|
| 637 |
+
)
|
| 638 |
+
elif envs.SGLANG_OPT_USE_AITER_INDEXER.get():
|
| 639 |
+
fn = _aiter_fp8_paged_mqa_logits
|
| 640 |
+
elif envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get():
|
| 641 |
+
if is_sm120_supported():
|
| 642 |
+
fn = fp8_paged_mqa_logits_torch_sm120
|
| 643 |
+
else:
|
| 644 |
+
fn = fp8_paged_mqa_logits_torch
|
| 645 |
+
elif is_xpu():
|
| 646 |
+
from sgl_kernel import fp8_paged_mqa_logits_triton
|
| 647 |
+
|
| 648 |
+
# TODO: switch from triton to SYCL when OOM is resolved
|
| 649 |
+
|
| 650 |
+
fn = fp8_paged_mqa_logits_triton
|
| 651 |
+
else:
|
| 652 |
+
from deep_gemm import fp8_paged_mqa_logits as fn
|
| 653 |
+
|
| 654 |
+
query_rows = q_indexer[0].shape[0] if use_fp4_indexer else q_indexer.shape[0]
|
| 655 |
+
|
| 656 |
+
def match_num_queries(tensor: torch.Tensor, value: int) -> torch.Tensor:
|
| 657 |
+
if tensor.shape[0] == query_rows:
|
| 658 |
+
return tensor
|
| 659 |
+
if tensor.shape[0] > query_rows:
|
| 660 |
+
return tensor[:query_rows]
|
| 661 |
+
pad = (0, 0) * (tensor.dim() - 1) + (0, query_rows - tensor.shape[0])
|
| 662 |
+
return F.pad(tensor, pad, value=value)
|
| 663 |
+
|
| 664 |
+
c4_seq_lens = match_num_queries(indexer_metadata.c4_seq_lens, value=1)
|
| 665 |
+
_c4sl = c4_seq_lens
|
| 666 |
+
page_table = match_num_queries(indexer_metadata.page_table, value=0)
|
| 667 |
+
c4_sparse_page_indices = match_num_queries(
|
| 668 |
+
core_metadata.c4_sparse_page_indices, value=-1
|
| 669 |
+
)
|
| 670 |
+
_use_tilelang = (
|
| 671 |
+
envs.SGLANG_OPT_USE_TILELANG_INDEXER.get() and not use_fp4_indexer
|
| 672 |
+
)
|
| 673 |
+
_use_aiter = envs.SGLANG_OPT_USE_AITER_INDEXER.get() and not use_fp4_indexer
|
| 674 |
+
if _c4sl.dim() == 1 and not _use_tilelang and not _use_aiter:
|
| 675 |
+
_c4sl = _c4sl.unsqueeze(-1)
|
| 676 |
+
nonpaged_plan = self._get_nonpaged_indexer_plan(
|
| 677 |
+
c4_indexer=c4_indexer,
|
| 678 |
+
forward_batch=forward_batch,
|
| 679 |
+
indexer_metadata=indexer_metadata,
|
| 680 |
+
page_table=page_table,
|
| 681 |
+
c4_seq_lens=c4_seq_lens,
|
| 682 |
+
query_rows=query_rows,
|
| 683 |
+
)
|
| 684 |
+
if nonpaged_plan is not None:
|
| 685 |
+
assert isinstance(q_indexer, torch.Tensor)
|
| 686 |
+
logits = self._forward_nonpaged_indexer(
|
| 687 |
+
q_indexer=q_indexer,
|
| 688 |
+
weights=weights,
|
| 689 |
+
c4_indexer=c4_indexer,
|
| 690 |
+
token_to_kv_pool=token_to_kv_pool,
|
| 691 |
+
plan=nonpaged_plan,
|
| 692 |
+
)
|
| 693 |
+
else:
|
| 694 |
+
c4_indexer_kv_cache = token_to_kv_pool.get_index_k_with_scale_buffer(
|
| 695 |
+
layer_id=c4_indexer.layer_id,
|
| 696 |
+
)
|
| 697 |
+
assert c4_indexer_kv_cache.dim() == 2
|
| 698 |
+
head_dim_with_sf = 68 if use_fp4_indexer else 132
|
| 699 |
+
c4_indexer_kv_cache = c4_indexer_kv_cache.view(
|
| 700 |
+
c4_indexer_kv_cache.shape[0], 64, 1, head_dim_with_sf
|
| 701 |
+
)
|
| 702 |
+
logits = fn(
|
| 703 |
+
q,
|
| 704 |
+
c4_indexer_kv_cache,
|
| 705 |
+
weights,
|
| 706 |
+
_c4sl,
|
| 707 |
+
page_table,
|
| 708 |
+
indexer_metadata.deep_gemm_metadata,
|
| 709 |
+
indexer_metadata.max_c4_seq_len,
|
| 710 |
+
False,
|
| 711 |
+
)
|
| 712 |
+
|
| 713 |
+
assert indexer_metadata.page_table is core_metadata.page_table
|
| 714 |
+
if self.debug_use_external_c4_sparse_indices:
|
| 715 |
+
return
|
| 716 |
+
|
| 717 |
+
indexer_capturer = get_global_indexer_capturer()
|
| 718 |
+
capture_enabled = indexer_capturer is not None
|
| 719 |
+
|
| 720 |
+
hisparse_coordinator = self.hisparse_coordinator
|
| 721 |
+
hisparse_decode = (
|
| 722 |
+
hisparse_coordinator is not None and forward_batch.forward_mode.is_decode()
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
raw_indices = None
|
| 726 |
+
if capture_enabled:
|
| 727 |
+
raw_indices = torch.empty_like(c4_sparse_page_indices)
|
| 728 |
+
elif hisparse_decode:
|
| 729 |
+
raw_indices = hisparse_coordinator.raw_indices_buffer[
|
| 730 |
+
: c4_sparse_page_indices.size(0)
|
| 731 |
+
]
|
| 732 |
+
elif core_metadata.c4_sparse_raw_indices is not None:
|
| 733 |
+
raw_indices = core_metadata.c4_sparse_raw_indices
|
| 734 |
+
|
| 735 |
+
if envs.SGLANG_TOPK_TRANSFORM_512_TORCH.get():
|
| 736 |
+
topk_transform_512_pytorch_vectorized(
|
| 737 |
+
logits,
|
| 738 |
+
c4_seq_lens,
|
| 739 |
+
page_table,
|
| 740 |
+
c4_sparse_page_indices,
|
| 741 |
+
indexer_metadata.c4_page_size,
|
| 742 |
+
raw_indices,
|
| 743 |
+
)
|
| 744 |
+
elif envs.SGLANG_OPT_USE_TOPK_V2.get() and raw_indices is None:
|
| 745 |
+
topk_transform_512_v2(
|
| 746 |
+
logits,
|
| 747 |
+
c4_seq_lens,
|
| 748 |
+
page_table,
|
| 749 |
+
c4_sparse_page_indices,
|
| 750 |
+
indexer_metadata.c4_page_size,
|
| 751 |
+
indexer_metadata.topk_metadata,
|
| 752 |
+
)
|
| 753 |
+
else:
|
| 754 |
+
topk_transform_512(
|
| 755 |
+
logits,
|
| 756 |
+
c4_seq_lens,
|
| 757 |
+
page_table,
|
| 758 |
+
c4_sparse_page_indices,
|
| 759 |
+
indexer_metadata.c4_page_size,
|
| 760 |
+
raw_indices,
|
| 761 |
+
)
|
| 762 |
+
if hisparse_coordinator is not None:
|
| 763 |
+
if hisparse_decode:
|
| 764 |
+
compress_layer_id = token_to_kv_pool.layer_mapping[
|
| 765 |
+
c4_indexer.layer_id
|
| 766 |
+
].compress_layer_id
|
| 767 |
+
core_metadata.c4_sparse_page_indices = (
|
| 768 |
+
hisparse_coordinator.swap_in_selected_pages(
|
| 769 |
+
req_pool_indices=forward_batch.req_pool_indices,
|
| 770 |
+
compressed_seq_lens=indexer_metadata.c4_seq_lens,
|
| 771 |
+
top_k_result=raw_indices,
|
| 772 |
+
layer_id=compress_layer_id,
|
| 773 |
+
)
|
| 774 |
+
)
|
| 775 |
+
else:
|
| 776 |
+
# flash_mla C4 attention requires int32 page indices.
|
| 777 |
+
core_metadata.c4_sparse_page_indices = (
|
| 778 |
+
token_to_kv_pool.c4_kv_pool.translate_loc_to_hisparse_device(
|
| 779 |
+
core_metadata.c4_sparse_page_indices
|
| 780 |
+
).to(torch.int32)
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
if capture_enabled:
|
| 784 |
+
compress_layer_id = token_to_kv_pool.layer_mapping[
|
| 785 |
+
c4_indexer.layer_id
|
| 786 |
+
].compress_layer_id
|
| 787 |
+
indexer_capturer.capture(compress_layer_id, raw_indices)
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
class C4Indexer(nn.Module):
|
| 791 |
+
def __init__(
|
| 792 |
+
self,
|
| 793 |
+
config: AgnesConfig,
|
| 794 |
+
layer_id: int,
|
| 795 |
+
freqs_cis: torch.Tensor,
|
| 796 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 797 |
+
prefix: str = "",
|
| 798 |
+
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
| 799 |
+
rotary_emb=None,
|
| 800 |
+
):
|
| 801 |
+
super().__init__()
|
| 802 |
+
self.layer_id = layer_id
|
| 803 |
+
self.dim = config.hidden_size
|
| 804 |
+
self.n_heads = config.index_n_heads
|
| 805 |
+
self.head_dim = config.index_head_dim
|
| 806 |
+
self.rope_head_dim = config.qk_rope_head_dim
|
| 807 |
+
self.index_topk = config.index_topk
|
| 808 |
+
self.q_lora_rank = config.q_lora_rank
|
| 809 |
+
self.softmax_scale = self.head_dim**-0.5
|
| 810 |
+
self.n_local_heads = self.n_heads
|
| 811 |
+
self.wq_b = ReplicatedLinear(
|
| 812 |
+
self.q_lora_rank,
|
| 813 |
+
self.n_heads * self.head_dim,
|
| 814 |
+
bias=False,
|
| 815 |
+
quant_config=quant_config,
|
| 816 |
+
params_dtype=torch.bfloat16,
|
| 817 |
+
prefix=add_prefix("wq_b", prefix),
|
| 818 |
+
)
|
| 819 |
+
self.weights_proj = ReplicatedLinear(
|
| 820 |
+
self.dim,
|
| 821 |
+
self.n_heads,
|
| 822 |
+
bias=False,
|
| 823 |
+
quant_config=None,
|
| 824 |
+
params_dtype=torch.bfloat16,
|
| 825 |
+
prefix=add_prefix("weights_proj", prefix),
|
| 826 |
+
)
|
| 827 |
+
self.compressor = Compressor(
|
| 828 |
+
config,
|
| 829 |
+
self.layer_id,
|
| 830 |
+
True,
|
| 831 |
+
freqs_cis,
|
| 832 |
+
compress_ratio=4,
|
| 833 |
+
head_dim=self.head_dim,
|
| 834 |
+
rotate=True,
|
| 835 |
+
prefix=add_prefix("compressor", prefix),
|
| 836 |
+
rotary_emb=rotary_emb,
|
| 837 |
+
)
|
| 838 |
+
self.rotary_emb = rotary_emb
|
| 839 |
+
self.freqs_cis = freqs_cis
|
| 840 |
+
self.weight_scale: float = self.softmax_scale * self.n_heads**-0.5
|
| 841 |
+
from sglang.srt.runtime_context import get_server_args
|
| 842 |
+
|
| 843 |
+
self.use_fp4_indexer = get_server_args().enable_deepseek_v4_fp4_indexer
|
| 844 |
+
self.alt_streams = alt_streams
|
| 845 |
+
|
| 846 |
+
def compute_q(
|
| 847 |
+
self,
|
| 848 |
+
q_lora: torch.Tensor,
|
| 849 |
+
positions: torch.Tensor,
|
| 850 |
+
weight: torch.Tensor,
|
| 851 |
+
) -> Tuple[IndexerQuery, torch.Tensor]:
|
| 852 |
+
q, _ = self.wq_b(q_lora)
|
| 853 |
+
q = q.view(-1, self.n_local_heads, self.head_dim)
|
| 854 |
+
if self.use_fp4_indexer:
|
| 855 |
+
return fused_q_indexer_rope_hadamard_fp4_quant(
|
| 856 |
+
q.contiguous(), weight, self.weight_scale, self.freqs_cis, positions
|
| 857 |
+
)
|
| 858 |
+
return fused_q_indexer_rope_hadamard_quant(
|
| 859 |
+
q, weight, self.weight_scale, self.freqs_cis, positions
|
| 860 |
+
)
|
| 861 |
+
|
| 862 |
+
def compute_weights(self, x: torch.Tensor, skip_scale=False) -> torch.Tensor:
|
| 863 |
+
out, _ = self.weights_proj(x)
|
| 864 |
+
if not skip_scale:
|
| 865 |
+
out = out * self.weight_scale
|
| 866 |
+
return out
|
| 867 |
+
|
| 868 |
+
def forward(
|
| 869 |
+
self,
|
| 870 |
+
x: torch.Tensor,
|
| 871 |
+
q_lora: torch.Tensor,
|
| 872 |
+
forward_batch: ForwardBatch,
|
| 873 |
+
attn_backend: AttentionBackend,
|
| 874 |
+
enable_multi_stream: bool = False,
|
| 875 |
+
q_lora_ready: Optional[torch.cuda.Event] = None,
|
| 876 |
+
skip_compressor: bool = False,
|
| 877 |
+
) -> None:
|
| 878 |
+
return attn_backend.forward_c4_indexer(
|
| 879 |
+
x=x,
|
| 880 |
+
q_lora=q_lora,
|
| 881 |
+
forward_batch=forward_batch,
|
| 882 |
+
c4_indexer=self,
|
| 883 |
+
alt_streams=self.alt_streams,
|
| 884 |
+
enable_multi_stream=enable_multi_stream,
|
| 885 |
+
q_lora_ready=q_lora_ready,
|
| 886 |
+
skip_compressor=skip_compressor,
|
| 887 |
+
)
|
sglang_patch/srt/layers/quantization/modelslim/modelslim.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from types import MappingProxyType
|
| 5 |
+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union, cast
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from sglang.srt.hardware_backend.npu.quantization.linear_method_npu import (
|
| 10 |
+
_NPULinearMethodBase,
|
| 11 |
+
)
|
| 12 |
+
from sglang.srt.layers.moe.moe_runner import MoeRunner, MoeRunnerConfig
|
| 13 |
+
from sglang.srt.layers.moe.utils import MoeRunnerBackend, get_moe_runner_backend
|
| 14 |
+
from sglang.srt.layers.quantization.base_config import (
|
| 15 |
+
FusedMoEMethodBase,
|
| 16 |
+
QuantizationConfig,
|
| 17 |
+
)
|
| 18 |
+
from sglang.srt.layers.quantization.modelslim.schemes import (
|
| 19 |
+
ModelSlimMXFP4Scheme,
|
| 20 |
+
ModelSlimMXFP4W4A8Scheme,
|
| 21 |
+
ModelSlimMXFP8Scheme,
|
| 22 |
+
ModelSlimW4A4Int4,
|
| 23 |
+
ModelSlimW4A4Int4MoE,
|
| 24 |
+
ModelSlimW4A8Int8MoE,
|
| 25 |
+
ModelSlimW8A8Int8,
|
| 26 |
+
ModelSlimW8A8Int8MoE,
|
| 27 |
+
)
|
| 28 |
+
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
|
| 29 |
+
from sglang.srt.utils import apply_module_patch
|
| 30 |
+
|
| 31 |
+
if TYPE_CHECKING:
|
| 32 |
+
from sglang.srt.layers.moe import MoeRunnerConfig
|
| 33 |
+
from sglang.srt.layers.moe.token_dispatcher import (
|
| 34 |
+
CombineInput,
|
| 35 |
+
StandardDispatchOutput,
|
| 36 |
+
)
|
| 37 |
+
from sglang.srt.layers.quantization.base_config import QuantizeMethodBase
|
| 38 |
+
from sglang.srt.layers.quantization.modelslim.schemes import (
|
| 39 |
+
ModelSlimLinearScheme,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
logger = logging.getLogger(__name__)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# func refers to RMSNorm.__init__
|
| 46 |
+
def npu_wrapper_rmsnorm_init(func):
|
| 47 |
+
def init(self, hidden_size: int, **extra_args) -> None:
|
| 48 |
+
func(self, hidden_size, **extra_args)
|
| 49 |
+
self.ignore_anti = True
|
| 50 |
+
# The Ascend w8a8_int8 quantization requires adding a bias in rmsnorm
|
| 51 |
+
self.bias = torch.nn.Parameter(torch.zeros(hidden_size), requires_grad=False)
|
| 52 |
+
|
| 53 |
+
return init
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# func refers to RMSNorm.forward_oot
|
| 57 |
+
def npu_wrapper_rmsnorm_forward(func):
|
| 58 |
+
def _rmsnorm_forward_oot(
|
| 59 |
+
self,
|
| 60 |
+
x: torch.Tensor,
|
| 61 |
+
residual: Optional[torch.Tensor] = None,
|
| 62 |
+
post_residual_addition: Optional[torch.Tensor] = None,
|
| 63 |
+
) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
| 64 |
+
if not x.is_contiguous():
|
| 65 |
+
x = x.contiguous()
|
| 66 |
+
if residual is not None:
|
| 67 |
+
if post_residual_addition is not None:
|
| 68 |
+
residual = residual + post_residual_addition
|
| 69 |
+
from sgl_kernel_npu.norm.add_rmsnorm_bias import add_rmsnorm_bias
|
| 70 |
+
|
| 71 |
+
out, residual_out = add_rmsnorm_bias(
|
| 72 |
+
x,
|
| 73 |
+
residual,
|
| 74 |
+
self.weight.data,
|
| 75 |
+
self.bias,
|
| 76 |
+
self.variance_epsilon,
|
| 77 |
+
)
|
| 78 |
+
return out.to(x.dtype), residual_out
|
| 79 |
+
|
| 80 |
+
out = torch.ops.npu.npu_rms_norm(x, self.weight.data, self.variance_epsilon)[0]
|
| 81 |
+
out = out + self.bias
|
| 82 |
+
return out.to(x.dtype)
|
| 83 |
+
|
| 84 |
+
return _rmsnorm_forward_oot
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class ModelSlimConfig(QuantizationConfig):
|
| 88 |
+
"""
|
| 89 |
+
Config class for ModelSlim Quantization, a NPU-specific quantization type.
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self, quant_config: Dict[str, Any] = {}):
|
| 93 |
+
super().__init__()
|
| 94 |
+
keys = [k for k in quant_config if isinstance(k, str)]
|
| 95 |
+
is_dsv4 = any(k.startswith("hc_head_") for k in keys)
|
| 96 |
+
if is_dsv4:
|
| 97 |
+
from sglang.srt.models.agnes import AgnesForCausalLM
|
| 98 |
+
|
| 99 |
+
remap = AgnesForCausalLM.remap_weight_name_to_dpsk_hf_format
|
| 100 |
+
quant_config = {
|
| 101 |
+
(remap(k) if isinstance(k, str) else k): v
|
| 102 |
+
for k, v in quant_config.items()
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
self.quant_description = quant_config
|
| 106 |
+
ignore = cast(List[str], quant_config.get("ignore", []))
|
| 107 |
+
self.ignore = ignore if ignore is not None else []
|
| 108 |
+
packed_modules_mapping = quant_config.get("packed_modules_mapping", {})
|
| 109 |
+
self.packed_modules_mapping = (
|
| 110 |
+
packed_modules_mapping if packed_modules_mapping is not None else {}
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
for name in self.quant_description.keys():
|
| 114 |
+
if "norm.bias" in name:
|
| 115 |
+
apply_module_patch(
|
| 116 |
+
"sglang.srt.layers.layernorm.RMSNorm",
|
| 117 |
+
"__init__",
|
| 118 |
+
[npu_wrapper_rmsnorm_init],
|
| 119 |
+
)
|
| 120 |
+
apply_module_patch(
|
| 121 |
+
"sglang.srt.layers.layernorm.RMSNorm",
|
| 122 |
+
"forward_npu",
|
| 123 |
+
[npu_wrapper_rmsnorm_forward],
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
def update_packed_modules_mapping(self, mapping: Dict[str, List[str]]) -> None:
|
| 127 |
+
self.packed_modules_mapping.update(mapping)
|
| 128 |
+
|
| 129 |
+
def get_linear_method(self) -> ModelSlimLinearMethod:
|
| 130 |
+
return ModelSlimLinearMethod(self)
|
| 131 |
+
|
| 132 |
+
@classmethod
|
| 133 |
+
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
|
| 134 |
+
return [torch.int8, torch.float16, torch.bfloat16]
|
| 135 |
+
|
| 136 |
+
@classmethod
|
| 137 |
+
def get_min_capability(cls) -> int:
|
| 138 |
+
return 0
|
| 139 |
+
|
| 140 |
+
@classmethod
|
| 141 |
+
def get_name(cls) -> str:
|
| 142 |
+
return "modelslim"
|
| 143 |
+
|
| 144 |
+
@classmethod
|
| 145 |
+
def get_config_filenames(cls) -> List[str]:
|
| 146 |
+
filenames = ["quant_model_description.json"]
|
| 147 |
+
return filenames
|
| 148 |
+
|
| 149 |
+
@classmethod
|
| 150 |
+
def from_config(cls, config: Dict[str, Any]) -> ModelSlimConfig:
|
| 151 |
+
return cls(config)
|
| 152 |
+
|
| 153 |
+
def get_quant_method(
|
| 154 |
+
self,
|
| 155 |
+
layer: torch.nn.Module,
|
| 156 |
+
prefix: str,
|
| 157 |
+
) -> Optional[QuantizeMethodBase]:
|
| 158 |
+
from sglang.srt.layers.linear import LinearBase
|
| 159 |
+
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
|
| 160 |
+
|
| 161 |
+
if isinstance(layer, LinearBase):
|
| 162 |
+
# TODO: we should remove this code and switch to the packed_modules_mapping declared inside the modeling files
|
| 163 |
+
key = "model"
|
| 164 |
+
if "vision_model" in prefix:
|
| 165 |
+
key = "vision_model"
|
| 166 |
+
elif "visual" in prefix:
|
| 167 |
+
key = "visual"
|
| 168 |
+
if "vision_tower" in prefix or "mm_projector" in prefix:
|
| 169 |
+
prefix = prefix.replace(r"attn.qkv_proj", r"wqkv")
|
| 170 |
+
prefix = prefix.replace(r"attn.proj", r"wo")
|
| 171 |
+
packed_modules_mapping_subset = self.packed_modules_mapping.get(key, {})
|
| 172 |
+
prefix_in_quant_config = prefix
|
| 173 |
+
proj_name = prefix.split(".")[-1]
|
| 174 |
+
if proj_name in packed_modules_mapping_subset:
|
| 175 |
+
prefix_in_quant_config = prefix.replace(
|
| 176 |
+
proj_name, packed_modules_mapping_subset[proj_name][0]
|
| 177 |
+
)
|
| 178 |
+
if self.is_layer_skipped(
|
| 179 |
+
prefix, packed_modules_mapping_subset
|
| 180 |
+
) or self.is_layer_skipped(prefix, self.packed_modules_mapping):
|
| 181 |
+
return UnquantizedLinearMethod()
|
| 182 |
+
layer.scheme = self.get_linear_scheme(layer, prefix_in_quant_config)
|
| 183 |
+
if layer.scheme is None:
|
| 184 |
+
return UnquantizedLinearMethod()
|
| 185 |
+
return ModelSlimLinearMethod(self)
|
| 186 |
+
elif isinstance(layer, FusedMoE):
|
| 187 |
+
moe_schemes = self.get_moe_scheme(layer, prefix)
|
| 188 |
+
if moe_schemes is None:
|
| 189 |
+
raise ValueError(f"No ModelSlim MoE scheme found for layer {prefix}")
|
| 190 |
+
layer.w13_scheme, layer.w2_scheme = moe_schemes
|
| 191 |
+
layer.w13_kernel, layer.w2_kernel = (
|
| 192 |
+
layer.w13_scheme.kernel,
|
| 193 |
+
layer.w2_scheme.kernel,
|
| 194 |
+
)
|
| 195 |
+
return ModelSlimFusedMoEMethod(self)
|
| 196 |
+
return None
|
| 197 |
+
|
| 198 |
+
def get_linear_scheme(
|
| 199 |
+
self, layer: torch.nn.Module, prefix: Optional[str] = None
|
| 200 |
+
) -> Optional[ModelSlimLinearScheme]:
|
| 201 |
+
"""
|
| 202 |
+
get_scheme method adjusted for modelslim, taken from
|
| 203 |
+
python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors.py
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
linear_quant_schemes = [
|
| 207 |
+
("W4A4_DYNAMIC", ModelSlimW4A4Int4),
|
| 208 |
+
("W8A8", ModelSlimW8A8Int8),
|
| 209 |
+
("W8A8_DYNAMIC", ModelSlimW8A8Int8),
|
| 210 |
+
("W8A8_MXFP8", ModelSlimMXFP8Scheme),
|
| 211 |
+
("W4A8_MXFP", ModelSlimMXFP4W4A8Scheme),
|
| 212 |
+
("W4A4_MXFP4", ModelSlimMXFP4Scheme),
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
quant_schemes = [self.quant_description.get(prefix + ".weight", "")]
|
| 216 |
+
|
| 217 |
+
for scheme_name, scheme_class in linear_quant_schemes:
|
| 218 |
+
if any(s == scheme_name for s in quant_schemes):
|
| 219 |
+
logger.info_once(f"Using {scheme_class.__name__}")
|
| 220 |
+
return scheme_class(quant_config=self.quant_description, prefix=prefix)
|
| 221 |
+
|
| 222 |
+
logger.warning(
|
| 223 |
+
f"Unsupported Linear modelslim scheme: "
|
| 224 |
+
f"{quant_schemes} in layer: {prefix}"
|
| 225 |
+
)
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
def get_moe_scheme(
|
| 229 |
+
self,
|
| 230 |
+
layer: torch.nn.Module,
|
| 231 |
+
prefix: str,
|
| 232 |
+
):
|
| 233 |
+
moe_quant_schemes = [
|
| 234 |
+
("W4A4_DYNAMIC", ModelSlimW4A4Int4MoE),
|
| 235 |
+
("W4A8_DYNAMIC", ModelSlimW4A8Int8MoE),
|
| 236 |
+
("W8A8_DYNAMIC", ModelSlimW8A8Int8MoE),
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
# Try multiple naming conventions:
|
| 240 |
+
# (gate_proj, up_proj, down_proj) – standard compressed-tensors format
|
| 241 |
+
# (w1, w3, w2) – MiniMax-M2.5 / some other models
|
| 242 |
+
naming_conventions = [
|
| 243 |
+
("gate_proj", "up_proj", "down_proj"),
|
| 244 |
+
("w1", "w3", "w2"),
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
w13_scheme_name = None
|
| 248 |
+
w2_scheme_name = None
|
| 249 |
+
for gate_name, up_name, down_name in naming_conventions:
|
| 250 |
+
w13_keys = [
|
| 251 |
+
f"{prefix}.0.{gate_name}.weight",
|
| 252 |
+
f"{prefix}.0.{up_name}.weight",
|
| 253 |
+
]
|
| 254 |
+
w2_key = f"{prefix}.0.{down_name}.weight"
|
| 255 |
+
w13_entries = {
|
| 256 |
+
key: self.quant_description[key]
|
| 257 |
+
for key in w13_keys
|
| 258 |
+
if key in self.quant_description
|
| 259 |
+
}
|
| 260 |
+
if w13_entries and w2_key in self.quant_description:
|
| 261 |
+
w13_names = list(w13_entries.values())
|
| 262 |
+
# For w13, both projections must agree on the scheme
|
| 263 |
+
unique_w13 = set(w13_names)
|
| 264 |
+
if len(unique_w13) > 1:
|
| 265 |
+
raise ValueError(
|
| 266 |
+
f"Mismatched ModelSlim quantization for W13 in layer {prefix}: "
|
| 267 |
+
f"{w13_entries}"
|
| 268 |
+
)
|
| 269 |
+
w13_scheme_name = w13_names[0]
|
| 270 |
+
w2_scheme_name = self.quant_description[w2_key]
|
| 271 |
+
break
|
| 272 |
+
|
| 273 |
+
if w13_scheme_name is None:
|
| 274 |
+
# Build a helpful error message listing all attempted key patterns
|
| 275 |
+
all_attempted = []
|
| 276 |
+
for gate_name, up_name, down_name in naming_conventions:
|
| 277 |
+
w13_keys = [
|
| 278 |
+
f"{prefix}.0.{gate_name}.weight",
|
| 279 |
+
f"{prefix}.0.{up_name}.weight",
|
| 280 |
+
]
|
| 281 |
+
w2_key = f"{prefix}.0.{down_name}.weight"
|
| 282 |
+
w13_found = any(k in self.quant_description for k in w13_keys)
|
| 283 |
+
w2_found = w2_key in self.quant_description
|
| 284 |
+
status = (
|
| 285 |
+
f"({gate_name}/{up_name}={'found' if w13_found else 'missing'}, "
|
| 286 |
+
f"{down_name}={'found' if w2_found else 'missing'})"
|
| 287 |
+
)
|
| 288 |
+
all_attempted.append(status)
|
| 289 |
+
raise ValueError(
|
| 290 |
+
f"Missing ModelSlim MoE quantization description for layer {prefix}: "
|
| 291 |
+
+ "; ".join(all_attempted)
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Map scheme names to classes
|
| 295 |
+
scheme_map = dict(
|
| 296 |
+
moe_quant_schemes
|
| 297 |
+
) # dict: "W4A4_DYNAMIC" -> ModelSlimW4A4Int4MoE, etc.
|
| 298 |
+
|
| 299 |
+
# Instantiate the schemes
|
| 300 |
+
def instantiate(name, weight_group):
|
| 301 |
+
cls = scheme_map.get(name)
|
| 302 |
+
if cls is None:
|
| 303 |
+
logger.warning(f"Unsupported scheme '{name}' for layer {prefix}")
|
| 304 |
+
return None
|
| 305 |
+
return cls(self, weight_group)
|
| 306 |
+
|
| 307 |
+
w13_scheme = instantiate(w13_scheme_name, weight_group="w13")
|
| 308 |
+
w2_scheme = instantiate(w2_scheme_name, weight_group="w2")
|
| 309 |
+
if w13_scheme is None or w2_scheme is None:
|
| 310 |
+
raise ValueError(
|
| 311 |
+
f"Unsupported ModelSlim MoE schemes for layer {prefix}: "
|
| 312 |
+
f"W13='{w13_scheme_name}', W2='{w2_scheme_name}'"
|
| 313 |
+
)
|
| 314 |
+
logger.info_once(f"Using {type(w13_scheme).__name__} for W13")
|
| 315 |
+
logger.info_once(f"Using {type(w2_scheme).__name__} for W2")
|
| 316 |
+
|
| 317 |
+
return w13_scheme, w2_scheme
|
| 318 |
+
|
| 319 |
+
def is_layer_skipped(
|
| 320 |
+
self, prefix: str, fused_mapping: Mapping[str, List[str]] = MappingProxyType({})
|
| 321 |
+
):
|
| 322 |
+
# adapted from vllm.model_executor.layers.quantization.utils.quant_utils.is_layer_skipped
|
| 323 |
+
proj_name = prefix.split(".")[-1]
|
| 324 |
+
if proj_name in fused_mapping:
|
| 325 |
+
shard_prefixes = [
|
| 326 |
+
prefix.replace(proj_name, shard_proj_name)
|
| 327 |
+
for shard_proj_name in fused_mapping[proj_name]
|
| 328 |
+
]
|
| 329 |
+
|
| 330 |
+
is_skipped = None
|
| 331 |
+
for shard_prefix in shard_prefixes:
|
| 332 |
+
is_shard_skipped = (
|
| 333 |
+
self.quant_description.get(shard_prefix + ".weight", "") == "FLOAT"
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
if is_skipped is None:
|
| 337 |
+
is_skipped = is_shard_skipped
|
| 338 |
+
elif is_shard_skipped != is_skipped:
|
| 339 |
+
raise ValueError(
|
| 340 |
+
f"Detected some but not all shards of {prefix} "
|
| 341 |
+
"are quantized. All shards of fused layers "
|
| 342 |
+
"to have the same precision."
|
| 343 |
+
)
|
| 344 |
+
else:
|
| 345 |
+
is_skipped = self.quant_description.get(prefix + ".weight", "") == "FLOAT"
|
| 346 |
+
|
| 347 |
+
assert is_skipped is not None
|
| 348 |
+
return is_skipped
|
| 349 |
+
|
| 350 |
+
def get_scaled_act_names(self) -> List[str]:
|
| 351 |
+
return []
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class ModelSlimLinearMethod(_NPULinearMethodBase):
|
| 355 |
+
|
| 356 |
+
def __init__(self, quantization_config: ModelSlimConfig):
|
| 357 |
+
self.quantization_config = quantization_config
|
| 358 |
+
|
| 359 |
+
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
| 360 |
+
layer.scheme.process_weights_after_loading(layer)
|
| 361 |
+
|
| 362 |
+
def create_weights(
|
| 363 |
+
self,
|
| 364 |
+
layer: torch.nn.Module,
|
| 365 |
+
input_size_per_partition: int,
|
| 366 |
+
output_partition_sizes: List[int],
|
| 367 |
+
input_size: int,
|
| 368 |
+
output_size: int,
|
| 369 |
+
params_dtype: torch.dtype,
|
| 370 |
+
**extra_weight_attrs,
|
| 371 |
+
):
|
| 372 |
+
"""
|
| 373 |
+
Use the ModelSlimLinearScheme associated with the layer to create
|
| 374 |
+
the necessary parameters for the layer. See LinearMethodBase for param
|
| 375 |
+
details
|
| 376 |
+
"""
|
| 377 |
+
weight_loader = extra_weight_attrs.get("weight_loader")
|
| 378 |
+
layer.scheme.create_weights(
|
| 379 |
+
layer=layer,
|
| 380 |
+
input_size=input_size,
|
| 381 |
+
input_size_per_partition=input_size_per_partition,
|
| 382 |
+
output_partition_sizes=output_partition_sizes,
|
| 383 |
+
output_size=output_size,
|
| 384 |
+
params_dtype=params_dtype,
|
| 385 |
+
weight_loader=weight_loader,
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
def apply(
|
| 389 |
+
self,
|
| 390 |
+
layer: torch.nn.Module,
|
| 391 |
+
x: torch.Tensor,
|
| 392 |
+
bias: Optional[torch.Tensor] = None,
|
| 393 |
+
):
|
| 394 |
+
"""
|
| 395 |
+
Use the output of create_weights and the ModelSlimLinearScheme
|
| 396 |
+
associated with the layer to apply the forward pass with the
|
| 397 |
+
layer input. See LinearMethodBase for param details
|
| 398 |
+
|
| 399 |
+
"""
|
| 400 |
+
|
| 401 |
+
scheme = layer.scheme
|
| 402 |
+
if scheme is None:
|
| 403 |
+
raise ValueError("A scheme must be defined for each layer")
|
| 404 |
+
return scheme.apply_weights(layer, x, bias=bias)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
class ModelSlimFusedMoEMethod(FusedMoEMethodBase):
|
| 408 |
+
"""
|
| 409 |
+
Fused MoE method for ModelSlim quantization on Ascend NPU.
|
| 410 |
+
|
| 411 |
+
Delegates routing, activation, and finalization to the modular NPU MoE
|
| 412 |
+
components introduced in the hardware backend refactoring.
|
| 413 |
+
"""
|
| 414 |
+
|
| 415 |
+
def __init__(self, quantization_config: ModelSlimConfig):
|
| 416 |
+
self.quantization_config = quantization_config
|
| 417 |
+
|
| 418 |
+
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
| 419 |
+
layer.w13_scheme.process_weights_after_loading(layer)
|
| 420 |
+
layer.w2_scheme.process_weights_after_loading(layer)
|
| 421 |
+
|
| 422 |
+
def create_weights(
|
| 423 |
+
self,
|
| 424 |
+
layer: torch.nn.Module,
|
| 425 |
+
num_experts: int,
|
| 426 |
+
hidden_size: int,
|
| 427 |
+
intermediate_size_per_partition: int,
|
| 428 |
+
params_dtype: torch.dtype,
|
| 429 |
+
**extra_weight_attrs,
|
| 430 |
+
):
|
| 431 |
+
"""
|
| 432 |
+
Use the ModelSlimMoEScheme associated with the layer to create
|
| 433 |
+
the necessary parameters for the layer. See FusedMoEMethodBase for param
|
| 434 |
+
details
|
| 435 |
+
"""
|
| 436 |
+
layer.w13_scheme.create_weights(
|
| 437 |
+
layer=layer,
|
| 438 |
+
num_experts=num_experts,
|
| 439 |
+
hidden_size=hidden_size,
|
| 440 |
+
intermediate_size_per_partition=intermediate_size_per_partition,
|
| 441 |
+
weight_prefix="w13",
|
| 442 |
+
**extra_weight_attrs,
|
| 443 |
+
)
|
| 444 |
+
layer.w2_scheme.create_weights(
|
| 445 |
+
layer=layer,
|
| 446 |
+
num_experts=num_experts,
|
| 447 |
+
hidden_size=hidden_size,
|
| 448 |
+
intermediate_size_per_partition=intermediate_size_per_partition,
|
| 449 |
+
weight_prefix="w2",
|
| 450 |
+
**extra_weight_attrs,
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
def create_moe_runner(
|
| 454 |
+
self, layer: torch.nn.Module, moe_runner_config: MoeRunnerConfig
|
| 455 |
+
):
|
| 456 |
+
moe_runner_config.layer = layer
|
| 457 |
+
self.moe_runner_config = moe_runner_config
|
| 458 |
+
backend = get_moe_runner_backend()
|
| 459 |
+
if backend.is_auto():
|
| 460 |
+
backend = MoeRunnerBackend.ASCEND
|
| 461 |
+
self.runner = MoeRunner(backend, moe_runner_config)
|
| 462 |
+
|
| 463 |
+
# ------------------------------------------------------------------
|
| 464 |
+
# Main apply()
|
| 465 |
+
# ------------------------------------------------------------------
|
| 466 |
+
def apply(
|
| 467 |
+
self,
|
| 468 |
+
layer,
|
| 469 |
+
dispatch_output: StandardDispatchOutput,
|
| 470 |
+
) -> CombineInput:
|
| 471 |
+
from sglang.srt.layers.moe.moe_runner.ascend import AscendQuantInfo
|
| 472 |
+
|
| 473 |
+
quant_info = AscendQuantInfo(
|
| 474 |
+
w13_weight=layer.w13_weight,
|
| 475 |
+
w2_weight=layer.w2_weight,
|
| 476 |
+
w13_weight_scale=layer.w13_weight_scale,
|
| 477 |
+
w2_weight_scale=layer.w2_weight_scale,
|
| 478 |
+
w13_weight_offset=layer.w13_weight_offset,
|
| 479 |
+
w2_weight_offset=layer.w2_weight_offset,
|
| 480 |
+
w13_scale_bias=getattr(layer, "w13_scale_bias", None),
|
| 481 |
+
w2_scale_bias=getattr(layer, "w2_scale_bias", None),
|
| 482 |
+
w13_weight_bias=getattr(layer, "w13_weight_bias", None),
|
| 483 |
+
w2_weight_bias=getattr(layer, "w2_weight_bias", None),
|
| 484 |
+
)
|
| 485 |
+
return self.runner.run(dispatch_output, quant_info)
|
sglang_patch/srt/mem_cache/kv_cache_configurator.py
ADDED
|
@@ -0,0 +1,1867 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import TYPE_CHECKING, Any, Optional
|
| 7 |
+
|
| 8 |
+
import msgspec
|
| 9 |
+
import torch
|
| 10 |
+
|
| 11 |
+
from sglang.srt.configs.hybrid_arch import hybrid_gdn_config, mambaish_config
|
| 12 |
+
from sglang.srt.configs.model_config import (
|
| 13 |
+
ModelConfig,
|
| 14 |
+
get_dsa_index_head_dim,
|
| 15 |
+
get_minimax_sparse_attention_config,
|
| 16 |
+
get_minimax_sparse_disable_value_layer_ids,
|
| 17 |
+
get_minimax_sparse_layer_ids,
|
| 18 |
+
is_deepseek_dsa,
|
| 19 |
+
is_deepseek_v4,
|
| 20 |
+
is_minimax_sparse,
|
| 21 |
+
)
|
| 22 |
+
from sglang.srt.distributed.parallel_state import get_world_group
|
| 23 |
+
from sglang.srt.environ import envs
|
| 24 |
+
from sglang.srt.layers.quantization.fp4_kv_cache_quant_method import (
|
| 25 |
+
get_kv_cache_quant_method,
|
| 26 |
+
resolve_kv_cache_quant,
|
| 27 |
+
)
|
| 28 |
+
from sglang.srt.mem_cache.allocation_sizing import get_req_to_token_extra_context_len
|
| 29 |
+
from sglang.srt.mem_cache.allocator import (
|
| 30 |
+
BaseTokenToKVPoolAllocator,
|
| 31 |
+
PagedTokenToKVPoolAllocator,
|
| 32 |
+
TokenToKVPoolAllocator,
|
| 33 |
+
)
|
| 34 |
+
from sglang.srt.mem_cache.allocator.hisparse import (
|
| 35 |
+
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
| 36 |
+
HiSparseTokenToKVPoolAllocator,
|
| 37 |
+
)
|
| 38 |
+
from sglang.srt.mem_cache.allocator.swa import (
|
| 39 |
+
PureSWATokenToKVPoolAllocator,
|
| 40 |
+
SWATokenToKVPoolAllocator,
|
| 41 |
+
)
|
| 42 |
+
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
| 43 |
+
from sglang.srt.mem_cache.hisparse_memory_pool import HiSparseDSATokenToKVPool
|
| 44 |
+
from sglang.srt.mem_cache.memory_pool import (
|
| 45 |
+
DSATokenToKVPool,
|
| 46 |
+
HybridLinearKVPool,
|
| 47 |
+
HybridReqToTokenPool,
|
| 48 |
+
KVCache,
|
| 49 |
+
MHATokenToKVPool,
|
| 50 |
+
MHATokenToKVPoolFP4,
|
| 51 |
+
MHATokenToKVPoolMXFP8,
|
| 52 |
+
MiniMaxSparseKVPool,
|
| 53 |
+
MLATokenToKVPool,
|
| 54 |
+
MLATokenToKVPoolFP4,
|
| 55 |
+
NoOpMHATokenToKVPool,
|
| 56 |
+
PageMajorMHATokenToKVPool,
|
| 57 |
+
ReqToTokenPool,
|
| 58 |
+
)
|
| 59 |
+
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
| 60 |
+
from sglang.srt.platforms import current_platform
|
| 61 |
+
from sglang.srt.runtime_context import get_parallel
|
| 62 |
+
from sglang.srt.server_args import ServerArgs
|
| 63 |
+
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
|
| 64 |
+
from sglang.srt.utils.common import (
|
| 65 |
+
get_available_gpu_memory,
|
| 66 |
+
get_device_memory_capacity,
|
| 67 |
+
is_float4_e2m1fn_x2,
|
| 68 |
+
is_hip,
|
| 69 |
+
is_npu,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
logger = logging.getLogger(__name__)
|
| 73 |
+
|
| 74 |
+
_is_hip = is_hip()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _get_dsv4_compress_state_dtypes() -> tuple[torch.dtype, torch.dtype]:
|
| 78 |
+
dtype_name = envs.SGLANG_DSV4_COMPRESS_STATE_DTYPE.get().strip().lower()
|
| 79 |
+
if dtype_name in ("float32", "fp32"):
|
| 80 |
+
return torch.float32, torch.float32
|
| 81 |
+
if dtype_name in ("bfloat16", "bf16"):
|
| 82 |
+
return torch.bfloat16, torch.bfloat16
|
| 83 |
+
raise ValueError(
|
| 84 |
+
"Unsupported SGLANG_DSV4_COMPRESS_STATE_DTYPE="
|
| 85 |
+
f"{dtype_name!r}. Expected one of: float32, fp32, bfloat16, bf16."
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
_is_npu = is_npu()
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _should_enable_lazy_compaction() -> bool:
|
| 93 |
+
"""Lazy compaction default — ON unless
|
| 94 |
+
`SGLANG_DISABLE_LAZY_COMPACTION=1` (escape hatch for A/B / rollback).
|
| 95 |
+
Centralized here so both unified-memory-pool factory call sites stay in sync.
|
| 96 |
+
"""
|
| 97 |
+
return not envs.SGLANG_DISABLE_LAZY_COMPACTION.get()
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# the ratio of mamba cache pool size to max_running_requests
|
| 101 |
+
MAMBA_CACHE_SIZE_MAX_RUNNING_REQUESTS_RATIO = 3
|
| 102 |
+
MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP = 2
|
| 103 |
+
MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP_LAZY = 1
|
| 104 |
+
MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_OVERLAP = 1
|
| 105 |
+
|
| 106 |
+
if TYPE_CHECKING:
|
| 107 |
+
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
|
| 108 |
+
from sglang.srt.mem_cache.unified_memory_pool import (
|
| 109 |
+
UnifiedKVPool,
|
| 110 |
+
UnifiedPoolBundle,
|
| 111 |
+
)
|
| 112 |
+
from sglang.srt.model_executor.model_runner_components.layer_setup import (
|
| 113 |
+
ModelLayerInfo,
|
| 114 |
+
)
|
| 115 |
+
from sglang.srt.model_executor.model_runner_components.spec_aux_hidden_state import (
|
| 116 |
+
SpecAuxHiddenStateConfig,
|
| 117 |
+
)
|
| 118 |
+
from sglang.srt.model_executor.pool_configurator import (
|
| 119 |
+
MemoryPoolConfig,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class KVCacheConfigResult(msgspec.Struct, frozen=True, kw_only=True):
|
| 124 |
+
max_total_num_tokens: int
|
| 125 |
+
max_running_requests: int
|
| 126 |
+
full_max_total_num_tokens: Optional[int]
|
| 127 |
+
swa_max_total_num_tokens: Optional[int]
|
| 128 |
+
req_to_token_pool: ReqToTokenPool
|
| 129 |
+
token_to_kv_pool: KVCache
|
| 130 |
+
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator
|
| 131 |
+
memory_pool_config: MemoryPoolConfig
|
| 132 |
+
unified_memory_pool: Optional[UnifiedKVPool] = None
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class _InitializedPools(msgspec.Struct, frozen=True, kw_only=True):
|
| 136 |
+
req_to_token_pool: ReqToTokenPool
|
| 137 |
+
token_to_kv_pool: KVCache
|
| 138 |
+
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator
|
| 139 |
+
unified_memory_pool: Optional[UnifiedKVPool] = None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class _PoolSizes(msgspec.Struct, frozen=True, kw_only=True):
|
| 143 |
+
max_total_num_tokens: int
|
| 144 |
+
max_running_requests: int
|
| 145 |
+
full_max_total_num_tokens: Optional[int]
|
| 146 |
+
swa_max_total_num_tokens: Optional[int]
|
| 147 |
+
c4_max_total_num_tokens: int
|
| 148 |
+
c128_max_total_num_tokens: int
|
| 149 |
+
c4_state_pool_size: int
|
| 150 |
+
c128_state_pool_size: int
|
| 151 |
+
c4_state_dtype: Optional[torch.dtype]
|
| 152 |
+
c128_state_dtype: Optional[torch.dtype]
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
@dataclass(slots=True, kw_only=True)
|
| 156 |
+
class KVCacheConfigurator:
|
| 157 |
+
device: str
|
| 158 |
+
gpu_id: int
|
| 159 |
+
ps: ParallelState
|
| 160 |
+
pp_group: Any
|
| 161 |
+
model: Any
|
| 162 |
+
model_config: ModelConfig
|
| 163 |
+
server_args: ServerArgs
|
| 164 |
+
kv_cache_dtype: torch.dtype
|
| 165 |
+
model_dtype: torch.dtype
|
| 166 |
+
page_size: int
|
| 167 |
+
sliding_window_size: Optional[int]
|
| 168 |
+
spec_algorithm: SpeculativeAlgorithm
|
| 169 |
+
is_draft_worker: bool
|
| 170 |
+
post_capture_kv_active: bool
|
| 171 |
+
spec_aux_config: SpecAuxHiddenStateConfig
|
| 172 |
+
is_hybrid_swa: bool
|
| 173 |
+
is_hybrid_swa_compress: bool
|
| 174 |
+
use_mla_backend: bool
|
| 175 |
+
layer_info: ModelLayerInfo
|
| 176 |
+
forward_stream: Any
|
| 177 |
+
req_to_token_pool: Optional[ReqToTokenPool]
|
| 178 |
+
token_to_kv_pool_allocator: Optional[BaseTokenToKVPoolAllocator]
|
| 179 |
+
memory_pool_config: Optional[MemoryPoolConfig]
|
| 180 |
+
draft_model_idx: Optional[int] = None
|
| 181 |
+
mambaish_config: Optional[Any] = field(init=False)
|
| 182 |
+
hybrid_gdn_config: Optional[Any] = field(init=False)
|
| 183 |
+
is_inkling_mtp_draft: bool = field(init=False)
|
| 184 |
+
draft_swa_full_capacity: bool = field(init=False)
|
| 185 |
+
|
| 186 |
+
def __post_init__(self) -> None:
|
| 187 |
+
self.mambaish_config = mambaish_config(self.model_config)
|
| 188 |
+
self.hybrid_gdn_config = hybrid_gdn_config(self.model_config)
|
| 189 |
+
# Each multi-layer EAGLE MTP head owns one transformer block at
|
| 190 |
+
# layer_id=draft_model_idx; heads at a banded 's' depth route that layer
|
| 191 |
+
# into the SWA ring sub-pool (draft_swa_full_capacity) so the SWA
|
| 192 |
+
# store/read path activates for this depth, exactly like a trunk local
|
| 193 |
+
# layer.
|
| 194 |
+
self.is_inkling_mtp_draft = (
|
| 195 |
+
self.is_draft_worker
|
| 196 |
+
and self.draft_model_idx is not None
|
| 197 |
+
and self.model_config.hf_config.architectures[0]
|
| 198 |
+
== "InklingForConditionalGenerationMTP"
|
| 199 |
+
)
|
| 200 |
+
self.draft_swa_full_capacity = self.is_inkling_mtp_draft and (
|
| 201 |
+
self.draft_model_idx
|
| 202 |
+
in set(self.model_config.hf_text_config.mtp_local_layer_ids)
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
def _build_fp4_quant_method(self, *, num_layers: int):
|
| 206 |
+
if not is_float4_e2m1fn_x2(self.kv_cache_dtype):
|
| 207 |
+
return None
|
| 208 |
+
quant_name = resolve_kv_cache_quant(self.server_args.kv_cache_dtype)
|
| 209 |
+
if quant_name is None:
|
| 210 |
+
return None
|
| 211 |
+
quant_method = get_kv_cache_quant_method(
|
| 212 |
+
quant_name,
|
| 213 |
+
num_layers=num_layers,
|
| 214 |
+
device=self.device,
|
| 215 |
+
)
|
| 216 |
+
quant_method.load_scales_from_model(self.model)
|
| 217 |
+
return quant_method
|
| 218 |
+
|
| 219 |
+
def configure(self, *, pre_model_load_memory: int) -> KVCacheConfigResult:
|
| 220 |
+
"""Apply a resolved MemoryPoolConfig and initialize pools."""
|
| 221 |
+
if not self.spec_algorithm.is_none() and self.is_draft_worker:
|
| 222 |
+
assert (
|
| 223 |
+
self.memory_pool_config is not None
|
| 224 |
+
), "Draft worker requires memory_pool_config"
|
| 225 |
+
config = self.memory_pool_config
|
| 226 |
+
else:
|
| 227 |
+
config = self._resolve_memory_pool_config(pre_model_load_memory)
|
| 228 |
+
|
| 229 |
+
sizes = self._derive_pool_sizes(config=config)
|
| 230 |
+
|
| 231 |
+
pools = self._init_pools(
|
| 232 |
+
sizes=sizes,
|
| 233 |
+
req_to_token_pool=self.req_to_token_pool,
|
| 234 |
+
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
logger.info(
|
| 238 |
+
f"Memory pool end. "
|
| 239 |
+
f"avail mem={get_available_gpu_memory(self.device, self.gpu_id):.2f} GB"
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
return KVCacheConfigResult(
|
| 243 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 244 |
+
max_running_requests=sizes.max_running_requests,
|
| 245 |
+
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
|
| 246 |
+
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
|
| 247 |
+
req_to_token_pool=pools.req_to_token_pool,
|
| 248 |
+
token_to_kv_pool=pools.token_to_kv_pool,
|
| 249 |
+
token_to_kv_pool_allocator=pools.token_to_kv_pool_allocator,
|
| 250 |
+
memory_pool_config=config,
|
| 251 |
+
unified_memory_pool=pools.unified_memory_pool,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
def _derive_pool_sizes(self, *, config: MemoryPoolConfig) -> _PoolSizes:
|
| 255 |
+
max_total_num_tokens = config.max_total_num_tokens
|
| 256 |
+
max_running_requests = config.max_running_requests
|
| 257 |
+
full_max_total_num_tokens = None
|
| 258 |
+
swa_max_total_num_tokens = None
|
| 259 |
+
if self.is_hybrid_swa:
|
| 260 |
+
full_max_total_num_tokens = config.full_max_total_num_tokens
|
| 261 |
+
swa_max_total_num_tokens = config.swa_max_total_num_tokens
|
| 262 |
+
|
| 263 |
+
# DSV4 compressed-attention pool sizes. Draft worker reuses target's
|
| 264 |
+
# full/swa sizes but does NOT own c4/c128/state pools (those live on
|
| 265 |
+
# the target rank only); zero them out regardless of what config holds.
|
| 266 |
+
if self.is_draft_worker:
|
| 267 |
+
c4_max_total_num_tokens = 0
|
| 268 |
+
c128_max_total_num_tokens = 0
|
| 269 |
+
c4_state_pool_size = 0
|
| 270 |
+
c128_state_pool_size = 0
|
| 271 |
+
else:
|
| 272 |
+
c4_max_total_num_tokens = config.c4_max_total_num_tokens
|
| 273 |
+
c128_max_total_num_tokens = config.c128_max_total_num_tokens
|
| 274 |
+
c4_state_pool_size = config.c4_state_pool_size
|
| 275 |
+
c128_state_pool_size = config.c128_state_pool_size
|
| 276 |
+
|
| 277 |
+
# Draft worker does not own the compression-state pools, but keep the
|
| 278 |
+
# dtype attributes initialized so _init_pools can share one code path.
|
| 279 |
+
c4_state_dtype: Optional[torch.dtype] = None
|
| 280 |
+
c128_state_dtype: Optional[torch.dtype] = None
|
| 281 |
+
if is_deepseek_v4(self.model_config.hf_config):
|
| 282 |
+
c4_state_dtype, c128_state_dtype = _get_dsv4_compress_state_dtypes()
|
| 283 |
+
|
| 284 |
+
return _PoolSizes(
|
| 285 |
+
max_total_num_tokens=max_total_num_tokens,
|
| 286 |
+
max_running_requests=max_running_requests,
|
| 287 |
+
full_max_total_num_tokens=full_max_total_num_tokens,
|
| 288 |
+
swa_max_total_num_tokens=swa_max_total_num_tokens,
|
| 289 |
+
c4_max_total_num_tokens=c4_max_total_num_tokens,
|
| 290 |
+
c128_max_total_num_tokens=c128_max_total_num_tokens,
|
| 291 |
+
c4_state_pool_size=c4_state_pool_size,
|
| 292 |
+
c128_state_pool_size=c128_state_pool_size,
|
| 293 |
+
c4_state_dtype=c4_state_dtype,
|
| 294 |
+
c128_state_dtype=c128_state_dtype,
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
def _init_pools(
|
| 298 |
+
self,
|
| 299 |
+
*,
|
| 300 |
+
sizes: _PoolSizes,
|
| 301 |
+
req_to_token_pool: Optional[ReqToTokenPool],
|
| 302 |
+
token_to_kv_pool_allocator: Optional[BaseTokenToKVPoolAllocator],
|
| 303 |
+
) -> _InitializedPools:
|
| 304 |
+
"""Initialize the memory pools."""
|
| 305 |
+
token_to_kv_pool = None
|
| 306 |
+
|
| 307 |
+
# Unified-pool fast path: build req_to_token + token_to_kv pool + allocator
|
| 308 |
+
# from one byte buffer, then return. Gated to the target worker
|
| 309 |
+
# (req_to_token_pool is None); supports hybrid Mamba and hybrid SWA (not DSV4).
|
| 310 |
+
if (
|
| 311 |
+
self.server_args.enable_unified_memory
|
| 312 |
+
and self.server_args.disaggregation_mode == "null"
|
| 313 |
+
and req_to_token_pool is None
|
| 314 |
+
):
|
| 315 |
+
if self.mambaish_config is not None:
|
| 316 |
+
bundle = self._init_unified_mamba_pools(
|
| 317 |
+
max_num_reqs=sizes.max_running_requests,
|
| 318 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 319 |
+
)
|
| 320 |
+
elif self.is_hybrid_swa and not is_deepseek_v4(self.model_config.hf_config):
|
| 321 |
+
bundle = self._init_unified_swa_pools(
|
| 322 |
+
max_num_reqs=sizes.max_running_requests,
|
| 323 |
+
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
|
| 324 |
+
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
|
| 325 |
+
)
|
| 326 |
+
else:
|
| 327 |
+
# Fail loud, not silently fall through to the normal pools (which would
|
| 328 |
+
# leave the flag a no-op). The feature replaces the HYBRID pools only.
|
| 329 |
+
raise ValueError(
|
| 330 |
+
"--enable-unified-memory only supports hybrid Mamba and "
|
| 331 |
+
"hybrid sliding-window-attention models (DeepSeek-V4 excluded); "
|
| 332 |
+
f"the current model ({self.model_config.hf_config.architectures}) "
|
| 333 |
+
"is neither, so the unified memory pool cannot be built. Drop "
|
| 334 |
+
"--enable-unified-memory for this model."
|
| 335 |
+
)
|
| 336 |
+
return _InitializedPools(
|
| 337 |
+
req_to_token_pool=bundle.req_to_token_pool,
|
| 338 |
+
token_to_kv_pool=bundle.token_to_kv_pool,
|
| 339 |
+
token_to_kv_pool_allocator=bundle.token_to_kv_pool_allocator,
|
| 340 |
+
unified_memory_pool=bundle.unified_memory_pool,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
# Initialize req_to_token_pool
|
| 344 |
+
if req_to_token_pool is None:
|
| 345 |
+
req_to_token_pool = self._build_req_to_token_pool(
|
| 346 |
+
max_num_reqs=sizes.max_running_requests
|
| 347 |
+
)
|
| 348 |
+
else:
|
| 349 |
+
# Draft worker shares req_to_token_pool with the target worker.
|
| 350 |
+
assert self.is_draft_worker
|
| 351 |
+
# Each multi-layer EAGLE MTP head owns one transformer block at
|
| 352 |
+
# layer_id=draft_model_idx and needs its own sconv/mamba cache while
|
| 353 |
+
# sharing the target's request-to-token mapping.
|
| 354 |
+
if self.is_inkling_mtp_draft and isinstance(
|
| 355 |
+
req_to_token_pool, HybridReqToTokenPool
|
| 356 |
+
):
|
| 357 |
+
# speculative_num_draft_tokens=None: draft heads never run
|
| 358 |
+
# TARGET_VERIFY, so their pools skip the per-step intermediate
|
| 359 |
+
# (SpeculativeState) buffers only the target pool consumes.
|
| 360 |
+
req_to_token_pool = req_to_token_pool.clone_with_new_mamba(
|
| 361 |
+
mamba_size=self.server_args.max_mamba_cache_size,
|
| 362 |
+
mamba_spec_state_size=sizes.max_running_requests,
|
| 363 |
+
cache_params=self.mambaish_config.mamba2_cache_params,
|
| 364 |
+
device=self.device,
|
| 365 |
+
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(),
|
| 366 |
+
draft_model_idx=self.draft_model_idx,
|
| 367 |
+
speculative_eagle_topk=self.server_args.speculative_eagle_topk,
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
# Initialize token_to_kv_pool
|
| 371 |
+
is_dsa_model = is_deepseek_dsa(self.model_config.hf_config)
|
| 372 |
+
is_dsv4_model = is_deepseek_v4(self.model_config.hf_config)
|
| 373 |
+
|
| 374 |
+
self._validate_prefill_only_disable_kv_cache_pool_family(
|
| 375 |
+
is_dsa_model, is_dsv4_model, current_platform
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
token_to_kv_pool = self._build_token_to_kv_pool(
|
| 379 |
+
sizes=sizes,
|
| 380 |
+
is_dsa_model=is_dsa_model,
|
| 381 |
+
is_dsv4_model=is_dsv4_model,
|
| 382 |
+
req_to_token_pool=req_to_token_pool,
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
token_to_kv_pool_allocator = self._build_token_to_kv_pool_allocator(
|
| 386 |
+
sizes=sizes,
|
| 387 |
+
token_to_kv_pool=token_to_kv_pool,
|
| 388 |
+
is_dsv4_model=is_dsv4_model,
|
| 389 |
+
req_to_token_pool=req_to_token_pool,
|
| 390 |
+
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Defensive check: the explicit validation above should reject known
|
| 394 |
+
# unsupported pool families before allocation. Keep this guard here so
|
| 395 |
+
# future pool-selection refactors fail at boot instead of on first use.
|
| 396 |
+
if (
|
| 397 |
+
self.server_args.prefill_only_disable_kv_cache
|
| 398 |
+
and not self.is_draft_worker
|
| 399 |
+
and not isinstance(token_to_kv_pool, NoOpMHATokenToKVPool)
|
| 400 |
+
):
|
| 401 |
+
raise RuntimeError(
|
| 402 |
+
"--prefill-only-disable-kv-cache expected NoOpMHATokenToKVPool but the "
|
| 403 |
+
f"runtime pool is {type(token_to_kv_pool).__name__}. This pool "
|
| 404 |
+
"family is not yet supported by --prefill-only-disable-kv-cache. "
|
| 405 |
+
"Supported configurations today: plain MHA models on CUDA with the FA "
|
| 406 |
+
"(fa3/fa4) prefill backend, --is-embedding, --chunked-prefill-size=-1, "
|
| 407 |
+
"--disable-radix-cache, no context-parallel attention, no HiSparse, "
|
| 408 |
+
"and --kv-cache-dtype not in {nvfp4, fp4_mx_block16}."
|
| 409 |
+
)
|
| 410 |
+
return _InitializedPools(
|
| 411 |
+
req_to_token_pool=req_to_token_pool,
|
| 412 |
+
token_to_kv_pool=token_to_kv_pool,
|
| 413 |
+
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
def _init_unified_mamba_pools(
|
| 417 |
+
self, *, max_num_reqs: int, max_total_num_tokens: int
|
| 418 |
+
) -> UnifiedPoolBundle:
|
| 419 |
+
"""Build the shared-KV-pool stack for a hybrid-Mamba model:
|
| 420 |
+
one byte buffer split between the full-attn MHA KV pool and the
|
| 421 |
+
per-request Mamba state pool, with virtual slot ids above the
|
| 422 |
+
allocator."""
|
| 423 |
+
from sglang.srt.mem_cache.unified_memory_pool import init_unified_mamba_pools
|
| 424 |
+
|
| 425 |
+
config = self.mambaish_config
|
| 426 |
+
assert config is not None
|
| 427 |
+
assert (
|
| 428 |
+
not self.use_mla_backend
|
| 429 |
+
), "unified memory pool does not support MLA-hybrid-Mamba yet"
|
| 430 |
+
# The full sub-pool is page-aware (via `MultiEndedAllocator(page_size=...)`);
|
| 431 |
+
# the mamba sub-pool stays page=1.
|
| 432 |
+
assert self.page_size >= 1, f"page_size must be >= 1, got {self.page_size}"
|
| 433 |
+
# Mirror the non-shared path's extra_max_context_len computation.
|
| 434 |
+
extra_max_context_len = 4
|
| 435 |
+
if self.server_args.speculative_num_draft_tokens is not None:
|
| 436 |
+
extra_max_context_len += self.server_args.speculative_num_draft_tokens
|
| 437 |
+
|
| 438 |
+
mamba_layer_ids = [
|
| 439 |
+
i
|
| 440 |
+
for i in config.mamba2_cache_params.layers
|
| 441 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 442 |
+
]
|
| 443 |
+
full_attention_layer_ids = [
|
| 444 |
+
i
|
| 445 |
+
for i in config.full_attention_layer_ids
|
| 446 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 447 |
+
]
|
| 448 |
+
|
| 449 |
+
bundle = init_unified_mamba_pools(
|
| 450 |
+
device=self.device,
|
| 451 |
+
kv_cache_dtype=self.kv_cache_dtype,
|
| 452 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 453 |
+
head_dim=self.model_config.head_dim,
|
| 454 |
+
page_size=self.page_size,
|
| 455 |
+
start_layer=self.layer_info.start_layer,
|
| 456 |
+
end_layer=self.layer_info.end_layer,
|
| 457 |
+
is_draft_worker=self.is_draft_worker,
|
| 458 |
+
use_mla_backend=self.use_mla_backend,
|
| 459 |
+
mamba_layer_ids=mamba_layer_ids,
|
| 460 |
+
full_attention_layer_ids=full_attention_layer_ids,
|
| 461 |
+
mamba2_cache_params=config.mamba2_cache_params,
|
| 462 |
+
model_context_len=self.model_config.context_len,
|
| 463 |
+
extra_max_context_len=extra_max_context_len,
|
| 464 |
+
max_total_num_tokens=max_total_num_tokens,
|
| 465 |
+
max_mamba_cache_size=self.server_args.max_mamba_cache_size,
|
| 466 |
+
max_num_reqs=max_num_reqs,
|
| 467 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 468 |
+
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(),
|
| 469 |
+
speculative_num_draft_tokens=self.server_args.speculative_num_draft_tokens,
|
| 470 |
+
disable_overlap_schedule=self.server_args.disable_overlap_schedule,
|
| 471 |
+
need_sort=self.server_args.disaggregation_mode in ("decode", "prefill"),
|
| 472 |
+
mamba_full_memory_ratio=self.server_args.mamba_full_memory_ratio,
|
| 473 |
+
# Overlap mode: the allocator's `free` drops a wait_stream(forward_stream)
|
| 474 |
+
# barrier so eager compaction serializes after the in-flight forward's
|
| 475 |
+
# v2p/KV reads. Near-no-op in normal mode.
|
| 476 |
+
forward_stream=self.forward_stream,
|
| 477 |
+
# Lazy compaction: default ON, env-var escape hatch for rollback / A/B.
|
| 478 |
+
lazy_compaction=_should_enable_lazy_compaction(),
|
| 479 |
+
)
|
| 480 |
+
return bundle
|
| 481 |
+
|
| 482 |
+
def _init_unified_swa_pools(
|
| 483 |
+
self,
|
| 484 |
+
*,
|
| 485 |
+
max_num_reqs: int,
|
| 486 |
+
full_max_total_num_tokens: Optional[int],
|
| 487 |
+
swa_max_total_num_tokens: Optional[int],
|
| 488 |
+
) -> UnifiedPoolBundle:
|
| 489 |
+
"""Build the unified-pool stack for a hybrid-SWA model (Triton): one byte
|
| 490 |
+
buffer split between the full-attention and SWA KV pools."""
|
| 491 |
+
from sglang.srt.mem_cache.unified_memory_pool import (
|
| 492 |
+
UnifiedPoolBundle,
|
| 493 |
+
init_unified_swa_pools,
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
assert self.is_hybrid_swa, "_init_unified_swa_pools called on a non-SWA model"
|
| 497 |
+
# Both sub-pools are page-aware; the SWA composite runs alloc_extend_kernel
|
| 498 |
+
# once in virtual space and binds the new pages on both sub-allocators.
|
| 499 |
+
assert self.page_size >= 1, f"page_size must be >= 1, got {self.page_size}"
|
| 500 |
+
assert (
|
| 501 |
+
not self.use_mla_backend
|
| 502 |
+
), "unified memory pool does not support MLA-SWA hybrid yet"
|
| 503 |
+
# Mirror the non-shared path's extra_max_context_len computation.
|
| 504 |
+
extra_max_context_len = 4
|
| 505 |
+
if self.server_args.speculative_num_draft_tokens is not None:
|
| 506 |
+
extra_max_context_len += self.server_args.speculative_num_draft_tokens
|
| 507 |
+
req_to_token_pool = ReqToTokenPool(
|
| 508 |
+
size=max_num_reqs,
|
| 509 |
+
max_context_len=self.model_config.context_len + extra_max_context_len,
|
| 510 |
+
device=self.device,
|
| 511 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
head_num = self.model_config.get_num_kv_heads(get_parallel().attn_tp_size)
|
| 515 |
+
head_dim = self.model_config.head_dim
|
| 516 |
+
if self.is_hybrid_swa_compress:
|
| 517 |
+
# Asymmetric head dims between full and SWA (NPU compress path):
|
| 518 |
+
# pull SWA-specific dims from the hf text config.
|
| 519 |
+
v_head_dim = self.model_config.hf_text_config.v_head_dim
|
| 520 |
+
swa_head_num = max(
|
| 521 |
+
1,
|
| 522 |
+
self.model_config.hf_text_config.swa_num_key_value_heads
|
| 523 |
+
// get_parallel().attn_tp_size,
|
| 524 |
+
)
|
| 525 |
+
swa_head_dim = self.model_config.hf_text_config.swa_head_dim
|
| 526 |
+
swa_v_head_dim = self.model_config.hf_text_config.swa_v_head_dim
|
| 527 |
+
else:
|
| 528 |
+
v_head_dim = head_dim
|
| 529 |
+
swa_head_num = head_num
|
| 530 |
+
swa_head_dim = head_dim
|
| 531 |
+
swa_v_head_dim = head_dim
|
| 532 |
+
|
| 533 |
+
# Filter layer ids to this worker's [start_layer, end_layer) range.
|
| 534 |
+
swa_attention_layer_ids = [
|
| 535 |
+
i
|
| 536 |
+
for i in self.model_config.swa_attention_layer_ids
|
| 537 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 538 |
+
]
|
| 539 |
+
full_attention_layer_ids = [
|
| 540 |
+
i
|
| 541 |
+
for i in self.model_config.full_attention_layer_ids
|
| 542 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 543 |
+
]
|
| 544 |
+
|
| 545 |
+
bundle = init_unified_swa_pools(
|
| 546 |
+
device=self.device,
|
| 547 |
+
kv_cache_dtype=self.kv_cache_dtype,
|
| 548 |
+
head_num=head_num,
|
| 549 |
+
head_dim=head_dim,
|
| 550 |
+
v_head_dim=v_head_dim,
|
| 551 |
+
swa_head_num=swa_head_num,
|
| 552 |
+
swa_head_dim=swa_head_dim,
|
| 553 |
+
swa_v_head_dim=swa_v_head_dim,
|
| 554 |
+
page_size=self.page_size,
|
| 555 |
+
start_layer=self.layer_info.start_layer,
|
| 556 |
+
end_layer=self.layer_info.end_layer,
|
| 557 |
+
swa_attention_layer_ids=swa_attention_layer_ids,
|
| 558 |
+
full_attention_layer_ids=full_attention_layer_ids,
|
| 559 |
+
full_max_total_num_tokens=full_max_total_num_tokens,
|
| 560 |
+
swa_max_total_num_tokens=swa_max_total_num_tokens,
|
| 561 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 562 |
+
need_sort=self.server_args.disaggregation_mode in ("decode", "prefill"),
|
| 563 |
+
# Overlap mode: same wait_stream(forward_stream) rationale as
|
| 564 |
+
# `_init_unified_mamba_pools`.
|
| 565 |
+
forward_stream=self.forward_stream,
|
| 566 |
+
# Lazy compaction: default ON, with env var escape hatch for rollback / A/B.
|
| 567 |
+
lazy_compaction=_should_enable_lazy_compaction(),
|
| 568 |
+
)
|
| 569 |
+
return UnifiedPoolBundle(
|
| 570 |
+
unified_memory_pool=bundle.unified_memory_pool,
|
| 571 |
+
token_to_kv_pool=bundle.token_to_kv_pool,
|
| 572 |
+
token_to_kv_pool_allocator=bundle.token_to_kv_pool_allocator,
|
| 573 |
+
req_to_token_pool=req_to_token_pool,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
def _validate_prefill_only_disable_kv_cache_pool_family(
|
| 577 |
+
self,
|
| 578 |
+
is_dsa_model: bool,
|
| 579 |
+
is_dsv4_model: bool,
|
| 580 |
+
current_platform,
|
| 581 |
+
):
|
| 582 |
+
if not self.server_args.prefill_only_disable_kv_cache or self.is_draft_worker:
|
| 583 |
+
return
|
| 584 |
+
|
| 585 |
+
unsupported_pool_family = None
|
| 586 |
+
if is_dsv4_model:
|
| 587 |
+
unsupported_pool_family = "DeepSeekV4TokenToKVPool"
|
| 588 |
+
elif current_platform.is_out_of_tree() and not self.mambaish_config:
|
| 589 |
+
unsupported_pool_family = "out-of-tree platform KV pool"
|
| 590 |
+
elif (
|
| 591 |
+
self.server_args.attention_backend == "ascend" and not self.mambaish_config
|
| 592 |
+
):
|
| 593 |
+
unsupported_pool_family = "NPU/Ascend KV pool"
|
| 594 |
+
elif self.use_mla_backend and is_dsa_model:
|
| 595 |
+
unsupported_pool_family = "DSA/MLA KV pool"
|
| 596 |
+
elif self.use_mla_backend and not self.mambaish_config:
|
| 597 |
+
unsupported_pool_family = "MLA KV pool"
|
| 598 |
+
elif self.is_hybrid_swa:
|
| 599 |
+
unsupported_pool_family = "SWA KV pool"
|
| 600 |
+
elif self.mambaish_config:
|
| 601 |
+
unsupported_pool_family = "hybrid linear/Mamba KV pool"
|
| 602 |
+
elif is_float4_e2m1fn_x2(self.kv_cache_dtype):
|
| 603 |
+
unsupported_pool_family = "FP4 MHA KV pool"
|
| 604 |
+
|
| 605 |
+
if unsupported_pool_family is not None:
|
| 606 |
+
raise RuntimeError(
|
| 607 |
+
"--prefill-only-disable-kv-cache is not supported for "
|
| 608 |
+
f"{unsupported_pool_family}. Supported configurations today: plain MHA "
|
| 609 |
+
"models on CUDA with the FA (fa3/fa4) prefill backend, --is-embedding, "
|
| 610 |
+
"--chunked-prefill-size=-1, --disable-radix-cache, no context-parallel "
|
| 611 |
+
"attention, no HiSparse, and --kv-cache-dtype not in {nvfp4, fp4_mx_block16}."
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
def _build_req_to_token_pool(self, *, max_num_reqs: int) -> ReqToTokenPool:
|
| 615 |
+
extra_max_context_len = get_req_to_token_extra_context_len(self.server_args)
|
| 616 |
+
|
| 617 |
+
if self.server_args.disaggregation_mode == "decode":
|
| 618 |
+
# Extra slots for pre-allocated requests
|
| 619 |
+
pre_alloc_size = self.server_args.disaggregation_decode_extra_slots
|
| 620 |
+
if self.mambaish_config:
|
| 621 |
+
req_to_token_pool = self._build_hybrid_mamba_decode_req_pool(
|
| 622 |
+
max_num_reqs=max_num_reqs,
|
| 623 |
+
extra_max_context_len=extra_max_context_len,
|
| 624 |
+
pre_alloc_size=pre_alloc_size,
|
| 625 |
+
)
|
| 626 |
+
else:
|
| 627 |
+
req_to_token_pool = self._build_decode_req_pool(
|
| 628 |
+
max_num_reqs=max_num_reqs,
|
| 629 |
+
extra_max_context_len=extra_max_context_len,
|
| 630 |
+
pre_alloc_size=pre_alloc_size,
|
| 631 |
+
)
|
| 632 |
+
elif self.mambaish_config:
|
| 633 |
+
req_to_token_pool = self._build_hybrid_req_pool(
|
| 634 |
+
max_num_reqs=max_num_reqs,
|
| 635 |
+
extra_max_context_len=extra_max_context_len,
|
| 636 |
+
)
|
| 637 |
+
else:
|
| 638 |
+
req_to_token_pool = self._build_default_req_pool(
|
| 639 |
+
max_num_reqs=max_num_reqs,
|
| 640 |
+
extra_max_context_len=extra_max_context_len,
|
| 641 |
+
)
|
| 642 |
+
return req_to_token_pool
|
| 643 |
+
|
| 644 |
+
def _build_hybrid_mamba_decode_req_pool(
|
| 645 |
+
self,
|
| 646 |
+
*,
|
| 647 |
+
max_num_reqs: int,
|
| 648 |
+
extra_max_context_len: int,
|
| 649 |
+
pre_alloc_size: int,
|
| 650 |
+
) -> ReqToTokenPool:
|
| 651 |
+
from sglang.srt.disaggregation.decode import (
|
| 652 |
+
HybridMambaDecodeReqToTokenPool,
|
| 653 |
+
)
|
| 654 |
+
|
| 655 |
+
req_to_token_pool = HybridMambaDecodeReqToTokenPool(
|
| 656 |
+
size=max_num_reqs,
|
| 657 |
+
max_context_len=self.model_config.context_len + extra_max_context_len,
|
| 658 |
+
device=self.device,
|
| 659 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 660 |
+
cache_params=self.mambaish_config.mamba2_cache_params,
|
| 661 |
+
mamba_layer_ids=(
|
| 662 |
+
[
|
| 663 |
+
i
|
| 664 |
+
for i in self.mambaish_config.mamba2_cache_params.layers
|
| 665 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 666 |
+
]
|
| 667 |
+
),
|
| 668 |
+
speculative_num_draft_tokens=self.server_args.max_speculative_num_draft_tokens,
|
| 669 |
+
speculative_eagle_topk=self.server_args.speculative_eagle_topk,
|
| 670 |
+
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(),
|
| 671 |
+
pre_alloc_size=pre_alloc_size,
|
| 672 |
+
enable_overlap_schedule=not self.server_args.disable_overlap_schedule,
|
| 673 |
+
mamba_size=self.server_args.max_mamba_cache_size,
|
| 674 |
+
start_layer=self.layer_info.start_layer,
|
| 675 |
+
)
|
| 676 |
+
return req_to_token_pool
|
| 677 |
+
|
| 678 |
+
def _build_decode_req_pool(
|
| 679 |
+
self,
|
| 680 |
+
*,
|
| 681 |
+
max_num_reqs: int,
|
| 682 |
+
extra_max_context_len: int,
|
| 683 |
+
pre_alloc_size: int,
|
| 684 |
+
) -> ReqToTokenPool:
|
| 685 |
+
from sglang.srt.disaggregation.decode import DecodeReqToTokenPool
|
| 686 |
+
|
| 687 |
+
req_to_token_pool = DecodeReqToTokenPool(
|
| 688 |
+
size=max_num_reqs,
|
| 689 |
+
max_context_len=self.model_config.context_len + extra_max_context_len,
|
| 690 |
+
device=self.device,
|
| 691 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 692 |
+
pre_alloc_size=pre_alloc_size,
|
| 693 |
+
)
|
| 694 |
+
return req_to_token_pool
|
| 695 |
+
|
| 696 |
+
def _build_hybrid_req_pool(
|
| 697 |
+
self,
|
| 698 |
+
*,
|
| 699 |
+
max_num_reqs: int,
|
| 700 |
+
extra_max_context_len: int,
|
| 701 |
+
) -> ReqToTokenPool:
|
| 702 |
+
req_to_token_pool = HybridReqToTokenPool(
|
| 703 |
+
size=max_num_reqs,
|
| 704 |
+
mamba_size=self.server_args.max_mamba_cache_size,
|
| 705 |
+
mamba_spec_state_size=max_num_reqs,
|
| 706 |
+
max_context_len=self.model_config.context_len + extra_max_context_len,
|
| 707 |
+
device=self.device,
|
| 708 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 709 |
+
cache_params=self.mambaish_config.mamba2_cache_params,
|
| 710 |
+
mamba_layer_ids=(
|
| 711 |
+
[
|
| 712 |
+
i
|
| 713 |
+
for i in self.mambaish_config.mamba2_cache_params.layers
|
| 714 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 715 |
+
]
|
| 716 |
+
),
|
| 717 |
+
enable_mamba_extra_buffer=self.server_args.enable_mamba_extra_buffer(),
|
| 718 |
+
enable_mamba_extra_buffer_lazy=self.server_args.enable_mamba_extra_buffer_lazy(),
|
| 719 |
+
speculative_num_draft_tokens=self.server_args.max_speculative_num_draft_tokens,
|
| 720 |
+
speculative_eagle_topk=self.server_args.speculative_eagle_topk,
|
| 721 |
+
enable_overlap_schedule=not self.server_args.disable_overlap_schedule,
|
| 722 |
+
start_layer=self.layer_info.start_layer,
|
| 723 |
+
enable_linear_replayssm=self.server_args.enable_linear_replayssm,
|
| 724 |
+
linear_replayssm_cache_len=self.server_args.linear_replayssm_cache_len,
|
| 725 |
+
mamba_envelope_layout=self.server_args.enable_page_major_kv_layout,
|
| 726 |
+
# ReplaySSM spec-verify is GDN-only: activate the pool machinery
|
| 727 |
+
# (rings + cursors + the intermediate_ssm gate) only for GDN-hybrid
|
| 728 |
+
# models, so any other mamba-ish model (Mamba2/Nemotron, lightning,
|
| 729 |
+
# ...) run with the flag set stays byte-identical to flag-off.
|
| 730 |
+
enable_gdn_replayssm_spec=(
|
| 731 |
+
self.server_args.enable_gdn_replayssm_spec
|
| 732 |
+
and self.hybrid_gdn_config is not None
|
| 733 |
+
),
|
| 734 |
+
)
|
| 735 |
+
return req_to_token_pool
|
| 736 |
+
|
| 737 |
+
def _build_default_req_pool(
|
| 738 |
+
self,
|
| 739 |
+
*,
|
| 740 |
+
max_num_reqs: int,
|
| 741 |
+
extra_max_context_len: int,
|
| 742 |
+
) -> ReqToTokenPool:
|
| 743 |
+
# DSV4 on NPU needs an extended ReqToTokenPool holding per-req
|
| 744 |
+
# swa/c4/c128/c{4,128}_state tables; others stay on the stock one.
|
| 745 |
+
req_to_token_pool_cls = ReqToTokenPool
|
| 746 |
+
if _is_npu and is_deepseek_v4(self.model_config.hf_config):
|
| 747 |
+
from sglang.srt.hardware_backend.npu.dsv4.dsv4_req_to_token_pool import (
|
| 748 |
+
DSV4NPUReqToTokenPool,
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
req_to_token_pool_cls = DSV4NPUReqToTokenPool
|
| 752 |
+
|
| 753 |
+
req_to_token_pool = req_to_token_pool_cls(
|
| 754 |
+
size=max_num_reqs,
|
| 755 |
+
max_context_len=self.model_config.context_len + extra_max_context_len,
|
| 756 |
+
device=self.device,
|
| 757 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 758 |
+
)
|
| 759 |
+
return req_to_token_pool
|
| 760 |
+
|
| 761 |
+
def _build_token_to_kv_pool(
|
| 762 |
+
self,
|
| 763 |
+
*,
|
| 764 |
+
sizes: _PoolSizes,
|
| 765 |
+
is_dsa_model: bool,
|
| 766 |
+
is_dsv4_model: bool,
|
| 767 |
+
req_to_token_pool: ReqToTokenPool,
|
| 768 |
+
) -> KVCache:
|
| 769 |
+
# Page-granularity envelope layout for the MHA-shaped (full / SWA) pools,
|
| 770 |
+
# selected by swapping in the PageMajorMHATokenToKVPool subclass. The
|
| 771 |
+
# default keeps upstream's per-layer layout. The Mamba state pool is routed
|
| 772 |
+
# separately via `mamba_envelope_layout` on the req-to-token pool above.
|
| 773 |
+
enable_page_major = self.server_args.enable_page_major_kv_layout
|
| 774 |
+
mha_pool_class = (
|
| 775 |
+
PageMajorMHATokenToKVPool if enable_page_major else MHATokenToKVPool
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
if is_dsv4_model:
|
| 779 |
+
token_to_kv_pool = self._build_dsv4_kv_pool(
|
| 780 |
+
max_running_requests=sizes.max_running_requests,
|
| 781 |
+
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
|
| 782 |
+
c4_max_total_num_tokens=sizes.c4_max_total_num_tokens,
|
| 783 |
+
c128_max_total_num_tokens=sizes.c128_max_total_num_tokens,
|
| 784 |
+
c4_state_pool_size=sizes.c4_state_pool_size,
|
| 785 |
+
c128_state_pool_size=sizes.c128_state_pool_size,
|
| 786 |
+
c4_state_dtype=sizes.c4_state_dtype,
|
| 787 |
+
c128_state_dtype=sizes.c128_state_dtype,
|
| 788 |
+
req_to_token_pool=req_to_token_pool,
|
| 789 |
+
)
|
| 790 |
+
elif current_platform.is_out_of_tree() and not self.mambaish_config:
|
| 791 |
+
if self.use_mla_backend and is_dsa_model:
|
| 792 |
+
token_to_kv_pool = self._build_oot_dsa_kv_pool(
|
| 793 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 794 |
+
)
|
| 795 |
+
elif self.use_mla_backend:
|
| 796 |
+
token_to_kv_pool = self._build_oot_mla_kv_pool(
|
| 797 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 798 |
+
is_dsa_model=is_dsa_model,
|
| 799 |
+
)
|
| 800 |
+
else:
|
| 801 |
+
token_to_kv_pool = self._build_oot_mha_kv_pool(
|
| 802 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 803 |
+
)
|
| 804 |
+
elif (
|
| 805 |
+
self.server_args.attention_backend == "ascend" and not self.mambaish_config
|
| 806 |
+
):
|
| 807 |
+
if self.is_hybrid_swa:
|
| 808 |
+
token_to_kv_pool = self._build_ascend_swa_kv_pool(
|
| 809 |
+
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
|
| 810 |
+
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
|
| 811 |
+
)
|
| 812 |
+
elif self.use_mla_backend:
|
| 813 |
+
token_to_kv_pool = self._build_ascend_mla_kv_pool(
|
| 814 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 815 |
+
is_dsa_model=is_dsa_model,
|
| 816 |
+
)
|
| 817 |
+
else:
|
| 818 |
+
token_to_kv_pool = self._build_ascend_mha_kv_pool(
|
| 819 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 820 |
+
)
|
| 821 |
+
elif self.use_mla_backend and is_dsa_model:
|
| 822 |
+
token_to_kv_pool = self._build_dsa_kv_pool(
|
| 823 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 824 |
+
)
|
| 825 |
+
elif self.use_mla_backend and not self.mambaish_config:
|
| 826 |
+
assert not is_dsa_model
|
| 827 |
+
if is_float4_e2m1fn_x2(self.kv_cache_dtype):
|
| 828 |
+
token_to_kv_pool = self._build_mla_fp4_kv_pool(
|
| 829 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 830 |
+
)
|
| 831 |
+
else:
|
| 832 |
+
token_to_kv_pool = self._build_mla_kv_pool(
|
| 833 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 834 |
+
)
|
| 835 |
+
else:
|
| 836 |
+
if self.is_hybrid_swa:
|
| 837 |
+
token_to_kv_pool = self._build_hybrid_swa_kv_pool(
|
| 838 |
+
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
|
| 839 |
+
swa_max_total_num_tokens=sizes.swa_max_total_num_tokens,
|
| 840 |
+
mha_pool_class=mha_pool_class,
|
| 841 |
+
)
|
| 842 |
+
elif is_minimax_sparse(self.model_config.hf_config):
|
| 843 |
+
token_to_kv_pool = self._build_minimax_sparse_kv_pool(
|
| 844 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 845 |
+
)
|
| 846 |
+
elif self.mambaish_config:
|
| 847 |
+
token_to_kv_pool = self._build_hybrid_linear_kv_pool(
|
| 848 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 849 |
+
req_to_token_pool=req_to_token_pool,
|
| 850 |
+
mha_pool_class=mha_pool_class,
|
| 851 |
+
)
|
| 852 |
+
else:
|
| 853 |
+
quant_method = None
|
| 854 |
+
if is_float4_e2m1fn_x2(self.kv_cache_dtype):
|
| 855 |
+
assert (
|
| 856 |
+
not enable_page_major
|
| 857 |
+
), "page-major KV layout is not supported with fp4 KV cache"
|
| 858 |
+
quant_method = self._build_fp4_quant_method(
|
| 859 |
+
num_layers=self.layer_info.num_effective_layers
|
| 860 |
+
)
|
| 861 |
+
token_to_kv_pool = self._build_mha_kv_pool(
|
| 862 |
+
max_total_num_tokens=sizes.max_total_num_tokens,
|
| 863 |
+
mha_pool_class=mha_pool_class,
|
| 864 |
+
quant_method=quant_method,
|
| 865 |
+
)
|
| 866 |
+
return token_to_kv_pool
|
| 867 |
+
|
| 868 |
+
def _build_dsv4_kv_pool(
|
| 869 |
+
self,
|
| 870 |
+
*,
|
| 871 |
+
max_running_requests: int,
|
| 872 |
+
swa_max_total_num_tokens: Optional[int],
|
| 873 |
+
c4_max_total_num_tokens: int,
|
| 874 |
+
c128_max_total_num_tokens: int,
|
| 875 |
+
c4_state_pool_size: int,
|
| 876 |
+
c128_state_pool_size: int,
|
| 877 |
+
c4_state_dtype: Optional[torch.dtype],
|
| 878 |
+
c128_state_dtype: Optional[torch.dtype],
|
| 879 |
+
req_to_token_pool: ReqToTokenPool,
|
| 880 |
+
) -> KVCache:
|
| 881 |
+
swa_page_size = self.server_args.page_size
|
| 882 |
+
if not _is_npu:
|
| 883 |
+
assert swa_page_size == 256, "In paged swa mode, page_size must be 256."
|
| 884 |
+
|
| 885 |
+
if self.is_draft_worker:
|
| 886 |
+
from sglang.srt.models.agnes_nextn import (
|
| 887 |
+
COMPRESS_RATIO_NEXTN_LAYER,
|
| 888 |
+
)
|
| 889 |
+
|
| 890 |
+
compression_ratios = [
|
| 891 |
+
COMPRESS_RATIO_NEXTN_LAYER
|
| 892 |
+
] * self.layer_info.num_effective_layers
|
| 893 |
+
else:
|
| 894 |
+
compression_ratios = self.model_config.compress_ratios
|
| 895 |
+
|
| 896 |
+
# NPU + DSV4 → paged-state subclass: the fused compressor kernel
|
| 897 |
+
# needs cache_mode=1 (paged); Atlas A3 rejects cache_mode=2 (ring),
|
| 898 |
+
# so the CUDA ring-buffer state path can't be shared. CUDA keeps
|
| 899 |
+
# DeepSeekV4TokenToKVPool unchanged; NPU recomputes state sizes below.
|
| 900 |
+
if _is_npu:
|
| 901 |
+
from sglang.srt.hardware_backend.npu.dsv4.dsv4_memory_pool import (
|
| 902 |
+
DSV4NPUTokenToKVPool,
|
| 903 |
+
npu_state_pool_size,
|
| 904 |
+
)
|
| 905 |
+
|
| 906 |
+
pool_cls = DSV4NPUTokenToKVPool
|
| 907 |
+
# Recompute state pool sizes for the NPU paged formula (CUDA's
|
| 908 |
+
# ring sizes are dropped here). Tail-only allocation keeps the
|
| 909 |
+
# per-req-budget formula sufficient at any prefill length: long
|
| 910 |
+
# prompts allocate only ``tail+128`` (c4) / ``tail`` (c128)
|
| 911 |
+
# slots (tail = seq_len % 128), and decode is drained by
|
| 912 |
+
# sliding eviction in ``ScheduleBatch._evict_swa``.
|
| 913 |
+
c4_state_pool_size = npu_state_pool_size(
|
| 914 |
+
ratio=4,
|
| 915 |
+
page_size=self.server_args.page_size,
|
| 916 |
+
max_num_reqs=max_running_requests,
|
| 917 |
+
)
|
| 918 |
+
c128_state_pool_size = npu_state_pool_size(
|
| 919 |
+
ratio=128,
|
| 920 |
+
page_size=self.server_args.page_size,
|
| 921 |
+
max_num_reqs=max_running_requests,
|
| 922 |
+
)
|
| 923 |
+
else:
|
| 924 |
+
pool_cls = DeepSeekV4TokenToKVPool
|
| 925 |
+
c4_state_pool_size = c4_state_pool_size
|
| 926 |
+
c128_state_pool_size = c128_state_pool_size
|
| 927 |
+
|
| 928 |
+
token_to_kv_pool = pool_cls(
|
| 929 |
+
max_num_reqs=max_running_requests,
|
| 930 |
+
# SWA ring is indexed by req_pool_idx; PD decode inflates req_to_token
|
| 931 |
+
# past max_running_requests (pre-alloc), so size to the real capacity.
|
| 932 |
+
num_req_slots=req_to_token_pool.req_to_token.shape[0],
|
| 933 |
+
swa_size=swa_max_total_num_tokens,
|
| 934 |
+
c4_size=c4_max_total_num_tokens,
|
| 935 |
+
c128_size=c128_max_total_num_tokens,
|
| 936 |
+
c4_state_pool_size=c4_state_pool_size,
|
| 937 |
+
c128_state_pool_size=c128_state_pool_size,
|
| 938 |
+
page_size=self.server_args.page_size,
|
| 939 |
+
swa_page_size=swa_page_size,
|
| 940 |
+
sliding_window=self.model_config.window_size,
|
| 941 |
+
dtype=self.kv_cache_dtype,
|
| 942 |
+
c4_state_dtype=c4_state_dtype,
|
| 943 |
+
c128_state_dtype=c128_state_dtype,
|
| 944 |
+
qk_nope_head_dim=self.model_config.qk_nope_head_dim,
|
| 945 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 946 |
+
indexer_head_dim=self.model_config.index_head_dim,
|
| 947 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 948 |
+
device=self.device,
|
| 949 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 950 |
+
compression_ratios=compression_ratios,
|
| 951 |
+
start_layer=self.layer_info.start_layer,
|
| 952 |
+
end_layer=self.layer_info.end_layer,
|
| 953 |
+
enable_hisparse=self.server_args.enable_hisparse,
|
| 954 |
+
online_mtp_max_draft_tokens=(
|
| 955 |
+
self.server_args.max_speculative_num_draft_tokens or 0
|
| 956 |
+
),
|
| 957 |
+
)
|
| 958 |
+
return token_to_kv_pool
|
| 959 |
+
|
| 960 |
+
def _build_oot_dsa_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 961 |
+
PoolCls = current_platform.get_dsa_kv_pool_cls()
|
| 962 |
+
token_to_kv_pool = PoolCls(
|
| 963 |
+
max_total_num_tokens,
|
| 964 |
+
page_size=self.server_args.page_size,
|
| 965 |
+
dtype=self.kv_cache_dtype,
|
| 966 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 967 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 968 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 969 |
+
device=self.device,
|
| 970 |
+
kv_cache_dim=calculate_mla_kv_cache_dim(
|
| 971 |
+
model_config=self.model_config,
|
| 972 |
+
kv_cache_dtype=self.kv_cache_dtype,
|
| 973 |
+
server_args=self.server_args,
|
| 974 |
+
),
|
| 975 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 976 |
+
start_layer=self.layer_info.start_layer,
|
| 977 |
+
end_layer=self.layer_info.end_layer,
|
| 978 |
+
index_head_dim=get_dsa_index_head_dim(self.model_config.hf_config),
|
| 979 |
+
)
|
| 980 |
+
return token_to_kv_pool
|
| 981 |
+
|
| 982 |
+
def _build_oot_mla_kv_pool(
|
| 983 |
+
self, *, max_total_num_tokens: int, is_dsa_model: bool
|
| 984 |
+
) -> KVCache:
|
| 985 |
+
PoolCls = current_platform.get_mla_kv_pool_cls()
|
| 986 |
+
token_to_kv_pool = PoolCls(
|
| 987 |
+
max_total_num_tokens,
|
| 988 |
+
page_size=self.server_args.page_size,
|
| 989 |
+
dtype=self.kv_cache_dtype,
|
| 990 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 991 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 992 |
+
index_head_dim=(self.model_config.index_head_dim if is_dsa_model else None),
|
| 993 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 994 |
+
device=self.device,
|
| 995 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 996 |
+
start_layer=self.layer_info.start_layer,
|
| 997 |
+
end_layer=self.layer_info.end_layer,
|
| 998 |
+
)
|
| 999 |
+
return token_to_kv_pool
|
| 1000 |
+
|
| 1001 |
+
def _build_oot_mha_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1002 |
+
PoolCls = current_platform.get_mha_kv_pool_cls()
|
| 1003 |
+
token_to_kv_pool = PoolCls(
|
| 1004 |
+
max_total_num_tokens,
|
| 1005 |
+
page_size=self.server_args.page_size,
|
| 1006 |
+
dtype=self.kv_cache_dtype,
|
| 1007 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1008 |
+
head_dim=self.model_config.head_dim,
|
| 1009 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1010 |
+
device=self.device,
|
| 1011 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1012 |
+
start_layer=self.layer_info.start_layer,
|
| 1013 |
+
end_layer=self.layer_info.end_layer,
|
| 1014 |
+
)
|
| 1015 |
+
return token_to_kv_pool
|
| 1016 |
+
|
| 1017 |
+
def _build_ascend_swa_kv_pool(
|
| 1018 |
+
self,
|
| 1019 |
+
*,
|
| 1020 |
+
full_max_total_num_tokens: Optional[int],
|
| 1021 |
+
swa_max_total_num_tokens: Optional[int],
|
| 1022 |
+
) -> KVCache:
|
| 1023 |
+
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
|
| 1024 |
+
NPUMHATokenToKVPool,
|
| 1025 |
+
)
|
| 1026 |
+
|
| 1027 |
+
kwargs = {}
|
| 1028 |
+
if self.is_hybrid_swa_compress:
|
| 1029 |
+
kwargs = {
|
| 1030 |
+
"swa_head_num": max(
|
| 1031 |
+
1,
|
| 1032 |
+
self.model_config.hf_text_config.swa_num_key_value_heads
|
| 1033 |
+
// get_parallel().attn_tp_size,
|
| 1034 |
+
),
|
| 1035 |
+
"swa_head_dim": self.model_config.swa_head_dim,
|
| 1036 |
+
"swa_v_head_dim": self.model_config.swa_v_head_dim,
|
| 1037 |
+
"v_head_dim": self.model_config.v_head_dim,
|
| 1038 |
+
}
|
| 1039 |
+
token_to_kv_pool = SWAKVPool(
|
| 1040 |
+
size=full_max_total_num_tokens,
|
| 1041 |
+
size_swa=swa_max_total_num_tokens,
|
| 1042 |
+
page_size=self.server_args.page_size,
|
| 1043 |
+
dtype=self.kv_cache_dtype,
|
| 1044 |
+
post_capture_active=self.post_capture_kv_active,
|
| 1045 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1046 |
+
head_dim=self.model_config.head_dim,
|
| 1047 |
+
swa_attention_layer_ids=self.model_config.swa_attention_layer_ids,
|
| 1048 |
+
full_attention_layer_ids=self.model_config.full_attention_layer_ids,
|
| 1049 |
+
device=self.device,
|
| 1050 |
+
token_to_kv_pool_class=NPUMHATokenToKVPool,
|
| 1051 |
+
**kwargs,
|
| 1052 |
+
)
|
| 1053 |
+
return token_to_kv_pool
|
| 1054 |
+
|
| 1055 |
+
def _build_ascend_mla_kv_pool(
|
| 1056 |
+
self, *, max_total_num_tokens: int, is_dsa_model: bool
|
| 1057 |
+
) -> KVCache:
|
| 1058 |
+
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
|
| 1059 |
+
NPUMLATokenToKVPool,
|
| 1060 |
+
)
|
| 1061 |
+
|
| 1062 |
+
token_to_kv_pool = NPUMLATokenToKVPool(
|
| 1063 |
+
max_total_num_tokens,
|
| 1064 |
+
page_size=self.server_args.page_size,
|
| 1065 |
+
dtype=self.kv_cache_dtype,
|
| 1066 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 1067 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 1068 |
+
index_head_dim=(self.model_config.index_head_dim if is_dsa_model else None),
|
| 1069 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1070 |
+
device=self.device,
|
| 1071 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1072 |
+
start_layer=self.layer_info.start_layer,
|
| 1073 |
+
end_layer=self.layer_info.end_layer,
|
| 1074 |
+
)
|
| 1075 |
+
return token_to_kv_pool
|
| 1076 |
+
|
| 1077 |
+
def _build_ascend_mha_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1078 |
+
from sglang.srt.hardware_backend.npu.memory_pool_npu import (
|
| 1079 |
+
NPUMHATokenToKVPool,
|
| 1080 |
+
)
|
| 1081 |
+
|
| 1082 |
+
token_to_kv_pool = NPUMHATokenToKVPool(
|
| 1083 |
+
max_total_num_tokens,
|
| 1084 |
+
page_size=self.server_args.page_size,
|
| 1085 |
+
dtype=self.kv_cache_dtype,
|
| 1086 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1087 |
+
head_dim=self.model_config.head_dim,
|
| 1088 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1089 |
+
device=self.device,
|
| 1090 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1091 |
+
start_layer=self.layer_info.start_layer,
|
| 1092 |
+
end_layer=self.layer_info.end_layer,
|
| 1093 |
+
)
|
| 1094 |
+
return token_to_kv_pool
|
| 1095 |
+
|
| 1096 |
+
def _build_dsa_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1097 |
+
from sglang.srt.layers.cp.utils import get_glm_dsa_cp_layer_shard_info
|
| 1098 |
+
|
| 1099 |
+
(
|
| 1100 |
+
dsa_cp_layer_shard_rank,
|
| 1101 |
+
dsa_cp_layer_shard_size,
|
| 1102 |
+
) = get_glm_dsa_cp_layer_shard_info(self)
|
| 1103 |
+
pool_kwargs = {}
|
| 1104 |
+
if self.server_args.enable_hisparse:
|
| 1105 |
+
PoolCls = HiSparseDSATokenToKVPool
|
| 1106 |
+
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
| 1107 |
+
|
| 1108 |
+
pool_kwargs["host_to_device_ratio"] = parse_hisparse_config(
|
| 1109 |
+
self.server_args
|
| 1110 |
+
).host_to_device_ratio
|
| 1111 |
+
elif dsa_cp_layer_shard_rank is not None:
|
| 1112 |
+
# DSA cache layer split: shard KV/indexer layers across CP ranks.
|
| 1113 |
+
from sglang.srt.mem_cache.dsa_cache_layer_split import (
|
| 1114 |
+
LayerSplitDSATokenToKVPool,
|
| 1115 |
+
)
|
| 1116 |
+
|
| 1117 |
+
PoolCls = LayerSplitDSATokenToKVPool
|
| 1118 |
+
pool_kwargs["layer_shard_rank"] = dsa_cp_layer_shard_rank
|
| 1119 |
+
pool_kwargs["layer_shard_size"] = dsa_cp_layer_shard_size
|
| 1120 |
+
else:
|
| 1121 |
+
PoolCls = DSATokenToKVPool
|
| 1122 |
+
token_to_kv_pool = PoolCls(
|
| 1123 |
+
max_total_num_tokens,
|
| 1124 |
+
page_size=self.server_args.page_size,
|
| 1125 |
+
dtype=self.kv_cache_dtype,
|
| 1126 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 1127 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 1128 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1129 |
+
device=self.device,
|
| 1130 |
+
kv_cache_dim=calculate_mla_kv_cache_dim(
|
| 1131 |
+
model_config=self.model_config,
|
| 1132 |
+
kv_cache_dtype=self.kv_cache_dtype,
|
| 1133 |
+
server_args=self.server_args,
|
| 1134 |
+
),
|
| 1135 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1136 |
+
start_layer=self.layer_info.start_layer,
|
| 1137 |
+
end_layer=self.layer_info.end_layer,
|
| 1138 |
+
index_head_dim=get_dsa_index_head_dim(self.model_config.hf_config),
|
| 1139 |
+
**pool_kwargs,
|
| 1140 |
+
)
|
| 1141 |
+
return token_to_kv_pool
|
| 1142 |
+
|
| 1143 |
+
def _build_mla_fp4_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1144 |
+
token_to_kv_pool = MLATokenToKVPoolFP4(
|
| 1145 |
+
max_total_num_tokens,
|
| 1146 |
+
page_size=self.server_args.page_size,
|
| 1147 |
+
dtype=self.kv_cache_dtype,
|
| 1148 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 1149 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 1150 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1151 |
+
device=self.device,
|
| 1152 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1153 |
+
start_layer=self.layer_info.start_layer,
|
| 1154 |
+
end_layer=self.layer_info.end_layer,
|
| 1155 |
+
)
|
| 1156 |
+
return token_to_kv_pool
|
| 1157 |
+
|
| 1158 |
+
def _build_mla_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1159 |
+
token_to_kv_pool = MLATokenToKVPool(
|
| 1160 |
+
max_total_num_tokens,
|
| 1161 |
+
page_size=self.server_args.page_size,
|
| 1162 |
+
dtype=self.kv_cache_dtype,
|
| 1163 |
+
kv_lora_rank=self.model_config.kv_lora_rank,
|
| 1164 |
+
qk_rope_head_dim=self.model_config.qk_rope_head_dim,
|
| 1165 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1166 |
+
device=self.device,
|
| 1167 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1168 |
+
start_layer=self.layer_info.start_layer,
|
| 1169 |
+
end_layer=self.layer_info.end_layer,
|
| 1170 |
+
)
|
| 1171 |
+
return token_to_kv_pool
|
| 1172 |
+
|
| 1173 |
+
def _build_hybrid_swa_kv_pool(
|
| 1174 |
+
self,
|
| 1175 |
+
*,
|
| 1176 |
+
full_max_total_num_tokens: Optional[int],
|
| 1177 |
+
swa_max_total_num_tokens: Optional[int],
|
| 1178 |
+
mha_pool_class: type,
|
| 1179 |
+
) -> KVCache:
|
| 1180 |
+
kwargs = {}
|
| 1181 |
+
if self.is_hybrid_swa_compress:
|
| 1182 |
+
kwargs = {
|
| 1183 |
+
"swa_head_num": max(
|
| 1184 |
+
1,
|
| 1185 |
+
self.model_config.hf_text_config.swa_num_key_value_heads
|
| 1186 |
+
// get_parallel().attn_tp_size,
|
| 1187 |
+
),
|
| 1188 |
+
"swa_head_dim": self.model_config.swa_head_dim,
|
| 1189 |
+
"swa_v_head_dim": self.model_config.swa_v_head_dim,
|
| 1190 |
+
"v_head_dim": self.model_config.v_head_dim,
|
| 1191 |
+
}
|
| 1192 |
+
swa_pool_class = (
|
| 1193 |
+
MHATokenToKVPoolMXFP8
|
| 1194 |
+
if self.server_args.kv_cache_dtype == "mxfp8"
|
| 1195 |
+
else mha_pool_class
|
| 1196 |
+
)
|
| 1197 |
+
swa_attention_layer_ids = self.model_config.swa_attention_layer_ids
|
| 1198 |
+
full_attention_layer_ids = self.model_config.full_attention_layer_ids
|
| 1199 |
+
if self.is_inkling_mtp_draft:
|
| 1200 |
+
if self.draft_swa_full_capacity:
|
| 1201 |
+
# Banded 's' depth: route the draft's single layer into the SWA
|
| 1202 |
+
# ring sub-pool so use_sliding_window_kv_pool activates the SWA
|
| 1203 |
+
# store/read path for this depth, exactly like a trunk local
|
| 1204 |
+
# layer.
|
| 1205 |
+
swa_attention_layer_ids = [self.draft_model_idx]
|
| 1206 |
+
full_attention_layer_ids = []
|
| 1207 |
+
else:
|
| 1208 |
+
swa_attention_layer_ids = []
|
| 1209 |
+
full_attention_layer_ids = [self.draft_model_idx]
|
| 1210 |
+
# Size the banded draft's SWA ring to FULL draft capacity (not the
|
| 1211 |
+
# trunk-window-derived swa_max): with the identity full->swa mapping
|
| 1212 |
+
# registered in _build_token_to_kv_pool_allocator, every logical slot
|
| 1213 |
+
# the shared target allocator hands out (up to full_max) must be
|
| 1214 |
+
# addressable in the ring, whatever the head-vs-trunk window
|
| 1215 |
+
# relationship.
|
| 1216 |
+
size_swa = (
|
| 1217 |
+
full_max_total_num_tokens
|
| 1218 |
+
if self.draft_swa_full_capacity
|
| 1219 |
+
else swa_max_total_num_tokens
|
| 1220 |
+
)
|
| 1221 |
+
token_to_kv_pool = SWAKVPool(
|
| 1222 |
+
size=full_max_total_num_tokens,
|
| 1223 |
+
size_swa=size_swa,
|
| 1224 |
+
page_size=self.server_args.page_size,
|
| 1225 |
+
dtype=self.kv_cache_dtype,
|
| 1226 |
+
post_capture_active=self.post_capture_kv_active,
|
| 1227 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1228 |
+
head_dim=self.model_config.head_dim,
|
| 1229 |
+
swa_attention_layer_ids=swa_attention_layer_ids,
|
| 1230 |
+
full_attention_layer_ids=full_attention_layer_ids,
|
| 1231 |
+
device=self.device,
|
| 1232 |
+
enable_kv_cache_copy=(self.server_args.speculative_algorithm is not None),
|
| 1233 |
+
token_to_kv_pool_class=swa_pool_class,
|
| 1234 |
+
**kwargs,
|
| 1235 |
+
)
|
| 1236 |
+
return token_to_kv_pool
|
| 1237 |
+
|
| 1238 |
+
def _build_minimax_sparse_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1239 |
+
_hf_config = self.model_config.hf_config
|
| 1240 |
+
sparse_cfg = get_minimax_sparse_attention_config(_hf_config)
|
| 1241 |
+
dense_layer_ids, sparse_layer_ids = get_minimax_sparse_layer_ids(sparse_cfg)
|
| 1242 |
+
disable_value_sparse_layer_ids = get_minimax_sparse_disable_value_layer_ids(
|
| 1243 |
+
sparse_cfg
|
| 1244 |
+
)
|
| 1245 |
+
token_to_kv_pool = MiniMaxSparseKVPool(
|
| 1246 |
+
size=max_total_num_tokens,
|
| 1247 |
+
page_size=self.server_args.page_size,
|
| 1248 |
+
dtype=self.kv_cache_dtype,
|
| 1249 |
+
index_dtype=self.model_dtype,
|
| 1250 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1251 |
+
head_dim=self.model_config.head_dim,
|
| 1252 |
+
idx_head_dim=sparse_cfg["sparse_index_dim"],
|
| 1253 |
+
dense_layer_ids=dense_layer_ids,
|
| 1254 |
+
sparse_layer_ids=sparse_layer_ids,
|
| 1255 |
+
disable_value_sparse_layer_ids=disable_value_sparse_layer_ids,
|
| 1256 |
+
device=self.device,
|
| 1257 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1258 |
+
start_layer=self.layer_info.start_layer,
|
| 1259 |
+
end_layer=self.layer_info.end_layer,
|
| 1260 |
+
)
|
| 1261 |
+
return token_to_kv_pool
|
| 1262 |
+
|
| 1263 |
+
def _build_hybrid_linear_kv_pool(
|
| 1264 |
+
self,
|
| 1265 |
+
*,
|
| 1266 |
+
max_total_num_tokens: int,
|
| 1267 |
+
req_to_token_pool: ReqToTokenPool,
|
| 1268 |
+
mha_pool_class: type,
|
| 1269 |
+
) -> KVCache:
|
| 1270 |
+
extra_args = {}
|
| 1271 |
+
if self.use_mla_backend:
|
| 1272 |
+
extra_args = {
|
| 1273 |
+
"kv_lora_rank": self.model_config.kv_lora_rank,
|
| 1274 |
+
"qk_rope_head_dim": self.model_config.qk_rope_head_dim,
|
| 1275 |
+
}
|
| 1276 |
+
full_attention_layer_ids = (
|
| 1277 |
+
[0]
|
| 1278 |
+
if self.is_draft_worker
|
| 1279 |
+
else [
|
| 1280 |
+
i
|
| 1281 |
+
for i in self.mambaish_config.full_attention_layer_ids
|
| 1282 |
+
if self.layer_info.start_layer <= i < self.layer_info.end_layer
|
| 1283 |
+
]
|
| 1284 |
+
)
|
| 1285 |
+
quant_method = self._build_fp4_quant_method(
|
| 1286 |
+
num_layers=len(full_attention_layer_ids)
|
| 1287 |
+
)
|
| 1288 |
+
# MXFP8 KV cache needs the block-scaled pool (data + UE8M0 scale
|
| 1289 |
+
# buffers) for the full-attention layers, same as the SWA branch.
|
| 1290 |
+
full_pool_class = (
|
| 1291 |
+
MHATokenToKVPoolMXFP8
|
| 1292 |
+
if self.server_args.kv_cache_dtype == "mxfp8" and not self.use_mla_backend
|
| 1293 |
+
else mha_pool_class
|
| 1294 |
+
)
|
| 1295 |
+
token_to_kv_pool = HybridLinearKVPool(
|
| 1296 |
+
page_size=self.server_args.page_size,
|
| 1297 |
+
size=max_total_num_tokens,
|
| 1298 |
+
dtype=self.kv_cache_dtype,
|
| 1299 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1300 |
+
head_dim=self.model_config.head_dim,
|
| 1301 |
+
# if draft worker, we only need 1 attention layer's kv pool
|
| 1302 |
+
full_attention_layer_ids=full_attention_layer_ids,
|
| 1303 |
+
device=self.device,
|
| 1304 |
+
mamba_pool=req_to_token_pool.mamba_pool,
|
| 1305 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1306 |
+
enable_kv_cache_copy=(self.server_args.speculative_algorithm is not None),
|
| 1307 |
+
use_mla=self.use_mla_backend,
|
| 1308 |
+
start_layer=self.layer_info.start_layer,
|
| 1309 |
+
full_kv_pool_class=full_pool_class,
|
| 1310 |
+
quant_method=quant_method,
|
| 1311 |
+
post_capture_active=self.post_capture_kv_active and quant_method is None,
|
| 1312 |
+
**extra_args,
|
| 1313 |
+
)
|
| 1314 |
+
return token_to_kv_pool
|
| 1315 |
+
|
| 1316 |
+
def _build_mha_fp4_kv_pool(self, *, max_total_num_tokens: int) -> KVCache:
|
| 1317 |
+
token_to_kv_pool = MHATokenToKVPoolFP4(
|
| 1318 |
+
max_total_num_tokens,
|
| 1319 |
+
page_size=self.server_args.page_size,
|
| 1320 |
+
dtype=self.kv_cache_dtype,
|
| 1321 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1322 |
+
head_dim=self.model_config.head_dim,
|
| 1323 |
+
v_head_dim=self.model_config.v_head_dim,
|
| 1324 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1325 |
+
device=self.device,
|
| 1326 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1327 |
+
start_layer=self.layer_info.start_layer,
|
| 1328 |
+
end_layer=self.layer_info.end_layer,
|
| 1329 |
+
enable_alt_stream=not self.server_args.enable_pdmux,
|
| 1330 |
+
enable_kv_cache_copy=(self.server_args.speculative_algorithm is not None),
|
| 1331 |
+
)
|
| 1332 |
+
return token_to_kv_pool
|
| 1333 |
+
|
| 1334 |
+
def _build_mha_kv_pool(
|
| 1335 |
+
self, *, max_total_num_tokens: int, mha_pool_class: type, quant_method=None
|
| 1336 |
+
) -> KVCache:
|
| 1337 |
+
if self.server_args.kv_cache_dtype == "mxfp8":
|
| 1338 |
+
pool_cls = MHATokenToKVPoolMXFP8
|
| 1339 |
+
else:
|
| 1340 |
+
pool_cls = (
|
| 1341 |
+
NoOpMHATokenToKVPool
|
| 1342 |
+
if self.server_args.prefill_only_disable_kv_cache
|
| 1343 |
+
else mha_pool_class
|
| 1344 |
+
)
|
| 1345 |
+
pool_kwargs = {}
|
| 1346 |
+
if quant_method is not None:
|
| 1347 |
+
pool_kwargs["quant_method"] = quant_method
|
| 1348 |
+
else:
|
| 1349 |
+
pool_kwargs["post_capture_active"] = self.post_capture_kv_active
|
| 1350 |
+
token_to_kv_pool = pool_cls(
|
| 1351 |
+
max_total_num_tokens,
|
| 1352 |
+
page_size=self.server_args.page_size,
|
| 1353 |
+
dtype=self.kv_cache_dtype,
|
| 1354 |
+
head_num=self.model_config.get_num_kv_heads(get_parallel().attn_tp_size),
|
| 1355 |
+
head_dim=self.model_config.head_dim,
|
| 1356 |
+
v_head_dim=self.model_config.v_head_dim,
|
| 1357 |
+
layer_num=self.layer_info.num_effective_layers,
|
| 1358 |
+
device=self.device,
|
| 1359 |
+
enable_memory_saver=self.server_args.enable_memory_saver,
|
| 1360 |
+
start_layer=self.layer_info.start_layer,
|
| 1361 |
+
end_layer=self.layer_info.end_layer,
|
| 1362 |
+
enable_alt_stream=not self.server_args.enable_pdmux,
|
| 1363 |
+
enable_kv_cache_copy=(self.server_args.speculative_algorithm is not None),
|
| 1364 |
+
**pool_kwargs,
|
| 1365 |
+
)
|
| 1366 |
+
return token_to_kv_pool
|
| 1367 |
+
|
| 1368 |
+
def _build_token_to_kv_pool_allocator(
|
| 1369 |
+
self,
|
| 1370 |
+
*,
|
| 1371 |
+
sizes: _PoolSizes,
|
| 1372 |
+
token_to_kv_pool: KVCache,
|
| 1373 |
+
is_dsv4_model: bool,
|
| 1374 |
+
req_to_token_pool: ReqToTokenPool,
|
| 1375 |
+
token_to_kv_pool_allocator: Optional[BaseTokenToKVPoolAllocator],
|
| 1376 |
+
) -> BaseTokenToKVPoolAllocator:
|
| 1377 |
+
# Initialize token_to_kv_pool_allocator
|
| 1378 |
+
need_sort = self.server_args.disaggregation_mode in ("decode", "prefill")
|
| 1379 |
+
if token_to_kv_pool_allocator is None:
|
| 1380 |
+
if current_platform.is_out_of_tree():
|
| 1381 |
+
AllocatorCls = current_platform.get_paged_allocator_cls()
|
| 1382 |
+
token_to_kv_pool_allocator = AllocatorCls(
|
| 1383 |
+
sizes.max_total_num_tokens,
|
| 1384 |
+
page_size=self.server_args.page_size,
|
| 1385 |
+
dtype=self.kv_cache_dtype,
|
| 1386 |
+
device=self.device,
|
| 1387 |
+
kvcache=token_to_kv_pool,
|
| 1388 |
+
need_sort=need_sort,
|
| 1389 |
+
)
|
| 1390 |
+
elif _is_npu and (
|
| 1391 |
+
self.server_args.attention_backend == "ascend"
|
| 1392 |
+
or is_dsv4_model
|
| 1393 |
+
or self.hybrid_gdn_config is not None
|
| 1394 |
+
):
|
| 1395 |
+
if self.is_hybrid_swa:
|
| 1396 |
+
# DSV4 on NPU: SWA allocator subclass that also drives the
|
| 1397 |
+
# c4/c128 allocators, producing a DSV4OutCacheLoc per alloc.
|
| 1398 |
+
if is_dsv4_model:
|
| 1399 |
+
from sglang.srt.hardware_backend.npu.dsv4.dsv4_allocator import (
|
| 1400 |
+
DSV4NPUTokenToKVPoolAllocator,
|
| 1401 |
+
)
|
| 1402 |
+
|
| 1403 |
+
swa_allocator_cls = DSV4NPUTokenToKVPoolAllocator
|
| 1404 |
+
else:
|
| 1405 |
+
swa_allocator_cls = SWATokenToKVPoolAllocator
|
| 1406 |
+
token_to_kv_pool_allocator = swa_allocator_cls(
|
| 1407 |
+
sizes.full_max_total_num_tokens,
|
| 1408 |
+
sizes.swa_max_total_num_tokens,
|
| 1409 |
+
page_size=self.server_args.page_size,
|
| 1410 |
+
dtype=self.kv_cache_dtype,
|
| 1411 |
+
device=self.device,
|
| 1412 |
+
kvcache=token_to_kv_pool,
|
| 1413 |
+
need_sort=need_sort,
|
| 1414 |
+
)
|
| 1415 |
+
else:
|
| 1416 |
+
from sglang.srt.hardware_backend.npu.allocator_npu import (
|
| 1417 |
+
NPUPagedTokenToKVPoolAllocator,
|
| 1418 |
+
)
|
| 1419 |
+
|
| 1420 |
+
token_to_kv_pool_allocator = NPUPagedTokenToKVPoolAllocator(
|
| 1421 |
+
sizes.max_total_num_tokens,
|
| 1422 |
+
page_size=self.server_args.page_size,
|
| 1423 |
+
dtype=self.kv_cache_dtype,
|
| 1424 |
+
device=self.device,
|
| 1425 |
+
kvcache=token_to_kv_pool,
|
| 1426 |
+
need_sort=need_sort,
|
| 1427 |
+
)
|
| 1428 |
+
else:
|
| 1429 |
+
if self.is_hybrid_swa and sizes.full_max_total_num_tokens == 0:
|
| 1430 |
+
token_to_kv_pool_allocator = PureSWATokenToKVPoolAllocator(
|
| 1431 |
+
sizes.swa_max_total_num_tokens,
|
| 1432 |
+
page_size=self.server_args.page_size,
|
| 1433 |
+
dtype=self.kv_cache_dtype,
|
| 1434 |
+
device=self.device,
|
| 1435 |
+
kvcache=token_to_kv_pool,
|
| 1436 |
+
need_sort=need_sort,
|
| 1437 |
+
)
|
| 1438 |
+
elif self.is_hybrid_swa:
|
| 1439 |
+
token_to_kv_pool_allocator = SWATokenToKVPoolAllocator(
|
| 1440 |
+
sizes.full_max_total_num_tokens,
|
| 1441 |
+
sizes.swa_max_total_num_tokens,
|
| 1442 |
+
page_size=self.server_args.page_size,
|
| 1443 |
+
dtype=self.kv_cache_dtype,
|
| 1444 |
+
device=self.device,
|
| 1445 |
+
kvcache=token_to_kv_pool,
|
| 1446 |
+
need_sort=need_sort,
|
| 1447 |
+
)
|
| 1448 |
+
else:
|
| 1449 |
+
if self.server_args.enable_hisparse:
|
| 1450 |
+
from sglang.srt.mem_cache.sparsity import (
|
| 1451 |
+
parse_hisparse_config,
|
| 1452 |
+
)
|
| 1453 |
+
|
| 1454 |
+
hisparse_cfg = parse_hisparse_config(self.server_args)
|
| 1455 |
+
token_to_kv_pool_allocator = HiSparseTokenToKVPoolAllocator(
|
| 1456 |
+
sizes.max_total_num_tokens,
|
| 1457 |
+
page_size=self.server_args.page_size,
|
| 1458 |
+
dtype=self.kv_cache_dtype,
|
| 1459 |
+
device=self.device,
|
| 1460 |
+
kvcache=token_to_kv_pool,
|
| 1461 |
+
need_sort=need_sort,
|
| 1462 |
+
host_to_device_ratio=hisparse_cfg.host_to_device_ratio,
|
| 1463 |
+
)
|
| 1464 |
+
elif (
|
| 1465 |
+
self.server_args.page_size == 1
|
| 1466 |
+
and self.server_args.dcp_size == 1
|
| 1467 |
+
):
|
| 1468 |
+
token_to_kv_pool_allocator = TokenToKVPoolAllocator(
|
| 1469 |
+
sizes.max_total_num_tokens,
|
| 1470 |
+
dtype=self.kv_cache_dtype,
|
| 1471 |
+
device=self.device,
|
| 1472 |
+
kvcache=token_to_kv_pool,
|
| 1473 |
+
need_sort=need_sort,
|
| 1474 |
+
)
|
| 1475 |
+
else:
|
| 1476 |
+
token_to_kv_pool_allocator = PagedTokenToKVPoolAllocator(
|
| 1477 |
+
sizes.max_total_num_tokens * self.server_args.dcp_size,
|
| 1478 |
+
page_size=self.server_args.page_size
|
| 1479 |
+
* self.server_args.dcp_size,
|
| 1480 |
+
dtype=self.kv_cache_dtype,
|
| 1481 |
+
device=self.device,
|
| 1482 |
+
kvcache=token_to_kv_pool,
|
| 1483 |
+
need_sort=need_sort,
|
| 1484 |
+
)
|
| 1485 |
+
|
| 1486 |
+
if self.server_args.enable_hisparse and is_dsv4_model:
|
| 1487 |
+
assert self.is_hybrid_swa, "DeepSeek V4 HiSparse requires SWA mode."
|
| 1488 |
+
token_to_kv_pool_allocator = DeepSeekV4HiSparseTokenToKVPoolAllocator(
|
| 1489 |
+
token_to_kv_pool_allocator
|
| 1490 |
+
)
|
| 1491 |
+
|
| 1492 |
+
# DSV4-NPU: wire allocator back-ref into req_to_token_pool so its
|
| 1493 |
+
# free(req) can release c4/c128 pool pages alongside the slot.
|
| 1494 |
+
if hasattr(req_to_token_pool, "register_dsv4_allocator"):
|
| 1495 |
+
req_to_token_pool.register_dsv4_allocator(token_to_kv_pool_allocator)
|
| 1496 |
+
|
| 1497 |
+
else:
|
| 1498 |
+
assert self.is_draft_worker
|
| 1499 |
+
if self.is_hybrid_swa:
|
| 1500 |
+
if self.draft_swa_full_capacity:
|
| 1501 |
+
# Banded depth: the SWA ring is full draft capacity, so use
|
| 1502 |
+
# an IDENTITY full->swa mapping — store and read locs both
|
| 1503 |
+
# equal out_cache_loc, and a slot is never evicted before
|
| 1504 |
+
# the request frees it. The window itself is enforced by the
|
| 1505 |
+
# FA sliding-window kernel, not by the ring. Layout mirrors
|
| 1506 |
+
# SWATokenToKVPoolAllocator's mapping (size + page_size
|
| 1507 |
+
# entries + trailing -1 sentinel so a -1 last_loc maps
|
| 1508 |
+
# to -1).
|
| 1509 |
+
n = sizes.full_max_total_num_tokens + self.page_size
|
| 1510 |
+
identity_mapping = torch.arange(
|
| 1511 |
+
n + 1, dtype=torch.int64, device=self.device
|
| 1512 |
+
)
|
| 1513 |
+
identity_mapping[-1] = -1
|
| 1514 |
+
token_to_kv_pool.register_mapping(identity_mapping)
|
| 1515 |
+
else:
|
| 1516 |
+
swa_allocator = getattr(
|
| 1517 |
+
token_to_kv_pool_allocator,
|
| 1518 |
+
"logical_attn_allocator",
|
| 1519 |
+
token_to_kv_pool_allocator,
|
| 1520 |
+
)
|
| 1521 |
+
assert isinstance(swa_allocator, SWATokenToKVPoolAllocator)
|
| 1522 |
+
token_to_kv_pool.register_mapping(
|
| 1523 |
+
swa_allocator.full_to_swa_index_mapping
|
| 1524 |
+
)
|
| 1525 |
+
return token_to_kv_pool_allocator
|
| 1526 |
+
|
| 1527 |
+
def _profile_available_bytes(self, pre_model_load_memory: int) -> int:
|
| 1528 |
+
# KV pool budget = currently-free GPU memory minus the non-static runtime
|
| 1529 |
+
# slack (pre_model_load_memory * (1 - mem_fraction_static)). Whatever is
|
| 1530 |
+
# already resident (model weights, etc.) is thus charged against it.
|
| 1531 |
+
available_gpu_memory = get_available_gpu_memory(
|
| 1532 |
+
self.device,
|
| 1533 |
+
self.gpu_id,
|
| 1534 |
+
distributed=get_world_group().world_size > 1,
|
| 1535 |
+
cpu_group=get_world_group().cpu_group,
|
| 1536 |
+
)
|
| 1537 |
+
|
| 1538 |
+
slack_gb = pre_model_load_memory * (1 - self.server_args.mem_fraction_static)
|
| 1539 |
+
if self.mambaish_config is not None and self.post_capture_kv_active:
|
| 1540 |
+
# Mamba state is a fixed pre-capture allocation, so it can't ride the ~0 post-capture slack.
|
| 1541 |
+
slack_gb = max(
|
| 1542 |
+
slack_gb,
|
| 1543 |
+
self.server_args.mamba_pre_capture_reserve_mb(
|
| 1544 |
+
get_device_memory_capacity(self.device)
|
| 1545 |
+
)
|
| 1546 |
+
/ 1024,
|
| 1547 |
+
)
|
| 1548 |
+
rest_memory = available_gpu_memory - slack_gb
|
| 1549 |
+
if self.mambaish_config is not None:
|
| 1550 |
+
rest_memory = self._handle_max_mamba_cache(rest_memory)
|
| 1551 |
+
|
| 1552 |
+
# Loaded weights (target + draft) can exceed the static budget
|
| 1553 |
+
if rest_memory <= 0:
|
| 1554 |
+
minimum_mem_fraction_static = (
|
| 1555 |
+
1 - available_gpu_memory / pre_model_load_memory
|
| 1556 |
+
)
|
| 1557 |
+
suggested_mem_fraction_static = (
|
| 1558 |
+
math.ceil(minimum_mem_fraction_static * 1000) / 1000
|
| 1559 |
+
)
|
| 1560 |
+
raise ValueError(
|
| 1561 |
+
f"Loaded weights leave no GPU memory for the KV cache under "
|
| 1562 |
+
f"--mem-fraction-static={self.server_args.mem_fraction_static}. "
|
| 1563 |
+
f"Raise --mem-fraction-static above "
|
| 1564 |
+
f"{suggested_mem_fraction_static:.3f} "
|
| 1565 |
+
f"(minimum viable = 1 - available/pre = "
|
| 1566 |
+
f"{minimum_mem_fraction_static:.4f}). If using speculative "
|
| 1567 |
+
f"decoding, draft weights are now counted."
|
| 1568 |
+
)
|
| 1569 |
+
|
| 1570 |
+
return int(rest_memory * (1 << 30)) # return in bytes
|
| 1571 |
+
|
| 1572 |
+
def _calculate_mamba_ratio(self) -> int:
|
| 1573 |
+
if self.server_args.disable_radix_cache:
|
| 1574 |
+
return 1
|
| 1575 |
+
|
| 1576 |
+
additional_ratio = 0
|
| 1577 |
+
if self.server_args.enable_mamba_extra_buffer():
|
| 1578 |
+
# ping-pong buffer size is 2 when overlap schedule is on, 1 otherwise.
|
| 1579 |
+
# Lazy mode saves 1 slot (2 → 1) for overlap; non-overlap already uses 1.
|
| 1580 |
+
if not self.server_args.disable_overlap_schedule:
|
| 1581 |
+
if self.server_args.enable_mamba_extra_buffer_lazy():
|
| 1582 |
+
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP_LAZY
|
| 1583 |
+
else:
|
| 1584 |
+
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP
|
| 1585 |
+
else:
|
| 1586 |
+
assert (
|
| 1587 |
+
not self.server_args.enable_mamba_extra_buffer_lazy()
|
| 1588 |
+
), "Lazy extra buffer requires overlap schedule (--disable-overlap-schedule is incompatible)"
|
| 1589 |
+
additional_ratio = MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_OVERLAP
|
| 1590 |
+
|
| 1591 |
+
return MAMBA_CACHE_SIZE_MAX_RUNNING_REQUESTS_RATIO + additional_ratio
|
| 1592 |
+
|
| 1593 |
+
def _apply_token_constraints(self, token_capacity: int) -> int:
|
| 1594 |
+
"""Apply external constraints to token capacity: user cap, PP sync.
|
| 1595 |
+
|
| 1596 |
+
Page alignment is handled by the configurator, not here.
|
| 1597 |
+
If constraints change the value, the configurator re-runs and re-aligns.
|
| 1598 |
+
"""
|
| 1599 |
+
user_limit = self.server_args.max_total_tokens
|
| 1600 |
+
|
| 1601 |
+
# Apply user-specified upper bound
|
| 1602 |
+
if user_limit is not None:
|
| 1603 |
+
if user_limit > token_capacity:
|
| 1604 |
+
logging.warning(
|
| 1605 |
+
f"max_total_tokens={user_limit} is larger than the profiled value "
|
| 1606 |
+
f"{token_capacity}. Use the profiled value instead."
|
| 1607 |
+
)
|
| 1608 |
+
token_capacity = min(token_capacity, user_limit)
|
| 1609 |
+
|
| 1610 |
+
# Sync across PP ranks (each may have different layer counts)
|
| 1611 |
+
if self.server_args.pp_size > 1:
|
| 1612 |
+
tensor = torch.tensor(token_capacity, dtype=torch.int64)
|
| 1613 |
+
torch.distributed.all_reduce(
|
| 1614 |
+
tensor,
|
| 1615 |
+
op=torch.distributed.ReduceOp.MIN,
|
| 1616 |
+
group=get_world_group().cpu_group,
|
| 1617 |
+
)
|
| 1618 |
+
token_capacity = tensor.item()
|
| 1619 |
+
|
| 1620 |
+
return token_capacity
|
| 1621 |
+
|
| 1622 |
+
def resolve_max_num_reqs(self, token_capacity: int) -> int:
|
| 1623 |
+
"""Compute max concurrent requests (per dp worker) from the finalized
|
| 1624 |
+
token capacity."""
|
| 1625 |
+
# Estimate pool size (used as upper bound when user specifies max_running_requests)
|
| 1626 |
+
estimated = int(token_capacity / self.model_config.context_len * 512)
|
| 1627 |
+
estimated = max(min(estimated, 4096), 2048)
|
| 1628 |
+
|
| 1629 |
+
max_num_reqs = self.server_args.max_running_requests
|
| 1630 |
+
if max_num_reqs is not None:
|
| 1631 |
+
requested_per_worker = max_num_reqs // self.ps.attn_dp_size
|
| 1632 |
+
max_num_reqs = min(requested_per_worker, token_capacity // 2)
|
| 1633 |
+
else:
|
| 1634 |
+
requested_per_worker = None
|
| 1635 |
+
max_num_reqs = min(estimated, token_capacity // 2)
|
| 1636 |
+
|
| 1637 |
+
if self.mambaish_config is not None:
|
| 1638 |
+
ratio = self._calculate_mamba_ratio()
|
| 1639 |
+
max_num_reqs = min(
|
| 1640 |
+
max_num_reqs, self.server_args.max_mamba_cache_size // ratio
|
| 1641 |
+
)
|
| 1642 |
+
|
| 1643 |
+
if max_num_reqs <= 0:
|
| 1644 |
+
raise RuntimeError(
|
| 1645 |
+
f"Hybrid (mamba/linear-attention) state cache is too small to serve "
|
| 1646 |
+
f"any requests. max_mamba_cache_size={self.server_args.max_mamba_cache_size}, "
|
| 1647 |
+
f"mamba_ratio={ratio}, resulting max_num_reqs={max_num_reqs}. "
|
| 1648 |
+
f"Try: (1) reduce --max-running-requests, "
|
| 1649 |
+
f"(2) increase --mem-fraction-static, or "
|
| 1650 |
+
f"(3) use GPUs with more memory."
|
| 1651 |
+
)
|
| 1652 |
+
if requested_per_worker is not None and max_num_reqs < requested_per_worker:
|
| 1653 |
+
logger.warning(
|
| 1654 |
+
"max_running_requests was reduced from the requested %d to %d "
|
| 1655 |
+
"(per dp worker) due to the available KV cache capacity.",
|
| 1656 |
+
requested_per_worker,
|
| 1657 |
+
max_num_reqs,
|
| 1658 |
+
)
|
| 1659 |
+
return max_num_reqs
|
| 1660 |
+
|
| 1661 |
+
def _resolve_memory_pool_config(
|
| 1662 |
+
self, pre_model_load_memory: int
|
| 1663 |
+
) -> MemoryPoolConfig:
|
| 1664 |
+
"""Profile GPU memory and resolve all pool parameters into a config."""
|
| 1665 |
+
from sglang.srt.model_executor.pool_configurator import (
|
| 1666 |
+
create_memory_pool_configurator,
|
| 1667 |
+
)
|
| 1668 |
+
|
| 1669 |
+
available_bytes = self._profile_available_bytes(pre_model_load_memory)
|
| 1670 |
+
config = self.config_from_budget(available_bytes)
|
| 1671 |
+
config.max_running_requests = self.resolve_max_num_reqs(
|
| 1672 |
+
config.max_total_num_tokens
|
| 1673 |
+
)
|
| 1674 |
+
configurator = create_memory_pool_configurator(self)
|
| 1675 |
+
config = configurator.finalize_with_max_running_requests(config)
|
| 1676 |
+
config.mem_fraction_static = self.server_args.mem_fraction_static
|
| 1677 |
+
return config
|
| 1678 |
+
|
| 1679 |
+
def config_from_budget(
|
| 1680 |
+
self, budget_bytes: int, *, cap_tokens: Optional[int] = None
|
| 1681 |
+
) -> MemoryPoolConfig:
|
| 1682 |
+
"""Turn a KV byte budget into a pool config via the configurator, re-applying
|
| 1683 |
+
the external token constraints (user cap, page alignment, PP sync) and the
|
| 1684 |
+
optional ``cap_tokens`` clamp."""
|
| 1685 |
+
# Local import avoids a pool_configurator import cycle.
|
| 1686 |
+
from sglang.srt.model_executor.pool_configurator import (
|
| 1687 |
+
create_memory_pool_configurator,
|
| 1688 |
+
)
|
| 1689 |
+
|
| 1690 |
+
configurator = create_memory_pool_configurator(self)
|
| 1691 |
+
config = configurator.calculate_pool_sizes(
|
| 1692 |
+
budget_bytes, self.server_args.page_size
|
| 1693 |
+
)
|
| 1694 |
+
max_tokens = self._apply_token_constraints(config.max_total_num_tokens)
|
| 1695 |
+
if cap_tokens is not None:
|
| 1696 |
+
max_tokens = min(max_tokens, cap_tokens)
|
| 1697 |
+
if max_tokens != config.max_total_num_tokens:
|
| 1698 |
+
config = configurator.calculate_pool_sizes_from_max_tokens(
|
| 1699 |
+
max_tokens, self.server_args.page_size
|
| 1700 |
+
)
|
| 1701 |
+
return config
|
| 1702 |
+
|
| 1703 |
+
def _handle_max_mamba_cache(self, total_rest_memory):
|
| 1704 |
+
config = self.mambaish_config
|
| 1705 |
+
server_args = self.server_args
|
| 1706 |
+
assert config is not None
|
| 1707 |
+
|
| 1708 |
+
has_spec_dec = not self.spec_algorithm.is_none()
|
| 1709 |
+
if has_spec_dec:
|
| 1710 |
+
assert server_args.speculative_num_draft_tokens is not None
|
| 1711 |
+
assert server_args.max_running_requests is not None
|
| 1712 |
+
|
| 1713 |
+
if server_args.max_mamba_cache_size is not None:
|
| 1714 |
+
# Use explicitly set max_mamba_cache_size
|
| 1715 |
+
server_args.override(
|
| 1716 |
+
"mamba_pool.per_dp_shard",
|
| 1717 |
+
max_mamba_cache_size=server_args.max_mamba_cache_size
|
| 1718 |
+
// self.ps.attn_dp_size,
|
| 1719 |
+
)
|
| 1720 |
+
# Reserve intermediate memory based on capped max_num_reqs
|
| 1721 |
+
if has_spec_dec:
|
| 1722 |
+
ratio = self._calculate_mamba_ratio()
|
| 1723 |
+
capped_reqs = min(
|
| 1724 |
+
server_args.max_running_requests // self.ps.attn_dp_size,
|
| 1725 |
+
server_args.max_mamba_cache_size // ratio,
|
| 1726 |
+
)
|
| 1727 |
+
intermediate_size = (
|
| 1728 |
+
config.mamba2_cache_params.mamba_cache_per_req
|
| 1729 |
+
* capped_reqs
|
| 1730 |
+
* server_args.speculative_num_draft_tokens
|
| 1731 |
+
)
|
| 1732 |
+
total_rest_memory = total_rest_memory - (intermediate_size / (1 << 30))
|
| 1733 |
+
elif (
|
| 1734 |
+
server_args.disable_radix_cache
|
| 1735 |
+
and server_args.max_running_requests is not None
|
| 1736 |
+
):
|
| 1737 |
+
# Use explicitly set max_running_requests when radix cache is disabled
|
| 1738 |
+
server_args.override(
|
| 1739 |
+
"mamba_pool.from_max_running_requests",
|
| 1740 |
+
max_mamba_cache_size=server_args.max_running_requests
|
| 1741 |
+
// self.ps.attn_dp_size,
|
| 1742 |
+
)
|
| 1743 |
+
# Reserve intermediate memory based on capped max_num_reqs
|
| 1744 |
+
if has_spec_dec:
|
| 1745 |
+
intermediate_size = (
|
| 1746 |
+
config.mamba2_cache_params.mamba_cache_per_req
|
| 1747 |
+
* server_args.max_mamba_cache_size
|
| 1748 |
+
* server_args.speculative_num_draft_tokens
|
| 1749 |
+
)
|
| 1750 |
+
total_rest_memory = total_rest_memory - (intermediate_size / (1 << 30))
|
| 1751 |
+
else:
|
| 1752 |
+
# Use ratio-based calculation to auto-fit available memory
|
| 1753 |
+
assert config.mamba2_cache_params.mamba_cache_per_req > 0
|
| 1754 |
+
per_req = config.mamba2_cache_params.mamba_cache_per_req
|
| 1755 |
+
|
| 1756 |
+
# Solve jointly for max_mamba_cache_size accounting for intermediate memory.
|
| 1757 |
+
# The mamba budget (from the ratio split) must cover both:
|
| 1758 |
+
# 1. main mamba state: max_mamba_cache_size * per_req
|
| 1759 |
+
# 2. intermediate states: (max_mamba_cache_size / ratio) * D * per_req
|
| 1760 |
+
# So: max_mamba_cache_size * per_req * (1 + D/ratio) = mamba_budget_bytes
|
| 1761 |
+
mamba_budget = (
|
| 1762 |
+
total_rest_memory
|
| 1763 |
+
* server_args.mamba_full_memory_ratio
|
| 1764 |
+
/ (1 + server_args.mamba_full_memory_ratio)
|
| 1765 |
+
)
|
| 1766 |
+
mamba_budget_bytes = mamba_budget * (1 << 30)
|
| 1767 |
+
|
| 1768 |
+
if has_spec_dec:
|
| 1769 |
+
ratio = self._calculate_mamba_ratio()
|
| 1770 |
+
D = server_args.speculative_num_draft_tokens
|
| 1771 |
+
# Joint solve: main_state + intermediate = mamba_budget
|
| 1772 |
+
server_args.override(
|
| 1773 |
+
"mamba_pool.memory_budget_spec",
|
| 1774 |
+
max_mamba_cache_size=int(
|
| 1775 |
+
mamba_budget_bytes // (per_req * (1 + D / ratio))
|
| 1776 |
+
),
|
| 1777 |
+
)
|
| 1778 |
+
# Intermediate memory is included in mamba_budget, subtract it
|
| 1779 |
+
# so the return value only has main_state subtracted from total
|
| 1780 |
+
capped_reqs = min(
|
| 1781 |
+
server_args.max_running_requests // self.ps.attn_dp_size,
|
| 1782 |
+
server_args.max_mamba_cache_size // ratio,
|
| 1783 |
+
)
|
| 1784 |
+
intermediate_size = per_req * capped_reqs * D
|
| 1785 |
+
total_rest_memory = total_rest_memory - (intermediate_size / (1 << 30))
|
| 1786 |
+
else:
|
| 1787 |
+
server_args.override(
|
| 1788 |
+
"mamba_pool.memory_budget",
|
| 1789 |
+
max_mamba_cache_size=int(mamba_budget_bytes // per_req),
|
| 1790 |
+
)
|
| 1791 |
+
|
| 1792 |
+
# Validate: max_mamba_cache_size must be positive after memory allocation.
|
| 1793 |
+
# A non-positive value means GPU memory is insufficient for the requested
|
| 1794 |
+
# configuration. Fail fast with actionable advice instead of silently
|
| 1795 |
+
# producing garbled output at runtime.
|
| 1796 |
+
if server_args.max_mamba_cache_size <= 0:
|
| 1797 |
+
raise RuntimeError(
|
| 1798 |
+
f"Not enough GPU memory for hybrid (mamba/linear-attention) state cache. "
|
| 1799 |
+
f"Computed max_mamba_cache_size={server_args.max_mamba_cache_size} "
|
| 1800 |
+
f"(total_rest_memory={total_rest_memory:.2f} GB, "
|
| 1801 |
+
f"mamba_cache_per_req={config.mamba2_cache_params.mamba_cache_per_req / (1 << 20):.2f} MB). "
|
| 1802 |
+
f"Try: (1) reduce --max-running-requests, "
|
| 1803 |
+
f"(2) increase --mem-fraction-static, "
|
| 1804 |
+
f"(3) reduce --speculative-num-draft-tokens, or "
|
| 1805 |
+
f"(4) use GPUs with more memory."
|
| 1806 |
+
)
|
| 1807 |
+
|
| 1808 |
+
mamba_state_memory = (
|
| 1809 |
+
server_args.max_mamba_cache_size
|
| 1810 |
+
* config.mamba2_cache_params.mamba_cache_per_req
|
| 1811 |
+
/ (1 << 30)
|
| 1812 |
+
)
|
| 1813 |
+
return total_rest_memory - mamba_state_memory
|
| 1814 |
+
|
| 1815 |
+
|
| 1816 |
+
def calculate_mla_kv_cache_dim(
|
| 1817 |
+
*,
|
| 1818 |
+
model_config: ModelConfig,
|
| 1819 |
+
kv_cache_dtype: torch.dtype,
|
| 1820 |
+
server_args: ServerArgs,
|
| 1821 |
+
) -> int:
|
| 1822 |
+
is_dsa_model = is_deepseek_dsa(model_config.hf_config)
|
| 1823 |
+
kv_cache_dtype = kv_cache_dtype
|
| 1824 |
+
kv_lora_rank = model_config.kv_lora_rank
|
| 1825 |
+
qk_rope_head_dim = model_config.qk_rope_head_dim
|
| 1826 |
+
kv_cache_dim = kv_lora_rank + qk_rope_head_dim # default mla kv cache dim
|
| 1827 |
+
|
| 1828 |
+
# For non-DSA models, MLA kv cache dim is simply kv_lora_rank + qk_rope_head_dim
|
| 1829 |
+
if not is_dsa_model:
|
| 1830 |
+
return kv_cache_dim
|
| 1831 |
+
|
| 1832 |
+
# TRTLLM backend does not override kv_cache_dim for MLA kv cache
|
| 1833 |
+
# Assuming dsa prefill and decode backends are the same when using trtllm MLA backend,
|
| 1834 |
+
# since it is not compatible for trtllm and other mla attn backend due to the different
|
| 1835 |
+
# kv cache layout.
|
| 1836 |
+
if (
|
| 1837 |
+
server_args.dsa_prefill_backend == "trtllm"
|
| 1838 |
+
or server_args.dsa_decode_backend == "trtllm"
|
| 1839 |
+
):
|
| 1840 |
+
return kv_cache_dim
|
| 1841 |
+
|
| 1842 |
+
# On HIP, TileLang and AITER DSA kernels consume the raw MLA KV layout:
|
| 1843 |
+
# nope(512 fp8) + rope(64 fp8), without extra per-block scales.
|
| 1844 |
+
if _is_hip and (
|
| 1845 |
+
server_args.dsa_prefill_backend in ("tilelang", "aiter")
|
| 1846 |
+
or server_args.dsa_decode_backend in ("tilelang", "aiter")
|
| 1847 |
+
):
|
| 1848 |
+
return kv_cache_dim
|
| 1849 |
+
|
| 1850 |
+
quant_block_size = DSATokenToKVPool.quant_block_size
|
| 1851 |
+
rope_storage_dtype = DSATokenToKVPool.rope_storage_dtype
|
| 1852 |
+
# Calculate override_kv_cache_dim for FP8 storage in backends that use scaled KV layout
|
| 1853 |
+
# (excluding TRTLLM and HIP raw-layout kernels).
|
| 1854 |
+
# kv_lora_rank + scale storage (kv_lora_rank // quant_block_size * 4 bytes) + rope dimension storage
|
| 1855 |
+
# Note: rope dimension is stored in original dtype (bf16), not quantized to fp8
|
| 1856 |
+
if kv_cache_dtype == torch.float8_e4m3fn:
|
| 1857 |
+
assert (
|
| 1858 |
+
kv_lora_rank % quant_block_size == 0
|
| 1859 |
+
), f"kv_lora_rank {kv_lora_rank} must be multiple of quant_block_size {quant_block_size}"
|
| 1860 |
+
|
| 1861 |
+
return (
|
| 1862 |
+
kv_lora_rank
|
| 1863 |
+
+ kv_lora_rank // quant_block_size * 4
|
| 1864 |
+
+ qk_rope_head_dim * rope_storage_dtype.itemsize
|
| 1865 |
+
)
|
| 1866 |
+
|
| 1867 |
+
return kv_cache_dim
|
sglang_patch/srt/models/agnes.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
sglang_patch/srt/models/agnes_dspark.py
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Iterable, List, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
import msgspec
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
from sglang.jit_kernel.dsv4 import fused_q_norm_rope, fused_rope_inplace
|
| 12 |
+
from sglang.srt.configs.agnes import AgnesConfig
|
| 13 |
+
from sglang.srt.environ import envs
|
| 14 |
+
from sglang.srt.layers.layernorm import RMSNorm
|
| 15 |
+
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
|
| 16 |
+
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
| 17 |
+
from sglang.srt.layers.radix_attention import RadixAttention
|
| 18 |
+
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
|
| 19 |
+
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
| 20 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 21 |
+
from sglang.srt.model_executor.forward_context import get_token_to_kv_pool
|
| 22 |
+
from sglang.srt.model_executor.runner import get_is_capture_mode
|
| 23 |
+
from sglang.srt.model_loader.weight_utils import default_weight_loader
|
| 24 |
+
from sglang.srt.models.dbrx import ReplicatedLinear
|
| 25 |
+
from sglang.srt.models.agnes import (
|
| 26 |
+
DEEPSEEK_V4_STACKED_PARAMS_MAPPING,
|
| 27 |
+
AgnesDecoderLayer,
|
| 28 |
+
MqaAttentionBase,
|
| 29 |
+
_dequant_fp8_wo_a,
|
| 30 |
+
hc_head_torch,
|
| 31 |
+
make_hc_head_params,
|
| 32 |
+
)
|
| 33 |
+
from sglang.srt.models.dspark import (
|
| 34 |
+
DSparkConfidenceHead,
|
| 35 |
+
StepSampler,
|
| 36 |
+
gather_and_crop_vocab,
|
| 37 |
+
run_markov_block,
|
| 38 |
+
)
|
| 39 |
+
from sglang.srt.runtime_context import get_parallel
|
| 40 |
+
from sglang.srt.speculative.dspark_components.dspark_config import (
|
| 41 |
+
parse_dspark_draft_config,
|
| 42 |
+
)
|
| 43 |
+
from sglang.srt.speculative.dspark_components.kernels.dspark_draft_model import (
|
| 44 |
+
BuildStepLocal,
|
| 45 |
+
CommitKvProj,
|
| 46 |
+
)
|
| 47 |
+
from sglang.srt.speculative.ragged_verify import (
|
| 48 |
+
RaggedVerifyMode,
|
| 49 |
+
read_ragged_verify_mode,
|
| 50 |
+
)
|
| 51 |
+
from sglang.srt.utils import add_prefix, is_blackwell_supported
|
| 52 |
+
from sglang.srt.utils.async_probe import maybe_detect_in_closed_range
|
| 53 |
+
|
| 54 |
+
logger = logging.getLogger(__name__)
|
| 55 |
+
|
| 56 |
+
_PAD_NUM_HEADS = 64
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def apply_rotary_emb(
|
| 60 |
+
x: torch.Tensor, freqs_cis: torch.Tensor, inverse: bool = False
|
| 61 |
+
) -> torch.Tensor:
|
| 62 |
+
y = x
|
| 63 |
+
x = torch.view_as_complex(x.float().unflatten(-1, (-1, 2)))
|
| 64 |
+
if inverse:
|
| 65 |
+
freqs_cis = freqs_cis.conj()
|
| 66 |
+
if x.ndim == 3:
|
| 67 |
+
freqs_cis = freqs_cis.view(x.size(0), 1, x.size(-1))
|
| 68 |
+
else:
|
| 69 |
+
freqs_cis = freqs_cis.view(1, x.size(1), 1, x.size(-1))
|
| 70 |
+
x = torch.view_as_real(x * freqs_cis).flatten(-2)
|
| 71 |
+
y.copy_(x)
|
| 72 |
+
return y
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class DSparkAttention(MqaAttentionBase):
|
| 76 |
+
|
| 77 |
+
def __init__(
|
| 78 |
+
self,
|
| 79 |
+
config: AgnesConfig,
|
| 80 |
+
layer_id: int,
|
| 81 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 82 |
+
prefix: str = "",
|
| 83 |
+
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
| 84 |
+
) -> None:
|
| 85 |
+
super().__init__(
|
| 86 |
+
config,
|
| 87 |
+
layer_id,
|
| 88 |
+
quant_config,
|
| 89 |
+
prefix,
|
| 90 |
+
attn_tp_rank=get_parallel().attn_tp_rank,
|
| 91 |
+
attn_tp_size=get_parallel().attn_tp_size,
|
| 92 |
+
compress_ratio=0,
|
| 93 |
+
fuse_wqa_wkv=False,
|
| 94 |
+
wo_a_fp8=False,
|
| 95 |
+
wo_a_keeps_quant_config=False,
|
| 96 |
+
wo_b_reduce_results=True,
|
| 97 |
+
rope_original_seq_len=0,
|
| 98 |
+
)
|
| 99 |
+
assert (
|
| 100 |
+
self.compress_ratio == 0
|
| 101 |
+
), "DSpark draft attention requires compress_ratio == 0."
|
| 102 |
+
self.window_size = int(
|
| 103 |
+
getattr(config, "sliding_window", None) or config.window_size
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
self.attn = RadixAttention(
|
| 107 |
+
self.n_local_heads,
|
| 108 |
+
self.head_dim,
|
| 109 |
+
self.softmax_scale,
|
| 110 |
+
num_kv_heads=1,
|
| 111 |
+
layer_id=layer_id,
|
| 112 |
+
quant_config=quant_config,
|
| 113 |
+
prefix=add_prefix("attn", prefix),
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
self._use_fast_kernel = envs.SGLANG_DSPARK_FAST_KERNEL.get()
|
| 117 |
+
self.alt_streams = alt_streams
|
| 118 |
+
self._multi_stream_bs_limit = 128 if is_blackwell_supported() else 64
|
| 119 |
+
|
| 120 |
+
def kv_proj_only(self, x: torch.Tensor) -> torch.Tensor:
|
| 121 |
+
kv, _ = self.wkv(x)
|
| 122 |
+
return kv
|
| 123 |
+
|
| 124 |
+
def _local_attn_sink(self) -> torch.Tensor:
|
| 125 |
+
if self.attn_tp_size == 1:
|
| 126 |
+
return self.attn_sink
|
| 127 |
+
if self._attn_sink_local is None:
|
| 128 |
+
rank = self.attn_tp_rank
|
| 129 |
+
num_heads = self.n_local_heads
|
| 130 |
+
sink = self.attn_sink.new_zeros(max(num_heads, _PAD_NUM_HEADS))
|
| 131 |
+
sink[:num_heads] = self.attn_sink[rank * num_heads : (rank + 1) * num_heads]
|
| 132 |
+
self._attn_sink_local = sink
|
| 133 |
+
return self._attn_sink_local
|
| 134 |
+
|
| 135 |
+
def _store_block_kv(
|
| 136 |
+
self,
|
| 137 |
+
*,
|
| 138 |
+
kv: torch.Tensor,
|
| 139 |
+
positions: torch.Tensor,
|
| 140 |
+
forward_batch: ForwardBatch,
|
| 141 |
+
attn_backend,
|
| 142 |
+
pool: DeepSeekV4TokenToKVPool,
|
| 143 |
+
) -> None:
|
| 144 |
+
pool.set_swa_key_buffer_radix_fused_norm_rope(
|
| 145 |
+
layer_id=self.layer_id,
|
| 146 |
+
swa_loc=attn_backend.get_swa_out_cache_loc(forward_batch),
|
| 147 |
+
kv=kv,
|
| 148 |
+
kv_weight=self.kv_norm.weight.data,
|
| 149 |
+
eps=self.eps,
|
| 150 |
+
freqs_cis=self.freqs_cis,
|
| 151 |
+
positions=positions,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def _compute_q(
|
| 155 |
+
self,
|
| 156 |
+
x: torch.Tensor,
|
| 157 |
+
positions: torch.Tensor,
|
| 158 |
+
q_out: Optional[torch.Tensor] = None,
|
| 159 |
+
) -> torch.Tensor:
|
| 160 |
+
q, _ = self.wq_a(x)
|
| 161 |
+
q = self.q_norm(q)
|
| 162 |
+
q, _ = self.wq_b(q)
|
| 163 |
+
q = q.view(-1, self.n_local_heads, self.head_dim)
|
| 164 |
+
if self._use_fast_kernel:
|
| 165 |
+
if q_out is None:
|
| 166 |
+
q_out = torch.empty_like(q)
|
| 167 |
+
fused_q_norm_rope(q, q_out, self.eps, self.freqs_cis, positions)
|
| 168 |
+
return q_out
|
| 169 |
+
else:
|
| 170 |
+
q = q * torch.rsqrt(
|
| 171 |
+
q.float().square().mean(-1, keepdim=True) + self.eps
|
| 172 |
+
).to(q.dtype)
|
| 173 |
+
apply_rotary_emb(q[..., -self.rope_head_dim :], self.freqs_cis[positions])
|
| 174 |
+
if q_out is not None:
|
| 175 |
+
q_out.copy_(q)
|
| 176 |
+
return q_out
|
| 177 |
+
return q
|
| 178 |
+
|
| 179 |
+
def forward(
|
| 180 |
+
self,
|
| 181 |
+
positions: torch.Tensor,
|
| 182 |
+
hidden_states: torch.Tensor,
|
| 183 |
+
forward_batch: ForwardBatch,
|
| 184 |
+
) -> torch.Tensor:
|
| 185 |
+
from sglang.srt.model_executor.forward_context import get_attn_backend
|
| 186 |
+
|
| 187 |
+
pool = _resolve_dspark_pool()
|
| 188 |
+
attn_backend = get_attn_backend()
|
| 189 |
+
rd = self.rope_head_dim
|
| 190 |
+
|
| 191 |
+
enable_multi_stream = (
|
| 192 |
+
self.alt_streams is not None
|
| 193 |
+
and get_is_capture_mode()
|
| 194 |
+
and hidden_states.shape[0] <= self._multi_stream_bs_limit
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
q_padded: Optional[torch.Tensor] = None
|
| 198 |
+
q_out: Optional[torch.Tensor] = None
|
| 199 |
+
if self.n_local_heads < _PAD_NUM_HEADS:
|
| 200 |
+
q_padded = hidden_states.new_empty(
|
| 201 |
+
hidden_states.shape[0], _PAD_NUM_HEADS, self.head_dim
|
| 202 |
+
)
|
| 203 |
+
q_out = q_padded[:, : self.n_local_heads, :]
|
| 204 |
+
|
| 205 |
+
if enable_multi_stream:
|
| 206 |
+
current_stream = torch.cuda.current_stream()
|
| 207 |
+
stream_kv = self.alt_streams[0]
|
| 208 |
+
stream_kv.wait_stream(current_stream)
|
| 209 |
+
with torch.cuda.stream(stream_kv):
|
| 210 |
+
kv = self.kv_proj_only(hidden_states)
|
| 211 |
+
self._store_block_kv(
|
| 212 |
+
kv=kv,
|
| 213 |
+
positions=positions,
|
| 214 |
+
forward_batch=forward_batch,
|
| 215 |
+
attn_backend=attn_backend,
|
| 216 |
+
pool=pool,
|
| 217 |
+
)
|
| 218 |
+
q = self._compute_q(hidden_states, positions, q_out=q_out)
|
| 219 |
+
current_stream.wait_stream(stream_kv)
|
| 220 |
+
else:
|
| 221 |
+
kv = self.kv_proj_only(hidden_states)
|
| 222 |
+
self._store_block_kv(
|
| 223 |
+
kv=kv,
|
| 224 |
+
positions=positions,
|
| 225 |
+
forward_batch=forward_batch,
|
| 226 |
+
attn_backend=attn_backend,
|
| 227 |
+
pool=pool,
|
| 228 |
+
)
|
| 229 |
+
q = self._compute_q(hidden_states, positions, q_out=q_out)
|
| 230 |
+
|
| 231 |
+
if q_padded is not None:
|
| 232 |
+
q = q_padded
|
| 233 |
+
attn_sink = self._local_attn_sink()
|
| 234 |
+
|
| 235 |
+
o = attn_backend.forward(
|
| 236 |
+
q=q,
|
| 237 |
+
k=kv,
|
| 238 |
+
v=kv,
|
| 239 |
+
layer=self.attn,
|
| 240 |
+
forward_batch=forward_batch,
|
| 241 |
+
compress_ratio=0,
|
| 242 |
+
attn_sink=attn_sink,
|
| 243 |
+
save_kv_cache=False,
|
| 244 |
+
)
|
| 245 |
+
if o.shape[1] != self.n_local_heads:
|
| 246 |
+
o = o[:, : self.n_local_heads, :]
|
| 247 |
+
|
| 248 |
+
if self._use_fast_kernel:
|
| 249 |
+
fused_rope_inplace(
|
| 250 |
+
o[..., -rd:], None, self.freqs_cis, positions=positions, inverse=True
|
| 251 |
+
)
|
| 252 |
+
else:
|
| 253 |
+
apply_rotary_emb(o[..., -rd:], self.freqs_cis[positions], inverse=True)
|
| 254 |
+
|
| 255 |
+
o = o.view(
|
| 256 |
+
o.shape[0],
|
| 257 |
+
self.n_local_groups,
|
| 258 |
+
o.shape[1] * o.shape[2] // self.n_local_groups,
|
| 259 |
+
)
|
| 260 |
+
wo_a = self.wo_a.weight.view(self.n_local_groups, self.o_lora_rank, -1)
|
| 261 |
+
if self._use_fast_kernel:
|
| 262 |
+
o = torch.einsum("bgd,grd->bgr", o, wo_a)
|
| 263 |
+
else:
|
| 264 |
+
o = torch.einsum("bgd,grd->bgr", o.float(), wo_a.float()).to(q.dtype)
|
| 265 |
+
out, _ = self.wo_b(o.reshape(o.shape[0], o.shape[1] * o.shape[2]))
|
| 266 |
+
return out
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def _resolve_dspark_pool() -> DeepSeekV4TokenToKVPool:
|
| 270 |
+
pool = get_token_to_kv_pool()
|
| 271 |
+
assert isinstance(pool, DeepSeekV4TokenToKVPool), (
|
| 272 |
+
"DSpark draft attention requires a DeepSeekV4TokenToKVPool, "
|
| 273 |
+
f"got {type(pool).__name__}."
|
| 274 |
+
)
|
| 275 |
+
return pool
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class MarkovW2ShardGeometry(msgspec.Struct, frozen=True):
|
| 279 |
+
|
| 280 |
+
tp_size: int
|
| 281 |
+
org_vocab_start: int
|
| 282 |
+
org_vocab_end: int
|
| 283 |
+
num_embeddings_per_partition: int
|
| 284 |
+
num_embeddings_padded: int
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class DSparkV4MarkovHead(nn.Module):
|
| 288 |
+
|
| 289 |
+
markov_head_type = "vanilla"
|
| 290 |
+
|
| 291 |
+
def __init__(self, *, vocab_size: int, markov_rank: int) -> None:
|
| 292 |
+
super().__init__()
|
| 293 |
+
self.vocab_size = int(vocab_size)
|
| 294 |
+
self.markov_rank = int(markov_rank)
|
| 295 |
+
if self.markov_rank <= 0:
|
| 296 |
+
raise ValueError(
|
| 297 |
+
f"DSparkV4MarkovHead requires markov_rank > 0, got {self.markov_rank}."
|
| 298 |
+
)
|
| 299 |
+
self.markov_w1 = VocabParallelEmbedding(
|
| 300 |
+
self.vocab_size, self.markov_rank, enable_tp=False
|
| 301 |
+
)
|
| 302 |
+
self._opt_markov_w2_bf16 = envs.SGLANG_DSPARK_OPT_MARKOV_W2_BF16.get()
|
| 303 |
+
self._opt_markov_w2_tp_shard = envs.SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD.get()
|
| 304 |
+
markov_w2_dtype = torch.bfloat16 if self._opt_markov_w2_bf16 else torch.float32
|
| 305 |
+
self.markov_w2 = nn.Linear(
|
| 306 |
+
self.markov_rank, self.vocab_size, bias=False, dtype=markov_w2_dtype
|
| 307 |
+
)
|
| 308 |
+
self._tp_shard: Optional[MarkovW2ShardGeometry] = None
|
| 309 |
+
|
| 310 |
+
def configure_tp_shard(self, *, lm_head: nn.Module) -> None:
|
| 311 |
+
if not self._opt_markov_w2_tp_shard:
|
| 312 |
+
return
|
| 313 |
+
if int(lm_head.org_vocab_size) != self.vocab_size:
|
| 314 |
+
raise ValueError(
|
| 315 |
+
"DSpark markov_w2 TP-shard requires lm_head.org_vocab_size == "
|
| 316 |
+
f"markov vocab_size, got {int(lm_head.org_vocab_size)} vs "
|
| 317 |
+
f"{self.vocab_size}."
|
| 318 |
+
)
|
| 319 |
+
tp_size = int(lm_head.tp_size)
|
| 320 |
+
per_partition = int(lm_head.num_embeddings_per_partition)
|
| 321 |
+
num_padded = int(lm_head.num_embeddings_padded)
|
| 322 |
+
if per_partition * tp_size != num_padded:
|
| 323 |
+
raise ValueError(
|
| 324 |
+
"DSpark markov_w2 TP-shard could not align to the lm_head partition: "
|
| 325 |
+
f"num_embeddings_per_partition({per_partition}) * tp_size({tp_size}) != "
|
| 326 |
+
f"num_embeddings_padded({num_padded})."
|
| 327 |
+
)
|
| 328 |
+
attn_tp_size = get_parallel().attn_tp_group.world_size
|
| 329 |
+
if attn_tp_size != tp_size:
|
| 330 |
+
raise ValueError(
|
| 331 |
+
"DSpark markov_w2 TP-shard needs the attn-TP group (used for the per-step "
|
| 332 |
+
f"all-gather) to equal the lm_head shard group, got attn_tp_size="
|
| 333 |
+
f"{attn_tp_size} vs lm_head tp_size={tp_size}. This config (e.g. DP "
|
| 334 |
+
"attention without --enable-dp-lm-head, where lm_head shards over the "
|
| 335 |
+
"global TP group) is unsupported; disable "
|
| 336 |
+
"SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD."
|
| 337 |
+
)
|
| 338 |
+
self._tp_shard = MarkovW2ShardGeometry(
|
| 339 |
+
tp_size=tp_size,
|
| 340 |
+
org_vocab_start=int(lm_head.shard_indices.org_vocab_start_index),
|
| 341 |
+
org_vocab_end=int(lm_head.shard_indices.org_vocab_end_index),
|
| 342 |
+
num_embeddings_per_partition=per_partition,
|
| 343 |
+
num_embeddings_padded=num_padded,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
def get_prev_embeddings(self, token_ids: torch.Tensor) -> torch.Tensor:
|
| 347 |
+
return self.markov_w1(token_ids.long())
|
| 348 |
+
|
| 349 |
+
def project_bias(
|
| 350 |
+
self, latent_states: torch.Tensor, *, weight: Optional[torch.Tensor] = None
|
| 351 |
+
) -> torch.Tensor:
|
| 352 |
+
weight = self.markov_w2.weight if weight is None else weight
|
| 353 |
+
if self._opt_markov_w2_bf16:
|
| 354 |
+
return F.linear(latent_states.to(weight.dtype), weight).float()
|
| 355 |
+
return F.linear(latent_states.float(), weight)
|
| 356 |
+
|
| 357 |
+
def compute_step_bias(
|
| 358 |
+
self, token_ids: torch.Tensor, hidden_states: Optional[torch.Tensor]
|
| 359 |
+
) -> torch.Tensor:
|
| 360 |
+
del hidden_states
|
| 361 |
+
return self.project_bias(self.get_prev_embeddings(token_ids))
|
| 362 |
+
|
| 363 |
+
def apply_step_logits(
|
| 364 |
+
self,
|
| 365 |
+
logits: torch.Tensor,
|
| 366 |
+
*,
|
| 367 |
+
token_ids: torch.Tensor,
|
| 368 |
+
hidden_states: Optional[torch.Tensor],
|
| 369 |
+
) -> torch.Tensor:
|
| 370 |
+
if self._tp_shard is not None:
|
| 371 |
+
return self._apply_step_logits_sharded(
|
| 372 |
+
base_local=logits, token_ids=token_ids
|
| 373 |
+
)
|
| 374 |
+
return logits + self.compute_step_bias(token_ids, hidden_states)
|
| 375 |
+
|
| 376 |
+
def _apply_step_logits_sharded(
|
| 377 |
+
self, *, base_local: torch.Tensor, token_ids: torch.Tensor
|
| 378 |
+
) -> torch.Tensor:
|
| 379 |
+
shard = self._tp_shard
|
| 380 |
+
latent = self.get_prev_embeddings(token_ids)
|
| 381 |
+
weight_local = self.markov_w2.weight[
|
| 382 |
+
shard.org_vocab_start : shard.org_vocab_end
|
| 383 |
+
]
|
| 384 |
+
if self._opt_markov_w2_bf16:
|
| 385 |
+
bias = F.linear(latent.to(weight_local.dtype), weight_local)
|
| 386 |
+
else:
|
| 387 |
+
bias = F.linear(latent.float(), weight_local)
|
| 388 |
+
step_local = BuildStepLocal.execute(bias=bias, base_local=base_local)
|
| 389 |
+
if shard.tp_size > 1:
|
| 390 |
+
full = get_parallel().attn_tp_group.all_gather(step_local, dim=-1)
|
| 391 |
+
else:
|
| 392 |
+
full = step_local
|
| 393 |
+
return full[..., : self.vocab_size]
|
| 394 |
+
|
| 395 |
+
def forward(self, token_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 396 |
+
embed = self.get_prev_embeddings(token_ids)
|
| 397 |
+
logits = self.project_bias(embed)
|
| 398 |
+
return logits, embed
|
| 399 |
+
|
| 400 |
+
def sample_block(
|
| 401 |
+
self,
|
| 402 |
+
base_logits: torch.Tensor,
|
| 403 |
+
*,
|
| 404 |
+
first_prev_tokens: torch.Tensor,
|
| 405 |
+
hidden_states: Optional[torch.Tensor],
|
| 406 |
+
sampler: StepSampler,
|
| 407 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 408 |
+
return run_markov_block(
|
| 409 |
+
self,
|
| 410 |
+
base_logits,
|
| 411 |
+
first_prev_tokens=first_prev_tokens,
|
| 412 |
+
hidden_states=hidden_states,
|
| 413 |
+
sampler=sampler,
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def build_dspark_v4_confidence_head(
|
| 418 |
+
*, config: AgnesConfig, markov_rank: int
|
| 419 |
+
) -> Optional[DSparkConfidenceHead]:
|
| 420 |
+
if read_ragged_verify_mode() is RaggedVerifyMode.STATIC:
|
| 421 |
+
return None
|
| 422 |
+
if not hasattr(config, "enable_confidence_head"):
|
| 423 |
+
logger.warning(
|
| 424 |
+
"DSpark draft config has no enable_confidence_head field; treating the "
|
| 425 |
+
"confidence head as enabled."
|
| 426 |
+
)
|
| 427 |
+
with_markov_cfg = getattr(config, "confidence_head_with_markov", None)
|
| 428 |
+
with_markov = (
|
| 429 |
+
(markov_rank > 0) if with_markov_cfg is None else bool(with_markov_cfg)
|
| 430 |
+
)
|
| 431 |
+
if with_markov and markov_rank <= 0:
|
| 432 |
+
raise ValueError(
|
| 433 |
+
"DSpark V4 confidence_head_with_markov requires markov_rank > 0, "
|
| 434 |
+
f"got markov_rank={markov_rank}."
|
| 435 |
+
)
|
| 436 |
+
return DSparkConfidenceHead(
|
| 437 |
+
hidden_size=int(config.hidden_size),
|
| 438 |
+
markov_rank=int(markov_rank),
|
| 439 |
+
with_markov=with_markov,
|
| 440 |
+
bias=False,
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
class DSparkV4Stage(AgnesDecoderLayer):
|
| 445 |
+
|
| 446 |
+
def __init__(
|
| 447 |
+
self,
|
| 448 |
+
config: AgnesConfig,
|
| 449 |
+
layer_id: int,
|
| 450 |
+
stage_id: int,
|
| 451 |
+
num_stages: int,
|
| 452 |
+
num_target_layers: int,
|
| 453 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 454 |
+
prefix: str = "",
|
| 455 |
+
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
| 456 |
+
) -> None:
|
| 457 |
+
super().__init__(
|
| 458 |
+
config=config,
|
| 459 |
+
layer_id=layer_id,
|
| 460 |
+
quant_config=quant_config,
|
| 461 |
+
prefix=prefix,
|
| 462 |
+
is_nextn=True,
|
| 463 |
+
alt_streams=alt_streams,
|
| 464 |
+
)
|
| 465 |
+
self.stage_id = stage_id
|
| 466 |
+
self.dim = config.hidden_size
|
| 467 |
+
|
| 468 |
+
if stage_id == 0:
|
| 469 |
+
if num_target_layers <= 0:
|
| 470 |
+
raise ValueError(
|
| 471 |
+
"DSpark needs target layers for the target-hidden projection."
|
| 472 |
+
)
|
| 473 |
+
self.main_proj = ReplicatedLinear(
|
| 474 |
+
config.hidden_size * num_target_layers,
|
| 475 |
+
config.hidden_size,
|
| 476 |
+
bias=False,
|
| 477 |
+
quant_config=quant_config,
|
| 478 |
+
prefix=add_prefix("main_proj", prefix),
|
| 479 |
+
)
|
| 480 |
+
self.main_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 481 |
+
|
| 482 |
+
if stage_id == num_stages - 1:
|
| 483 |
+
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 484 |
+
(
|
| 485 |
+
self.hc_head_fn,
|
| 486 |
+
self.hc_head_base,
|
| 487 |
+
self.hc_head_scale,
|
| 488 |
+
) = make_hc_head_params(config.hc_mult, config.hidden_size)
|
| 489 |
+
|
| 490 |
+
def _build_self_attn(
|
| 491 |
+
self,
|
| 492 |
+
*,
|
| 493 |
+
config: AgnesConfig,
|
| 494 |
+
layer_id: int,
|
| 495 |
+
quant_config: Optional[QuantizationConfig],
|
| 496 |
+
prefix: str,
|
| 497 |
+
alt_streams: Optional[List[torch.cuda.Stream]],
|
| 498 |
+
compress_ratio_override: Optional[int],
|
| 499 |
+
) -> nn.Module:
|
| 500 |
+
del compress_ratio_override
|
| 501 |
+
return DSparkAttention(
|
| 502 |
+
config=config,
|
| 503 |
+
layer_id=layer_id,
|
| 504 |
+
quant_config=quant_config,
|
| 505 |
+
prefix=prefix,
|
| 506 |
+
alt_streams=alt_streams,
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
def _hc_pre_block(
|
| 510 |
+
self,
|
| 511 |
+
x: torch.Tensor,
|
| 512 |
+
hc_fn: torch.Tensor,
|
| 513 |
+
hc_scale: torch.Tensor,
|
| 514 |
+
hc_base: torch.Tensor,
|
| 515 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 516 |
+
y, post, comb, _ = self.hc_pre(x, hc_fn, hc_scale, hc_base)
|
| 517 |
+
return y, post, comb
|
| 518 |
+
|
| 519 |
+
def _hc_post_block(
|
| 520 |
+
self,
|
| 521 |
+
x: torch.Tensor,
|
| 522 |
+
residual: torch.Tensor,
|
| 523 |
+
post: torch.Tensor,
|
| 524 |
+
comb: torch.Tensor,
|
| 525 |
+
) -> torch.Tensor:
|
| 526 |
+
return self.hc_post(x, residual, post, comb)
|
| 527 |
+
|
| 528 |
+
def forward(
|
| 529 |
+
self,
|
| 530 |
+
positions: torch.Tensor,
|
| 531 |
+
hidden_states: torch.Tensor,
|
| 532 |
+
forward_batch: ForwardBatch,
|
| 533 |
+
) -> torch.Tensor:
|
| 534 |
+
residual = hidden_states
|
| 535 |
+
x, post, comb = self._hc_pre_block(
|
| 536 |
+
hidden_states, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
|
| 537 |
+
)
|
| 538 |
+
x = self.input_layernorm(x)
|
| 539 |
+
x = self.self_attn(positions, x, forward_batch)
|
| 540 |
+
x = self._hc_post_block(x, residual, post, comb)
|
| 541 |
+
|
| 542 |
+
residual = x
|
| 543 |
+
x, post, comb = self._hc_pre_block(
|
| 544 |
+
x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base
|
| 545 |
+
)
|
| 546 |
+
x = self.post_attention_layernorm(x)
|
| 547 |
+
x = self._run_ffn(x, forward_batch)
|
| 548 |
+
x = self._hc_post_block(x, residual, post, comb)
|
| 549 |
+
return x
|
| 550 |
+
|
| 551 |
+
def _run_ffn(self, x: torch.Tensor, forward_batch: ForwardBatch) -> torch.Tensor:
|
| 552 |
+
shape = x.shape
|
| 553 |
+
x = x.reshape(-1, self.dim)
|
| 554 |
+
y = self._run_moe_ffn_dp_sync(
|
| 555 |
+
x, forward_batch, input_ids=None, input_ids_global=None
|
| 556 |
+
)
|
| 557 |
+
return y.view(shape)
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
class AgnesForCausalLMDSpark(nn.Module):
|
| 561 |
+
|
| 562 |
+
def __init__(
|
| 563 |
+
self,
|
| 564 |
+
config: AgnesConfig,
|
| 565 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 566 |
+
prefix: str = "",
|
| 567 |
+
) -> None:
|
| 568 |
+
super().__init__()
|
| 569 |
+
self.config = config
|
| 570 |
+
self.quant_config = quant_config
|
| 571 |
+
|
| 572 |
+
dspark_config = parse_dspark_draft_config(draft_hf_config=config)
|
| 573 |
+
if not dspark_config.require_markov():
|
| 574 |
+
raise ValueError(
|
| 575 |
+
"DSpark V4 draft requires markov_rank > 0, "
|
| 576 |
+
f"got markov_rank={dspark_config.markov_rank}."
|
| 577 |
+
)
|
| 578 |
+
self.gamma = int(
|
| 579 |
+
dspark_config.resolve_gamma(default=int(config.num_hidden_layers))
|
| 580 |
+
)
|
| 581 |
+
self.block_size = self.gamma
|
| 582 |
+
if dspark_config.target_layer_ids is not None:
|
| 583 |
+
self.num_stages = len(dspark_config.target_layer_ids)
|
| 584 |
+
else:
|
| 585 |
+
self.num_stages = int(getattr(config, "num_nextn_predict_layers", 1) or 1)
|
| 586 |
+
|
| 587 |
+
target_num_layers = (
|
| 588 |
+
int(dspark_config.num_target_layers)
|
| 589 |
+
if dspark_config.num_target_layers is not None
|
| 590 |
+
else int(getattr(config, "num_hidden_layers", 1))
|
| 591 |
+
)
|
| 592 |
+
if dspark_config.target_layer_ids is not None:
|
| 593 |
+
self.num_target_features = len(dspark_config.target_layer_ids)
|
| 594 |
+
else:
|
| 595 |
+
self.num_target_features = target_num_layers
|
| 596 |
+
|
| 597 |
+
self.start_layer = 0
|
| 598 |
+
self.end_layer = self.num_stages
|
| 599 |
+
use_multi_stream = (
|
| 600 |
+
envs.SGLANG_OPT_USE_MULTI_STREAM_OVERLAP.get()
|
| 601 |
+
and envs.SGLANG_DSPARK_ENABLE_MULTI_STREAM.get()
|
| 602 |
+
and torch.cuda.is_available()
|
| 603 |
+
)
|
| 604 |
+
self.alt_streams: Optional[List[torch.cuda.Stream]] = (
|
| 605 |
+
[torch.cuda.Stream()] if use_multi_stream else None
|
| 606 |
+
)
|
| 607 |
+
self.stages = nn.ModuleList(
|
| 608 |
+
[
|
| 609 |
+
DSparkV4Stage(
|
| 610 |
+
config=config,
|
| 611 |
+
layer_id=stage_id,
|
| 612 |
+
stage_id=stage_id,
|
| 613 |
+
num_stages=self.num_stages,
|
| 614 |
+
num_target_layers=self.num_target_features,
|
| 615 |
+
quant_config=quant_config,
|
| 616 |
+
prefix=add_prefix(f"stages.{stage_id}", prefix),
|
| 617 |
+
alt_streams=self.alt_streams,
|
| 618 |
+
)
|
| 619 |
+
for stage_id in range(self.num_stages)
|
| 620 |
+
]
|
| 621 |
+
)
|
| 622 |
+
self.markov_head = DSparkV4MarkovHead(
|
| 623 |
+
vocab_size=int(config.vocab_size),
|
| 624 |
+
markov_rank=int(dspark_config.markov_rank),
|
| 625 |
+
)
|
| 626 |
+
self.confidence_head = build_dspark_v4_confidence_head(
|
| 627 |
+
config=config, markov_rank=int(dspark_config.markov_rank)
|
| 628 |
+
)
|
| 629 |
+
self.hc_mult = int(config.hc_mult)
|
| 630 |
+
self.norm_eps = float(config.rms_norm_eps)
|
| 631 |
+
self.hc_eps = float(config.hc_eps)
|
| 632 |
+
|
| 633 |
+
self.embed_tokens: Optional[nn.Module] = None
|
| 634 |
+
self.lm_head: Optional[nn.Module] = None
|
| 635 |
+
self._use_fp32_lm_head = envs.SGLANG_DSPARK_FP32_LM_HEAD.get()
|
| 636 |
+
self._opt_markov_w2_tp_shard = envs.SGLANG_DSPARK_OPT_MARKOV_W2_TP_SHARD.get()
|
| 637 |
+
|
| 638 |
+
@property
|
| 639 |
+
def enable_confidence_head(self) -> bool:
|
| 640 |
+
return self.confidence_head is not None
|
| 641 |
+
|
| 642 |
+
def attach_shared_modules(
|
| 643 |
+
self, *, embed_tokens: nn.Module, lm_head: nn.Module
|
| 644 |
+
) -> None:
|
| 645 |
+
self.embed_tokens = embed_tokens
|
| 646 |
+
self.lm_head = lm_head
|
| 647 |
+
self.markov_head.configure_tp_shard(lm_head=lm_head)
|
| 648 |
+
|
| 649 |
+
def project_target_hidden(self, main_hidden: torch.Tensor) -> torch.Tensor:
|
| 650 |
+
stage0 = self.stages[0]
|
| 651 |
+
projected, _ = stage0.main_proj(main_hidden)
|
| 652 |
+
return stage0.main_norm(projected)
|
| 653 |
+
|
| 654 |
+
def write_target_hidden_kv(
|
| 655 |
+
self,
|
| 656 |
+
*,
|
| 657 |
+
main_hidden: torch.Tensor,
|
| 658 |
+
swa_loc: torch.Tensor,
|
| 659 |
+
positions: torch.Tensor,
|
| 660 |
+
pool: DeepSeekV4TokenToKVPool,
|
| 661 |
+
) -> None:
|
| 662 |
+
main_x = self.project_target_hidden(main_hidden)
|
| 663 |
+
swa_loc = swa_loc.to(torch.int32)
|
| 664 |
+
kvs = CommitKvProj.execute(
|
| 665 |
+
main_x=main_x,
|
| 666 |
+
wkv_linears=[stage.self_attn.wkv for stage in self.stages],
|
| 667 |
+
)
|
| 668 |
+
for stage, kv in zip(self.stages, kvs):
|
| 669 |
+
attn = stage.self_attn
|
| 670 |
+
pool.set_swa_key_buffer_radix_fused_norm_rope(
|
| 671 |
+
layer_id=attn.layer_id,
|
| 672 |
+
swa_loc=swa_loc,
|
| 673 |
+
kv=kv,
|
| 674 |
+
kv_weight=attn.kv_norm.weight.data,
|
| 675 |
+
eps=attn.eps,
|
| 676 |
+
freqs_cis=attn.freqs_cis,
|
| 677 |
+
positions=positions,
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
def forward_embed(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 681 |
+
if self.embed_tokens is None:
|
| 682 |
+
raise ValueError(
|
| 683 |
+
"AgnesForCausalLMDSpark requires the target embed_tokens "
|
| 684 |
+
"(call attach_shared_modules first)."
|
| 685 |
+
)
|
| 686 |
+
x = self.embed_tokens(input_ids)
|
| 687 |
+
x = x.unsqueeze(1).repeat(1, self.hc_mult, 1)
|
| 688 |
+
return x
|
| 689 |
+
|
| 690 |
+
def forward(
|
| 691 |
+
self,
|
| 692 |
+
input_ids: torch.Tensor,
|
| 693 |
+
positions: torch.Tensor,
|
| 694 |
+
forward_batch: ForwardBatch,
|
| 695 |
+
input_embeds: Optional[torch.Tensor] = None,
|
| 696 |
+
get_embedding: bool = False,
|
| 697 |
+
pp_proxy_tensors=None,
|
| 698 |
+
) -> LogitsProcessorOutput:
|
| 699 |
+
del get_embedding, pp_proxy_tensors
|
| 700 |
+
if input_embeds is None:
|
| 701 |
+
input_embeds = self.forward_embed(input_ids)
|
| 702 |
+
x = input_embeds
|
| 703 |
+
for stage in self.stages:
|
| 704 |
+
x = stage(positions, x, forward_batch)
|
| 705 |
+
|
| 706 |
+
return LogitsProcessorOutput(next_token_logits=None, hidden_states=x)
|
| 707 |
+
|
| 708 |
+
def collapse_hc_head(self, x: torch.Tensor) -> torch.Tensor:
|
| 709 |
+
last = self.stages[-1]
|
| 710 |
+
return hc_head_torch(
|
| 711 |
+
x,
|
| 712 |
+
last.hc_head_fn,
|
| 713 |
+
last.hc_head_scale,
|
| 714 |
+
last.hc_head_base,
|
| 715 |
+
norm_eps=self.norm_eps,
|
| 716 |
+
hc_eps=self.hc_eps,
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
def compute_base_logits(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 720 |
+
|
| 721 |
+
x_post_hc = self.collapse_hc_head(x)
|
| 722 |
+
return self._logits_from_x_post_hc(x_post_hc), x_post_hc
|
| 723 |
+
|
| 724 |
+
def _logits_from_x_post_hc(self, x_post_hc: torch.Tensor) -> torch.Tensor:
|
| 725 |
+
if self.lm_head is None:
|
| 726 |
+
raise ValueError(
|
| 727 |
+
"AgnesForCausalLMDSpark requires the target lm_head "
|
| 728 |
+
"(call attach_shared_modules first)."
|
| 729 |
+
)
|
| 730 |
+
last = self.stages[-1]
|
| 731 |
+
x = last.norm(x_post_hc)
|
| 732 |
+
weight = self.lm_head.weight
|
| 733 |
+
if self._use_fp32_lm_head:
|
| 734 |
+
local_logits = F.linear(x.float(), weight.float())
|
| 735 |
+
else:
|
| 736 |
+
local_logits = torch.matmul(x.to(weight.dtype), weight.T)
|
| 737 |
+
if self._opt_markov_w2_tp_shard:
|
| 738 |
+
return local_logits
|
| 739 |
+
return gather_and_crop_vocab(local_logits, self.lm_head)
|
| 740 |
+
|
| 741 |
+
def compute_confidence(
|
| 742 |
+
self,
|
| 743 |
+
*,
|
| 744 |
+
anchor_tokens: torch.Tensor,
|
| 745 |
+
sampled_tokens: torch.Tensor,
|
| 746 |
+
x_post_hc: torch.Tensor,
|
| 747 |
+
) -> Optional[torch.Tensor]:
|
| 748 |
+
confidence_head = self.confidence_head
|
| 749 |
+
if confidence_head is None:
|
| 750 |
+
return None
|
| 751 |
+
bs = int(anchor_tokens.shape[0])
|
| 752 |
+
x_post_hc = x_post_hc.view(bs, self.gamma, -1)
|
| 753 |
+
if confidence_head.with_markov:
|
| 754 |
+
prev_seq = torch.cat(
|
| 755 |
+
[anchor_tokens.view(-1, 1), sampled_tokens[:, : self.gamma - 1]], dim=1
|
| 756 |
+
)
|
| 757 |
+
markov_embed_stack = self.markov_head.get_prev_embeddings(prev_seq)
|
| 758 |
+
else:
|
| 759 |
+
markov_embed_stack = None
|
| 760 |
+
confidence_raw = confidence_head(x_post_hc, markov_embed_stack)
|
| 761 |
+
confidence = confidence_head.apply_sts(confidence_raw)
|
| 762 |
+
maybe_detect_in_closed_range(
|
| 763 |
+
confidence, 0.0, 1.0, "DSpark confidence must lie in [0, 1]."
|
| 764 |
+
)
|
| 765 |
+
return confidence
|
| 766 |
+
|
| 767 |
+
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]) -> None:
|
| 768 |
+
params_dict = dict(self.named_parameters())
|
| 769 |
+
loaded_params = set()
|
| 770 |
+
|
| 771 |
+
weights = list(weights)
|
| 772 |
+
if any(name.endswith(".wo_a.scale") for name, _ in weights):
|
| 773 |
+
weights = list(_dequant_fp8_wo_a(weights))
|
| 774 |
+
|
| 775 |
+
stacked_params_mapping = DEEPSEEK_V4_STACKED_PARAMS_MAPPING
|
| 776 |
+
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
|
| 777 |
+
|
| 778 |
+
expert_params_mapping = FusedMoE.make_expert_params_mapping(
|
| 779 |
+
ckpt_gate_proj_name="gate_proj",
|
| 780 |
+
ckpt_down_proj_name="down_proj",
|
| 781 |
+
ckpt_up_proj_name="up_proj",
|
| 782 |
+
num_experts=self.config.n_routed_experts,
|
| 783 |
+
)
|
| 784 |
+
|
| 785 |
+
for name, loaded_weight in weights:
|
| 786 |
+
mapped = self._remap_dspark_weight_name(name)
|
| 787 |
+
if mapped is None:
|
| 788 |
+
continue
|
| 789 |
+
|
| 790 |
+
for param_name, weight_name, shard_id in stacked_params_mapping:
|
| 791 |
+
if weight_name not in mapped:
|
| 792 |
+
continue
|
| 793 |
+
candidate = mapped.replace(weight_name, param_name)
|
| 794 |
+
if candidate not in params_dict:
|
| 795 |
+
continue
|
| 796 |
+
param = params_dict[candidate]
|
| 797 |
+
weight_loader = param.weight_loader
|
| 798 |
+
weight_loader(param, loaded_weight, shard_id)
|
| 799 |
+
loaded_params.add(candidate)
|
| 800 |
+
break
|
| 801 |
+
else:
|
| 802 |
+
for (
|
| 803 |
+
param_name,
|
| 804 |
+
weight_name,
|
| 805 |
+
expert_id,
|
| 806 |
+
shard_id,
|
| 807 |
+
) in expert_params_mapping:
|
| 808 |
+
if weight_name not in mapped:
|
| 809 |
+
continue
|
| 810 |
+
candidate = mapped.replace(weight_name, param_name)
|
| 811 |
+
if candidate not in params_dict:
|
| 812 |
+
continue
|
| 813 |
+
param = params_dict[candidate]
|
| 814 |
+
weight_loader = param.weight_loader
|
| 815 |
+
weight_loader(
|
| 816 |
+
param,
|
| 817 |
+
loaded_weight,
|
| 818 |
+
candidate,
|
| 819 |
+
shard_id=shard_id,
|
| 820 |
+
expert_id=expert_id,
|
| 821 |
+
)
|
| 822 |
+
loaded_params.add(candidate)
|
| 823 |
+
break
|
| 824 |
+
else:
|
| 825 |
+
if mapped not in params_dict:
|
| 826 |
+
logger.warning(
|
| 827 |
+
"DSpark V4 draft: unexpected weight %r -> %r", name, mapped
|
| 828 |
+
)
|
| 829 |
+
continue
|
| 830 |
+
param = params_dict[mapped]
|
| 831 |
+
weight_loader = getattr(
|
| 832 |
+
param, "weight_loader", default_weight_loader
|
| 833 |
+
)
|
| 834 |
+
weight_loader(param, loaded_weight)
|
| 835 |
+
loaded_params.add(mapped)
|
| 836 |
+
|
| 837 |
+
self._assert_confidence_head_loaded(
|
| 838 |
+
params_dict=params_dict, loaded_params=loaded_params
|
| 839 |
+
)
|
| 840 |
+
|
| 841 |
+
def _assert_confidence_head_loaded(
|
| 842 |
+
self, *, params_dict: dict, loaded_params: set
|
| 843 |
+
) -> None:
|
| 844 |
+
if self.confidence_head is None:
|
| 845 |
+
return
|
| 846 |
+
confidence_param_names = {
|
| 847 |
+
name for name in params_dict if name.startswith("confidence_head.")
|
| 848 |
+
}
|
| 849 |
+
missing = confidence_param_names - loaded_params
|
| 850 |
+
if missing:
|
| 851 |
+
raise ValueError(
|
| 852 |
+
f"DSpark V4 confidence head is enabled but the checkpoint is missing "
|
| 853 |
+
f"{sorted(missing)}. Provide a checkpoint with trained confidence weights, "
|
| 854 |
+
f"or disable the confidence head (enable_confidence_head=False)."
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
def _remap_dspark_weight_name(self, name: str) -> Optional[str]:
|
| 858 |
+
if name.startswith(("embed.", "embed_tokens.", "head.", "lm_head.")):
|
| 859 |
+
return None
|
| 860 |
+
if "rotary_emb.inv_freq" in name:
|
| 861 |
+
return None
|
| 862 |
+
|
| 863 |
+
if not name.startswith("mtp."):
|
| 864 |
+
return None
|
| 865 |
+
parts = name.split(".", 2)
|
| 866 |
+
if len(parts) < 3:
|
| 867 |
+
return None
|
| 868 |
+
stage_id, rest = parts[1], parts[2]
|
| 869 |
+
|
| 870 |
+
if rest.startswith("markov_head."):
|
| 871 |
+
return f"markov_head.{rest[len('markov_head.'):]}"
|
| 872 |
+
|
| 873 |
+
if rest.startswith("confidence_head."):
|
| 874 |
+
if self.confidence_head is None:
|
| 875 |
+
return None
|
| 876 |
+
return f"confidence_head.{rest[len('confidence_head.'):]}"
|
| 877 |
+
|
| 878 |
+
mapped_rest = rest
|
| 879 |
+
mapped_rest = mapped_rest.replace("attn.", "self_attn.", 1)
|
| 880 |
+
mapped_rest = mapped_rest.replace("ffn.", "mlp.", 1)
|
| 881 |
+
mapped_rest = mapped_rest.replace("attn_norm.", "input_layernorm.", 1)
|
| 882 |
+
mapped_rest = mapped_rest.replace("ffn_norm.", "post_attention_layernorm.", 1)
|
| 883 |
+
mapped_rest = mapped_rest.replace(".w1.", ".gate_proj.")
|
| 884 |
+
mapped_rest = mapped_rest.replace(".w2.", ".down_proj.")
|
| 885 |
+
mapped_rest = mapped_rest.replace(".w3.", ".up_proj.")
|
| 886 |
+
mapped_rest = mapped_rest.replace(".gate.tid2eid", ".topk.tid2eid")
|
| 887 |
+
mapped_rest = mapped_rest.replace(".gate.bias", ".gate.e_score_correction_bias")
|
| 888 |
+
mapped_rest = mapped_rest.replace(".scale", ".weight_scale_inv")
|
| 889 |
+
return f"stages.{stage_id}.{mapped_rest}"
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
EntryClass = [AgnesForCausalLMDSpark]
|
sglang_patch/srt/models/agnes_nextn.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Iterable, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from torch import nn
|
| 7 |
+
from transformers import PretrainedConfig
|
| 8 |
+
|
| 9 |
+
from sglang.srt.distributed import get_pp_group
|
| 10 |
+
from sglang.srt.layers.attention.dsa.utils import (
|
| 11 |
+
can_dsa_cp_split,
|
| 12 |
+
dsa_use_prefill_cp,
|
| 13 |
+
is_dsa_enable_prefill_cp,
|
| 14 |
+
is_dsa_prefill_cp_round_robin_split,
|
| 15 |
+
)
|
| 16 |
+
from sglang.srt.layers.dp_attention import (
|
| 17 |
+
dp_gather_partial,
|
| 18 |
+
get_global_dp_buffer_len,
|
| 19 |
+
is_dp_attention_enabled,
|
| 20 |
+
)
|
| 21 |
+
from sglang.srt.layers.layernorm import RMSNorm
|
| 22 |
+
from sglang.srt.layers.linear import ReplicatedLinear
|
| 23 |
+
from sglang.srt.layers.logits_processor import LogitsProcessor
|
| 24 |
+
from sglang.srt.layers.moe.utils import get_moe_a2a_backend
|
| 25 |
+
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
| 26 |
+
from sglang.srt.layers.quantization.modelslim.modelslim import ModelSlimConfig
|
| 27 |
+
from sglang.srt.layers.utils.cp_utils import (
|
| 28 |
+
cp_all_gather_rerange_output,
|
| 29 |
+
cp_round_robin_input_ids,
|
| 30 |
+
cp_split_and_rebuild_data,
|
| 31 |
+
cp_split_and_rebuild_position,
|
| 32 |
+
prepare_context_parallel_metadata,
|
| 33 |
+
)
|
| 34 |
+
from sglang.srt.layers.vocab_parallel_embedding import (
|
| 35 |
+
ParallelLMHead,
|
| 36 |
+
VocabParallelEmbedding,
|
| 37 |
+
)
|
| 38 |
+
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
| 39 |
+
from sglang.srt.model_executor.forward_context import get_attn_backend
|
| 40 |
+
from sglang.srt.models.agnes import AgnesDecoderLayer, AgnesForCausalLM
|
| 41 |
+
from sglang.srt.runtime_context import get_parallel, get_server_args
|
| 42 |
+
from sglang.srt.utils import add_prefix
|
| 43 |
+
|
| 44 |
+
logger = logging.getLogger(__name__)
|
| 45 |
+
|
| 46 |
+
COMPRESS_RATIO_NEXTN_LAYER = 0
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class AgnesModelNextN(nn.Module):
|
| 50 |
+
def __init__(
|
| 51 |
+
self,
|
| 52 |
+
config: PretrainedConfig,
|
| 53 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 54 |
+
prefix: str = "",
|
| 55 |
+
) -> None:
|
| 56 |
+
super().__init__()
|
| 57 |
+
self.config = config
|
| 58 |
+
self.vocab_size = config.vocab_size
|
| 59 |
+
|
| 60 |
+
self.embed_tokens = VocabParallelEmbedding(
|
| 61 |
+
config.vocab_size,
|
| 62 |
+
config.hidden_size,
|
| 63 |
+
enable_tp=not is_dp_attention_enabled(),
|
| 64 |
+
prefix=add_prefix("embed_tokens", prefix),
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
self.enorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 68 |
+
self.hnorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 69 |
+
self.rms_norm_eps = config.rms_norm_eps
|
| 70 |
+
|
| 71 |
+
self.hc_eps = config.hc_eps
|
| 72 |
+
self.hc_mult = hc_mult = config.hc_mult
|
| 73 |
+
hc_dim = hc_mult * config.hidden_size
|
| 74 |
+
self.hc_head_fn = nn.Parameter(
|
| 75 |
+
torch.empty(hc_mult, hc_dim, dtype=torch.float32)
|
| 76 |
+
)
|
| 77 |
+
self.hc_head_base = nn.Parameter(torch.empty(hc_mult, dtype=torch.float32))
|
| 78 |
+
self.hc_head_scale = nn.Parameter(torch.empty(1, dtype=torch.float32))
|
| 79 |
+
|
| 80 |
+
self.e_proj = ReplicatedLinear(
|
| 81 |
+
config.hidden_size,
|
| 82 |
+
config.hidden_size,
|
| 83 |
+
bias=False,
|
| 84 |
+
quant_config=quant_config,
|
| 85 |
+
prefix=add_prefix("e_proj", prefix),
|
| 86 |
+
)
|
| 87 |
+
self.h_proj = ReplicatedLinear(
|
| 88 |
+
config.hidden_size,
|
| 89 |
+
config.hidden_size,
|
| 90 |
+
bias=False,
|
| 91 |
+
quant_config=quant_config,
|
| 92 |
+
prefix=add_prefix("h_proj", prefix),
|
| 93 |
+
)
|
| 94 |
+
if isinstance(quant_config, ModelSlimConfig):
|
| 95 |
+
prefix = "mtp.0"
|
| 96 |
+
else:
|
| 97 |
+
prefix = add_prefix("decoder", prefix)
|
| 98 |
+
|
| 99 |
+
self.decoder = AgnesDecoderLayer(
|
| 100 |
+
config,
|
| 101 |
+
layer_id=0,
|
| 102 |
+
quant_config=quant_config,
|
| 103 |
+
is_nextn=True,
|
| 104 |
+
prefix=prefix,
|
| 105 |
+
alt_streams=None,
|
| 106 |
+
compress_ratio_override=COMPRESS_RATIO_NEXTN_LAYER,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
|
| 110 |
+
if self.dsa_enable_prefill_cp:
|
| 111 |
+
self.cp_size = get_parallel().attn_cp_size
|
| 112 |
+
else:
|
| 113 |
+
self.cp_size = None
|
| 114 |
+
|
| 115 |
+
self.shared_head = nn.Module()
|
| 116 |
+
self.shared_head.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 117 |
+
|
| 118 |
+
def hc_head(
|
| 119 |
+
self,
|
| 120 |
+
x: torch.Tensor,
|
| 121 |
+
hc_fn: torch.Tensor,
|
| 122 |
+
hc_scale: torch.Tensor,
|
| 123 |
+
hc_base: torch.Tensor,
|
| 124 |
+
):
|
| 125 |
+
shape, dtype = x.size(), x.dtype
|
| 126 |
+
x = x.flatten(1).float()
|
| 127 |
+
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.rms_norm_eps)
|
| 128 |
+
mixes = F.linear(x, hc_fn) * rsqrt
|
| 129 |
+
pre = torch.sigmoid(mixes * hc_scale + hc_base) + self.hc_eps
|
| 130 |
+
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=1)
|
| 131 |
+
return y.to(dtype)
|
| 132 |
+
|
| 133 |
+
def forward(
|
| 134 |
+
self,
|
| 135 |
+
input_ids: torch.Tensor,
|
| 136 |
+
positions: torch.Tensor,
|
| 137 |
+
forward_batch: ForwardBatch,
|
| 138 |
+
input_embeds: torch.Tensor = None,
|
| 139 |
+
) -> torch.Tensor:
|
| 140 |
+
if input_embeds is None:
|
| 141 |
+
hidden_states = self.embed_tokens(input_ids)
|
| 142 |
+
else:
|
| 143 |
+
hidden_states = input_embeds
|
| 144 |
+
|
| 145 |
+
if hidden_states.shape[0] > 0:
|
| 146 |
+
n_tokens = hidden_states.shape[0]
|
| 147 |
+
d = self.config.hidden_size
|
| 148 |
+
hc_flat = forward_batch.spec_info.hidden_states.view(
|
| 149 |
+
n_tokens * self.hc_mult, d
|
| 150 |
+
)
|
| 151 |
+
h_proj_out, _ = self.h_proj(self.hnorm(hc_flat))
|
| 152 |
+
h_proj_hidden_states = h_proj_out.view(n_tokens, self.hc_mult, d)
|
| 153 |
+
|
| 154 |
+
e_proj_hidden_states, _ = self.e_proj(self.enorm(hidden_states))
|
| 155 |
+
hidden_states = e_proj_hidden_states[:, None, :] + h_proj_hidden_states
|
| 156 |
+
else:
|
| 157 |
+
hidden_states = hidden_states.unsqueeze(1).repeat(1, self.hc_mult, 1)
|
| 158 |
+
|
| 159 |
+
if get_parallel().attn_dp_size > 1 and get_moe_a2a_backend().is_none():
|
| 160 |
+
input_ids_global = torch.empty(
|
| 161 |
+
(get_global_dp_buffer_len(), 1),
|
| 162 |
+
dtype=input_ids.dtype,
|
| 163 |
+
device=input_ids.device,
|
| 164 |
+
)
|
| 165 |
+
dp_gather_partial(input_ids_global, input_ids[:, None], forward_batch)
|
| 166 |
+
input_ids_global = input_ids_global.squeeze(-1)
|
| 167 |
+
else:
|
| 168 |
+
input_ids_global = input_ids
|
| 169 |
+
|
| 170 |
+
if dsa_use_prefill_cp(forward_batch):
|
| 171 |
+
hidden_states = cp_split_and_rebuild_data(forward_batch, hidden_states)
|
| 172 |
+
positions = cp_split_and_rebuild_position(forward_batch, positions)
|
| 173 |
+
input_ids = cp_round_robin_input_ids(input_ids)
|
| 174 |
+
input_ids_global = input_ids
|
| 175 |
+
|
| 176 |
+
hidden_states, residual, post, comb = self.decoder(
|
| 177 |
+
positions=positions,
|
| 178 |
+
hidden_states=hidden_states,
|
| 179 |
+
forward_batch=forward_batch,
|
| 180 |
+
input_ids=input_ids,
|
| 181 |
+
input_ids_global=input_ids_global,
|
| 182 |
+
)
|
| 183 |
+
if residual is not None:
|
| 184 |
+
# NextN has a single decoder layer, so no later layer can consume a
|
| 185 |
+
# deferred fused hc_post state.
|
| 186 |
+
hidden_states = self.decoder.hc_post(hidden_states, residual, post, comb)
|
| 187 |
+
|
| 188 |
+
if dsa_use_prefill_cp(forward_batch):
|
| 189 |
+
hidden_states = cp_all_gather_rerange_output(
|
| 190 |
+
hidden_states,
|
| 191 |
+
self.cp_size,
|
| 192 |
+
forward_batch,
|
| 193 |
+
torch.cuda.current_stream(),
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
pre_hc_head = hidden_states.flatten(1)
|
| 197 |
+
|
| 198 |
+
hidden_states = self.hc_head(
|
| 199 |
+
hidden_states, self.hc_head_fn, self.hc_head_scale, self.hc_head_base
|
| 200 |
+
)
|
| 201 |
+
hidden_states = self.shared_head.norm(hidden_states)
|
| 202 |
+
|
| 203 |
+
return hidden_states, pre_hc_head
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class AgnesForCausalLMNextN(AgnesForCausalLM):
|
| 207 |
+
|
| 208 |
+
def __init__(
|
| 209 |
+
self,
|
| 210 |
+
config: PretrainedConfig,
|
| 211 |
+
quant_config: Optional[QuantizationConfig] = None,
|
| 212 |
+
prefix: str = "",
|
| 213 |
+
) -> None:
|
| 214 |
+
nn.Module.__init__(self)
|
| 215 |
+
self.config = config
|
| 216 |
+
self.tp_size = get_parallel().tp_size
|
| 217 |
+
self.pp_group = get_pp_group()
|
| 218 |
+
self.quant_config = quant_config
|
| 219 |
+
self.determine_num_fused_shared_experts()
|
| 220 |
+
self.dsa_enable_prefill_cp = is_dsa_enable_prefill_cp()
|
| 221 |
+
if self.dsa_enable_prefill_cp:
|
| 222 |
+
self.cp_rank = get_parallel().attn_cp_rank
|
| 223 |
+
self.cp_size = get_parallel().attn_cp_size
|
| 224 |
+
else:
|
| 225 |
+
self.cp_rank = None
|
| 226 |
+
self.cp_size = None
|
| 227 |
+
|
| 228 |
+
self.model = AgnesModelNextN(
|
| 229 |
+
config, quant_config, prefix=add_prefix("model", prefix)
|
| 230 |
+
)
|
| 231 |
+
self.lm_head = ParallelLMHead(
|
| 232 |
+
config.vocab_size,
|
| 233 |
+
config.hidden_size,
|
| 234 |
+
quant_config=quant_config,
|
| 235 |
+
prefix=add_prefix("model.shared_head.head", prefix),
|
| 236 |
+
use_attn_tp_group=get_server_args().enable_dp_lm_head,
|
| 237 |
+
)
|
| 238 |
+
self.logits_processor = LogitsProcessor(config)
|
| 239 |
+
|
| 240 |
+
@torch.no_grad()
|
| 241 |
+
def forward(
|
| 242 |
+
self,
|
| 243 |
+
input_ids: torch.Tensor,
|
| 244 |
+
positions: torch.Tensor,
|
| 245 |
+
forward_batch: ForwardBatch,
|
| 246 |
+
) -> torch.Tensor:
|
| 247 |
+
if self.dsa_enable_prefill_cp:
|
| 248 |
+
if can_dsa_cp_split(len(input_ids), self.cp_size, True, forward_batch):
|
| 249 |
+
forward_batch.attn_cp_metadata = prepare_context_parallel_metadata(
|
| 250 |
+
len(input_ids),
|
| 251 |
+
self.cp_rank,
|
| 252 |
+
self.cp_size,
|
| 253 |
+
forward_batch.seq_lens_cpu.tolist(),
|
| 254 |
+
extend_seqs_len=forward_batch.extend_seq_lens_cpu,
|
| 255 |
+
)
|
| 256 |
+
if is_dsa_prefill_cp_round_robin_split():
|
| 257 |
+
attn_backend = get_attn_backend()
|
| 258 |
+
metadata = attn_backend.forward_metadata
|
| 259 |
+
core_meta = metadata.core_attn_metadata
|
| 260 |
+
core_meta.apply_cp_reindex()
|
| 261 |
+
core_meta.init_flashmla_related(is_prefill=True)
|
| 262 |
+
if metadata.indexer_metadata is not None:
|
| 263 |
+
metadata.indexer_metadata = (
|
| 264 |
+
attn_backend.init_forward_metadata_indexer(core_meta)
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
hidden_states, pre_hc_head = self.model(input_ids, positions, forward_batch)
|
| 268 |
+
return self.logits_processor(
|
| 269 |
+
input_ids,
|
| 270 |
+
hidden_states,
|
| 271 |
+
self.lm_head,
|
| 272 |
+
forward_batch,
|
| 273 |
+
hidden_states_before_norm=pre_hc_head,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
| 277 |
+
super().load_weights(weights, is_nextn=True)
|
| 278 |
+
|
| 279 |
+
def post_load_weights(self, is_nextn=False, weight_names=None):
|
| 280 |
+
super().post_load_weights(is_nextn=True, weight_names=weight_names)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
EntryClass = [AgnesForCausalLMNextN]
|
sglang_patch/srt/models/deepseek_v2.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
sglang_patch/srt/parser/reasoning_parser.py
ADDED
|
@@ -0,0 +1,1495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
import re
|
| 3 |
+
from typing import Dict, List, Optional, Tuple, Type
|
| 4 |
+
|
| 5 |
+
from sglang.srt.entrypoints.openai.protocol import ChatCompletionRequest
|
| 6 |
+
from sglang.srt.function_call.hunyuan_detector import resolve_hunyuan_tokens
|
| 7 |
+
from sglang.srt.parser.harmony_parser import HarmonyParser
|
| 8 |
+
from sglang.srt.parser.inkling_tokenizer import (
|
| 9 |
+
CONTENT_INVOKE_TOOL_JSON,
|
| 10 |
+
CONTENT_MODEL_END_SAMPLING,
|
| 11 |
+
CONTENT_TEXT,
|
| 12 |
+
CONTENT_THINKING,
|
| 13 |
+
END_MESSAGE,
|
| 14 |
+
INKLING_CONTROL_TOKENS,
|
| 15 |
+
MESSAGE_MODEL,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class StreamingParseResult:
|
| 20 |
+
"""Result of streaming incremental parsing."""
|
| 21 |
+
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
normal_text: Optional[str] = None,
|
| 25 |
+
reasoning_text: Optional[str] = None,
|
| 26 |
+
):
|
| 27 |
+
self.normal_text = normal_text or ""
|
| 28 |
+
self.reasoning_text = reasoning_text or ""
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class BaseReasoningFormatDetector:
|
| 32 |
+
"""Base class providing two sets of interfaces: one-time and streaming incremental."""
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
think_start_token: str,
|
| 37 |
+
think_end_token: str,
|
| 38 |
+
think_excluded_tokens: Optional[List[str]] = None,
|
| 39 |
+
force_reasoning: bool = False,
|
| 40 |
+
stream_reasoning: bool = True,
|
| 41 |
+
tool_start_token: Optional[str] = None,
|
| 42 |
+
continue_final_message: bool = False,
|
| 43 |
+
previous_content: str = "",
|
| 44 |
+
thinks_internally: bool = False,
|
| 45 |
+
reasoning_default: str = "always",
|
| 46 |
+
force_nonempty_content: bool = False,
|
| 47 |
+
):
|
| 48 |
+
self.think_start_token = think_start_token
|
| 49 |
+
self.think_end_token = think_end_token
|
| 50 |
+
self.think_excluded_tokens = think_excluded_tokens
|
| 51 |
+
self.tool_start_token = tool_start_token
|
| 52 |
+
self.force_reasoning = force_reasoning
|
| 53 |
+
self._in_reasoning = force_reasoning
|
| 54 |
+
self.stream_reasoning = stream_reasoning
|
| 55 |
+
self.thinks_internally = thinks_internally
|
| 56 |
+
self.reasoning_default = reasoning_default
|
| 57 |
+
|
| 58 |
+
self._buffer = ""
|
| 59 |
+
self.stripped_think_start = False
|
| 60 |
+
self.think_start_self_label = ""
|
| 61 |
+
|
| 62 |
+
self._force_nonempty_content = force_nonempty_content
|
| 63 |
+
self._accumulated_reasoning = ""
|
| 64 |
+
|
| 65 |
+
self.continue_final_message = continue_final_message
|
| 66 |
+
if self.continue_final_message:
|
| 67 |
+
self.previous_content = previous_content
|
| 68 |
+
self.previous_count = len(previous_content)
|
| 69 |
+
else:
|
| 70 |
+
self.previous_content = ""
|
| 71 |
+
self.previous_count = 0
|
| 72 |
+
|
| 73 |
+
if self.think_start_token in self.previous_content:
|
| 74 |
+
self._in_reasoning = True
|
| 75 |
+
if self.think_end_token in self.previous_content:
|
| 76 |
+
self._in_reasoning = False
|
| 77 |
+
|
| 78 |
+
def _maybe_apply_force_nonempty_content(
|
| 79 |
+
self, ret: StreamingParseResult
|
| 80 |
+
) -> StreamingParseResult:
|
| 81 |
+
if self._force_nonempty_content and not ret.normal_text:
|
| 82 |
+
ret.normal_text, ret.reasoning_text = ret.reasoning_text, ret.normal_text
|
| 83 |
+
return ret
|
| 84 |
+
|
| 85 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 86 |
+
"""
|
| 87 |
+
One-time parsing: Detects and parses reasoning sections in the provided text.
|
| 88 |
+
Returns both reasoning content and normal text separately.
|
| 89 |
+
"""
|
| 90 |
+
return self._maybe_apply_force_nonempty_content(
|
| 91 |
+
self._detect_and_parse_impl(text)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
def _detect_and_parse_impl(self, text: str) -> StreamingParseResult:
|
| 95 |
+
in_reasoning = self._in_reasoning or self.think_start_token in text
|
| 96 |
+
|
| 97 |
+
if not in_reasoning:
|
| 98 |
+
return StreamingParseResult(normal_text=text)
|
| 99 |
+
|
| 100 |
+
# The text is considered to be in a reasoning block.
|
| 101 |
+
think_start_text = self.think_start_token + self.think_start_self_label
|
| 102 |
+
processed_text = text
|
| 103 |
+
while processed_text.startswith(think_start_text):
|
| 104 |
+
processed_text = processed_text[len(think_start_text) :]
|
| 105 |
+
|
| 106 |
+
if (
|
| 107 |
+
self.think_end_token not in processed_text
|
| 108 |
+
and self.think_end_token not in self.previous_content
|
| 109 |
+
):
|
| 110 |
+
# Check for tool_start_token interruption
|
| 111 |
+
if (
|
| 112 |
+
in_reasoning
|
| 113 |
+
and self.tool_start_token is not None
|
| 114 |
+
and self.tool_start_token in processed_text
|
| 115 |
+
):
|
| 116 |
+
# Find the first occurrence of tool_start_token and split there
|
| 117 |
+
tool_idx = processed_text.find(self.tool_start_token)
|
| 118 |
+
reasoning_text = processed_text[:tool_idx]
|
| 119 |
+
# Preserve tool_start_token in normal text
|
| 120 |
+
normal_text = processed_text[tool_idx:]
|
| 121 |
+
return StreamingParseResult(
|
| 122 |
+
normal_text=normal_text, reasoning_text=reasoning_text
|
| 123 |
+
)
|
| 124 |
+
# Assume reasoning was truncated before end token
|
| 125 |
+
return StreamingParseResult(reasoning_text=processed_text)
|
| 126 |
+
|
| 127 |
+
# Extract reasoning content
|
| 128 |
+
if self.think_end_token in processed_text:
|
| 129 |
+
splits = processed_text.split(self.think_end_token, maxsplit=1)
|
| 130 |
+
reasoning_text = splits[0]
|
| 131 |
+
normal_text = splits[1]
|
| 132 |
+
|
| 133 |
+
return StreamingParseResult(
|
| 134 |
+
normal_text=normal_text, reasoning_text=reasoning_text
|
| 135 |
+
)
|
| 136 |
+
else:
|
| 137 |
+
# think_end_token is in self.previous_content for continue_final_message=True case
|
| 138 |
+
return StreamingParseResult(normal_text=processed_text)
|
| 139 |
+
|
| 140 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 141 |
+
"""
|
| 142 |
+
Streaming incremental parsing for reasoning content.
|
| 143 |
+
Handles partial reasoning tags and content.
|
| 144 |
+
|
| 145 |
+
If stream_reasoning is False:
|
| 146 |
+
Accumulates reasoning content until the end tag is found
|
| 147 |
+
If stream_reasoning is True:
|
| 148 |
+
Streams reasoning content as it arrives
|
| 149 |
+
"""
|
| 150 |
+
ret = self._parse_streaming_increment_impl(new_text)
|
| 151 |
+
if self._force_nonempty_content:
|
| 152 |
+
if self._in_reasoning:
|
| 153 |
+
self._accumulated_reasoning += ret.reasoning_text
|
| 154 |
+
else:
|
| 155 |
+
self._accumulated_reasoning = ""
|
| 156 |
+
return ret
|
| 157 |
+
|
| 158 |
+
def _parse_streaming_increment_impl(self, new_text: str) -> StreamingParseResult:
|
| 159 |
+
self._buffer += new_text
|
| 160 |
+
current_text = self._buffer
|
| 161 |
+
|
| 162 |
+
think_start_text = self.think_start_token + self.think_start_self_label
|
| 163 |
+
|
| 164 |
+
# If the current text is a prefix of the think token, keep buffering
|
| 165 |
+
tokens_to_check = [think_start_text, self.think_end_token]
|
| 166 |
+
if self.tool_start_token:
|
| 167 |
+
tokens_to_check.append(self.tool_start_token)
|
| 168 |
+
if any(
|
| 169 |
+
token.startswith(current_text) and token != current_text
|
| 170 |
+
for token in tokens_to_check
|
| 171 |
+
):
|
| 172 |
+
return StreamingParseResult()
|
| 173 |
+
|
| 174 |
+
# Strip `<think>` token if present
|
| 175 |
+
if not self.stripped_think_start and think_start_text in current_text:
|
| 176 |
+
current_text = current_text.replace(think_start_text, "", 1)
|
| 177 |
+
self.stripped_think_start = True
|
| 178 |
+
self._in_reasoning = True
|
| 179 |
+
|
| 180 |
+
# Handle end of reasoning block
|
| 181 |
+
if self._in_reasoning and self.think_end_token in current_text:
|
| 182 |
+
end_idx = current_text.find(self.think_end_token)
|
| 183 |
+
|
| 184 |
+
reasoning_text = current_text[:end_idx]
|
| 185 |
+
|
| 186 |
+
self._buffer = ""
|
| 187 |
+
self._in_reasoning = False
|
| 188 |
+
normal_text = current_text[end_idx + len(self.think_end_token) :]
|
| 189 |
+
|
| 190 |
+
return StreamingParseResult(
|
| 191 |
+
normal_text=normal_text, reasoning_text=reasoning_text
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# Continue with reasoning content
|
| 195 |
+
if self._in_reasoning:
|
| 196 |
+
# Check for tool_start_token interruption
|
| 197 |
+
if self.tool_start_token and self.tool_start_token in current_text:
|
| 198 |
+
tool_idx = current_text.find(self.tool_start_token)
|
| 199 |
+
reasoning_text = current_text[:tool_idx]
|
| 200 |
+
# Preserve tool_start_token in normal text
|
| 201 |
+
normal_text = current_text[tool_idx:]
|
| 202 |
+
self._buffer = ""
|
| 203 |
+
self._in_reasoning = False
|
| 204 |
+
return StreamingParseResult(
|
| 205 |
+
normal_text=normal_text, reasoning_text=reasoning_text
|
| 206 |
+
)
|
| 207 |
+
if self.stream_reasoning:
|
| 208 |
+
# Stream the content immediately
|
| 209 |
+
self._buffer = ""
|
| 210 |
+
return StreamingParseResult(reasoning_text=current_text)
|
| 211 |
+
else:
|
| 212 |
+
return StreamingParseResult()
|
| 213 |
+
|
| 214 |
+
# If we're not in a reasoning block return as normal text
|
| 215 |
+
if not self._in_reasoning:
|
| 216 |
+
self._buffer = ""
|
| 217 |
+
return StreamingParseResult(normal_text=current_text)
|
| 218 |
+
|
| 219 |
+
return StreamingParseResult()
|
| 220 |
+
|
| 221 |
+
def finish(self) -> StreamingParseResult:
|
| 222 |
+
"""
|
| 223 |
+
Called once when the stream ends. If force_nonempty_content is set
|
| 224 |
+
and the stream ended mid-reasoning, reclassifies the accumulated
|
| 225 |
+
reasoning (plus any partial token still buffered) as normal text.
|
| 226 |
+
"""
|
| 227 |
+
if self._force_nonempty_content and self._in_reasoning:
|
| 228 |
+
# stream_reasoning=False never clears _buffer, so the opening think
|
| 229 |
+
# token (stripped only from the base class's local view) survives here.
|
| 230 |
+
buffer = self._buffer
|
| 231 |
+
think_start_text = self.think_start_token + self.think_start_self_label
|
| 232 |
+
if buffer.startswith(think_start_text):
|
| 233 |
+
buffer = buffer[len(think_start_text) :]
|
| 234 |
+
normal_text = self._accumulated_reasoning + buffer
|
| 235 |
+
self._accumulated_reasoning = ""
|
| 236 |
+
self._buffer = ""
|
| 237 |
+
if normal_text:
|
| 238 |
+
return StreamingParseResult(normal_text=normal_text)
|
| 239 |
+
return StreamingParseResult()
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class DeepSeekR1Detector(BaseReasoningFormatDetector):
|
| 243 |
+
"""
|
| 244 |
+
Detector for DeepSeek-R1 model.
|
| 245 |
+
Assumes reasoning format:
|
| 246 |
+
(<think>)*(.*)</think>
|
| 247 |
+
Returns all the text before the </think> tag as `reasoning_text`
|
| 248 |
+
and the rest of the text as `normal_text`.
|
| 249 |
+
|
| 250 |
+
Supported models:
|
| 251 |
+
- DeepSeek-R1: Always generates thinking content without <think> start tag
|
| 252 |
+
- DeepSeek-R1-0528: Generates thinking content with <think> start tag
|
| 253 |
+
|
| 254 |
+
Format patterns:
|
| 255 |
+
- DeepSeek-R1: "I need to think about this...</think>The answer is 42."
|
| 256 |
+
- DeepSeek-R1-0528: "<think>I need to think about this...</think>The answer is 42."
|
| 257 |
+
|
| 258 |
+
Args:
|
| 259 |
+
stream_reasoning (bool): If False, accumulates reasoning content until the end tag.
|
| 260 |
+
If True, streams reasoning content as it arrives.
|
| 261 |
+
"""
|
| 262 |
+
|
| 263 |
+
def __init__(
|
| 264 |
+
self,
|
| 265 |
+
stream_reasoning: bool = True,
|
| 266 |
+
force_reasoning: bool = True,
|
| 267 |
+
continue_final_message: bool = False,
|
| 268 |
+
previous_content: str = "",
|
| 269 |
+
force_nonempty_content: bool = False,
|
| 270 |
+
):
|
| 271 |
+
# DeepSeek-R1 is assumed to be reasoning until `</think>` token
|
| 272 |
+
super().__init__(
|
| 273 |
+
"<think>",
|
| 274 |
+
"</think>",
|
| 275 |
+
force_reasoning=True,
|
| 276 |
+
stream_reasoning=stream_reasoning,
|
| 277 |
+
continue_final_message=continue_final_message,
|
| 278 |
+
previous_content=previous_content,
|
| 279 |
+
force_nonempty_content=force_nonempty_content,
|
| 280 |
+
)
|
| 281 |
+
# https://github.com/sgl-project/sglang/pull/3202#discussion_r1950153599
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
class Qwen3Detector(BaseReasoningFormatDetector):
|
| 285 |
+
"""
|
| 286 |
+
Detector for Qwen3 models (e.g., Qwen/Qwen3-235B-A22B).
|
| 287 |
+
Assumes reasoning format:
|
| 288 |
+
(<think>)*(.*)</think>
|
| 289 |
+
|
| 290 |
+
Qwen3 models released before 07/2025 supports switching between thinking mode and normal
|
| 291 |
+
mode using `enable_thinking` parameter in the request parameter.
|
| 292 |
+
- enable_thinking=True: "<think>reasoning content</think>The answer is 42."
|
| 293 |
+
- enable_thinking=False: "The answer is 42." (no thinking tokens)
|
| 294 |
+
|
| 295 |
+
Args:
|
| 296 |
+
stream_reasoning (bool): If False, accumulates reasoning content until the end tag.
|
| 297 |
+
If True, streams reasoning content as it arrives.
|
| 298 |
+
"""
|
| 299 |
+
|
| 300 |
+
def __init__(
|
| 301 |
+
self,
|
| 302 |
+
stream_reasoning: bool = True,
|
| 303 |
+
force_reasoning: bool = False,
|
| 304 |
+
continue_final_message: bool = False,
|
| 305 |
+
previous_content: str = "",
|
| 306 |
+
force_nonempty_content: bool = False,
|
| 307 |
+
):
|
| 308 |
+
think_excluded_tokens = [
|
| 309 |
+
"<tool_call>",
|
| 310 |
+
"</tool_call>",
|
| 311 |
+
"<|im_end|>",
|
| 312 |
+
"<|endoftext|>",
|
| 313 |
+
]
|
| 314 |
+
super().__init__(
|
| 315 |
+
"<think>",
|
| 316 |
+
"</think>",
|
| 317 |
+
think_excluded_tokens=think_excluded_tokens,
|
| 318 |
+
force_reasoning=force_reasoning,
|
| 319 |
+
stream_reasoning=stream_reasoning,
|
| 320 |
+
# Qwen3.5 sometimes opens ``<tool_call>`` without closing
|
| 321 |
+
# ``</think>``; treat it as an implicit reasoning close.
|
| 322 |
+
tool_start_token="<tool_call>",
|
| 323 |
+
continue_final_message=continue_final_message,
|
| 324 |
+
previous_content=previous_content,
|
| 325 |
+
thinks_internally=True,
|
| 326 |
+
reasoning_default="enable_thinking",
|
| 327 |
+
force_nonempty_content=force_nonempty_content,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class KimiDetector(BaseReasoningFormatDetector):
|
| 332 |
+
"""
|
| 333 |
+
Detector for Kimi Thinking model.
|
| 334 |
+
Assumes reasoning format:
|
| 335 |
+
◁think▷*(.*)◁/think▷
|
| 336 |
+
Returns all the text before the ◁/think▷ tag as `reasoning_text`
|
| 337 |
+
and the rest of the text as `normal_text`.
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
def __init__(
|
| 341 |
+
self,
|
| 342 |
+
stream_reasoning: bool = True,
|
| 343 |
+
force_reasoning: bool = False,
|
| 344 |
+
continue_final_message: bool = False,
|
| 345 |
+
previous_content: str = "",
|
| 346 |
+
force_nonempty_content: bool = False,
|
| 347 |
+
):
|
| 348 |
+
super().__init__(
|
| 349 |
+
"◁think▷",
|
| 350 |
+
"◁/think▷",
|
| 351 |
+
force_reasoning=False,
|
| 352 |
+
stream_reasoning=stream_reasoning,
|
| 353 |
+
continue_final_message=continue_final_message,
|
| 354 |
+
previous_content=previous_content,
|
| 355 |
+
force_nonempty_content=force_nonempty_content,
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class KimiK2Detector(BaseReasoningFormatDetector):
|
| 360 |
+
"""
|
| 361 |
+
Detector for Kimi K2 models.
|
| 362 |
+
Assumes reasoning format:
|
| 363 |
+
(<think>)*(.*)</think>
|
| 364 |
+
|
| 365 |
+
Kimi K2 can switch from reasoning to tool-call section with
|
| 366 |
+
`<|tool_calls_section_begin|>` before emitting `</think>`.
|
| 367 |
+
"""
|
| 368 |
+
|
| 369 |
+
def __init__(
|
| 370 |
+
self,
|
| 371 |
+
stream_reasoning: bool = True,
|
| 372 |
+
force_reasoning: bool = False,
|
| 373 |
+
continue_final_message: bool = False,
|
| 374 |
+
previous_content: str = "",
|
| 375 |
+
force_nonempty_content: bool = False,
|
| 376 |
+
):
|
| 377 |
+
think_excluded_tokens = [
|
| 378 |
+
"<think>",
|
| 379 |
+
"<|tool_calls_section_begin|>",
|
| 380 |
+
"<|tool_call_begin|>",
|
| 381 |
+
"<|tool_call_argument_begin|>",
|
| 382 |
+
"<|tool_call_section_end|>",
|
| 383 |
+
"<|tool_call_end|>",
|
| 384 |
+
"[EOS]",
|
| 385 |
+
"<|im_end|>",
|
| 386 |
+
"<|end_header_id|>",
|
| 387 |
+
"[EOT]",
|
| 388 |
+
]
|
| 389 |
+
super().__init__(
|
| 390 |
+
"<think>",
|
| 391 |
+
"</think>",
|
| 392 |
+
think_excluded_tokens=think_excluded_tokens,
|
| 393 |
+
force_reasoning=force_reasoning,
|
| 394 |
+
stream_reasoning=stream_reasoning,
|
| 395 |
+
tool_start_token="<|tool_calls_section_begin|>",
|
| 396 |
+
continue_final_message=continue_final_message,
|
| 397 |
+
previous_content=previous_content,
|
| 398 |
+
reasoning_default="thinking",
|
| 399 |
+
force_nonempty_content=force_nonempty_content,
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
class Glm45Detector(BaseReasoningFormatDetector):
|
| 404 |
+
"""
|
| 405 |
+
Detector for GLM-4.5 models.
|
| 406 |
+
Assumes reasoning format:
|
| 407 |
+
(<think>)*(.*)</think>
|
| 408 |
+
|
| 409 |
+
GLM-4.5 uses `<tool_call>` as the tool start token to switch from reasoning mode to normal mode.
|
| 410 |
+
|
| 411 |
+
Args:
|
| 412 |
+
stream_reasoning (bool): If False, accumulates reasoning content until the end tag.
|
| 413 |
+
If True, streams reasoning content as it arrives.
|
| 414 |
+
"""
|
| 415 |
+
|
| 416 |
+
def __init__(
|
| 417 |
+
self,
|
| 418 |
+
stream_reasoning: bool = True,
|
| 419 |
+
force_reasoning: bool = False,
|
| 420 |
+
force_nonempty_content: bool = False,
|
| 421 |
+
):
|
| 422 |
+
think_excluded_tokens = [
|
| 423 |
+
"<tool_call>",
|
| 424 |
+
"</tool_call>",
|
| 425 |
+
"<eop>",
|
| 426 |
+
"<|user|>",
|
| 427 |
+
"<|endoftext|>",
|
| 428 |
+
]
|
| 429 |
+
super().__init__(
|
| 430 |
+
"<think>",
|
| 431 |
+
"</think>",
|
| 432 |
+
think_excluded_tokens=think_excluded_tokens,
|
| 433 |
+
force_reasoning=force_reasoning,
|
| 434 |
+
stream_reasoning=stream_reasoning,
|
| 435 |
+
tool_start_token="<tool_call>",
|
| 436 |
+
thinks_internally=True,
|
| 437 |
+
reasoning_default="enable_thinking",
|
| 438 |
+
force_nonempty_content=force_nonempty_content,
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
class GptOssDetector(BaseReasoningFormatDetector):
|
| 443 |
+
"""
|
| 444 |
+
Detector for T4-style reasoning format (GPT-OSS), using the HarmonyParser.
|
| 445 |
+
"""
|
| 446 |
+
|
| 447 |
+
def __init__(
|
| 448 |
+
self,
|
| 449 |
+
stream_reasoning: bool = True,
|
| 450 |
+
force_reasoning: bool = True,
|
| 451 |
+
continue_final_message: bool = False,
|
| 452 |
+
previous_content: str = "",
|
| 453 |
+
force_nonempty_content: bool = False,
|
| 454 |
+
):
|
| 455 |
+
super().__init__(
|
| 456 |
+
"<|channel|>analysis<|message|>",
|
| 457 |
+
"<|end|>",
|
| 458 |
+
force_reasoning=force_reasoning,
|
| 459 |
+
stream_reasoning=stream_reasoning,
|
| 460 |
+
continue_final_message=continue_final_message,
|
| 461 |
+
previous_content=previous_content,
|
| 462 |
+
force_nonempty_content=force_nonempty_content,
|
| 463 |
+
)
|
| 464 |
+
self.parser = HarmonyParser()
|
| 465 |
+
|
| 466 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 467 |
+
events = self.parser.parse(text)
|
| 468 |
+
# Flush the buffer for one-shot parsing
|
| 469 |
+
events += self.parser.parse("")
|
| 470 |
+
|
| 471 |
+
reasoning_text = "".join(
|
| 472 |
+
[e.content for e in events if e.event_type == "reasoning"]
|
| 473 |
+
)
|
| 474 |
+
normal_parts = []
|
| 475 |
+
for e in events:
|
| 476 |
+
if e.event_type == "normal":
|
| 477 |
+
normal_parts.append(e.content)
|
| 478 |
+
elif e.event_type == "tool_call":
|
| 479 |
+
# Use raw_text to preserve structural markers for function call detector
|
| 480 |
+
normal_parts.append(e.raw_text if e.raw_text else e.content)
|
| 481 |
+
normal_text = "".join(normal_parts)
|
| 482 |
+
# Tool call events preserve raw text with structural markers
|
| 483 |
+
|
| 484 |
+
return self._maybe_apply_force_nonempty_content(
|
| 485 |
+
StreamingParseResult(
|
| 486 |
+
normal_text=normal_text,
|
| 487 |
+
reasoning_text=reasoning_text,
|
| 488 |
+
)
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 492 |
+
events = self.parser.parse(new_text)
|
| 493 |
+
|
| 494 |
+
reasoning_text = "".join(
|
| 495 |
+
[e.content for e in events if e.event_type == "reasoning"]
|
| 496 |
+
)
|
| 497 |
+
normal_parts = []
|
| 498 |
+
for e in events:
|
| 499 |
+
if e.event_type == "normal":
|
| 500 |
+
normal_parts.append(e.content)
|
| 501 |
+
elif e.event_type == "tool_call":
|
| 502 |
+
# Use raw_text to preserve structural markers for function call detector
|
| 503 |
+
normal_parts.append(e.raw_text if e.raw_text else e.content)
|
| 504 |
+
normal_text = "".join(normal_parts)
|
| 505 |
+
|
| 506 |
+
return StreamingParseResult(
|
| 507 |
+
normal_text=normal_text,
|
| 508 |
+
reasoning_text=reasoning_text,
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
class MiniMaxAppendThinkDetector(BaseReasoningFormatDetector):
|
| 513 |
+
"""
|
| 514 |
+
Append `<think>` token to the beginning of the text.
|
| 515 |
+
"""
|
| 516 |
+
|
| 517 |
+
def __init__(
|
| 518 |
+
self,
|
| 519 |
+
stream_reasoning: bool = True,
|
| 520 |
+
force_reasoning: bool = False,
|
| 521 |
+
continue_final_message: bool = False,
|
| 522 |
+
previous_content: str = "",
|
| 523 |
+
force_nonempty_content: bool = False,
|
| 524 |
+
):
|
| 525 |
+
# scheduler.py need `reasoning_parser.detector.think_end_token`
|
| 526 |
+
super().__init__(
|
| 527 |
+
"<think>",
|
| 528 |
+
"</think>",
|
| 529 |
+
force_reasoning=force_reasoning,
|
| 530 |
+
stream_reasoning=stream_reasoning,
|
| 531 |
+
continue_final_message=continue_final_message,
|
| 532 |
+
previous_content=previous_content,
|
| 533 |
+
force_nonempty_content=force_nonempty_content,
|
| 534 |
+
)
|
| 535 |
+
self.is_first_chunk = False
|
| 536 |
+
|
| 537 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 538 |
+
if not self.is_first_chunk:
|
| 539 |
+
self.is_first_chunk = True
|
| 540 |
+
new_text = self.think_start_token + new_text
|
| 541 |
+
return StreamingParseResult(normal_text=new_text)
|
| 542 |
+
|
| 543 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 544 |
+
return StreamingParseResult(normal_text=self.think_start_token + text)
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
class Nemotron3Detector(BaseReasoningFormatDetector):
|
| 548 |
+
"""
|
| 549 |
+
Detector for Nemotron3 model.
|
| 550 |
+
Uses the same reasoning format as DeepSeek-R1: (<think>)*(.*)</think>
|
| 551 |
+
|
| 552 |
+
"""
|
| 553 |
+
|
| 554 |
+
def __init__(
|
| 555 |
+
self,
|
| 556 |
+
stream_reasoning: bool = True,
|
| 557 |
+
force_reasoning: bool = False,
|
| 558 |
+
continue_final_message: bool = False,
|
| 559 |
+
previous_content: str = "",
|
| 560 |
+
force_nonempty_content: bool = False,
|
| 561 |
+
):
|
| 562 |
+
super().__init__(
|
| 563 |
+
"<think>",
|
| 564 |
+
"</think>",
|
| 565 |
+
force_reasoning=force_reasoning,
|
| 566 |
+
stream_reasoning=stream_reasoning,
|
| 567 |
+
tool_start_token="<tool_call>",
|
| 568 |
+
continue_final_message=continue_final_message,
|
| 569 |
+
previous_content=previous_content,
|
| 570 |
+
reasoning_default="enable_thinking",
|
| 571 |
+
force_nonempty_content=force_nonempty_content,
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
class MiniMaxM3Detector(BaseReasoningFormatDetector):
|
| 576 |
+
"""MiniMax-M3 detector. Format: (<mm:think>)*(.*)</mm:think>.
|
| 577 |
+
|
| 578 |
+
In multi-turn chats M3 prefixes earlier non-thinking turns with a bare
|
| 579 |
+
``</mm:think>``, so a non-thinking reply may open with one stray closer; drop it unless thinking.
|
| 580 |
+
"""
|
| 581 |
+
|
| 582 |
+
def __init__(
|
| 583 |
+
self,
|
| 584 |
+
stream_reasoning: bool = True,
|
| 585 |
+
force_reasoning: bool = False,
|
| 586 |
+
continue_final_message: bool = False,
|
| 587 |
+
previous_content: str = "",
|
| 588 |
+
force_nonempty_content: bool = False,
|
| 589 |
+
):
|
| 590 |
+
super().__init__(
|
| 591 |
+
"<mm:think>",
|
| 592 |
+
"</mm:think>",
|
| 593 |
+
force_reasoning=force_reasoning,
|
| 594 |
+
stream_reasoning=stream_reasoning,
|
| 595 |
+
continue_final_message=continue_final_message,
|
| 596 |
+
previous_content=previous_content,
|
| 597 |
+
)
|
| 598 |
+
self._lead_buffer = ""
|
| 599 |
+
self._checked_leading_close = False
|
| 600 |
+
self._force_nonempty_content = force_nonempty_content
|
| 601 |
+
|
| 602 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 603 |
+
if not self._in_reasoning and text.lstrip().startswith(self.think_end_token):
|
| 604 |
+
text = text.lstrip()[len(self.think_end_token) :]
|
| 605 |
+
ret = super().detect_and_parse(text)
|
| 606 |
+
if self._force_nonempty_content and not ret.normal_text:
|
| 607 |
+
ret.normal_text, ret.reasoning_text = ret.reasoning_text, ret.normal_text
|
| 608 |
+
return ret
|
| 609 |
+
|
| 610 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 611 |
+
# ``</mm:think>`` is a single token, so a stray leading closer arrives whole.
|
| 612 |
+
if not self._checked_leading_close and not self._in_reasoning:
|
| 613 |
+
self._lead_buffer += new_text
|
| 614 |
+
stripped = self._lead_buffer.lstrip()
|
| 615 |
+
if not stripped:
|
| 616 |
+
return StreamingParseResult()
|
| 617 |
+
self._checked_leading_close = True
|
| 618 |
+
if stripped.startswith(self.think_end_token):
|
| 619 |
+
new_text = stripped[len(self.think_end_token) :]
|
| 620 |
+
else:
|
| 621 |
+
new_text = self._lead_buffer
|
| 622 |
+
self._lead_buffer = ""
|
| 623 |
+
if not new_text:
|
| 624 |
+
return StreamingParseResult()
|
| 625 |
+
return super().parse_streaming_increment(new_text)
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
class MistralDetector(BaseReasoningFormatDetector):
|
| 629 |
+
"""
|
| 630 |
+
Detector for Mistral models with reasoning (e.g., Mistral-Small-4-119B-2603).
|
| 631 |
+
Assumes reasoning format:
|
| 632 |
+
[THINK]reasoning content[/THINK]answer
|
| 633 |
+
|
| 634 |
+
Reasoning is optional — it only appears when reasoning_effort="high" is set.
|
| 635 |
+
When reasoning_effort="none", the model outputs directly without thinking tokens.
|
| 636 |
+
"""
|
| 637 |
+
|
| 638 |
+
def __init__(
|
| 639 |
+
self,
|
| 640 |
+
stream_reasoning: bool = True,
|
| 641 |
+
force_reasoning: bool = False,
|
| 642 |
+
continue_final_message: bool = False,
|
| 643 |
+
previous_content: str = "",
|
| 644 |
+
force_nonempty_content: bool = False,
|
| 645 |
+
):
|
| 646 |
+
super().__init__(
|
| 647 |
+
"[THINK]",
|
| 648 |
+
"[/THINK]",
|
| 649 |
+
force_reasoning=force_reasoning,
|
| 650 |
+
stream_reasoning=stream_reasoning,
|
| 651 |
+
continue_final_message=continue_final_message,
|
| 652 |
+
previous_content=previous_content,
|
| 653 |
+
reasoning_default="mistral",
|
| 654 |
+
force_nonempty_content=force_nonempty_content,
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
class HunyuanDetector(BaseReasoningFormatDetector):
|
| 659 |
+
"""
|
| 660 |
+
Detector for Hunyuan models (e.g., tencent/Hunyuan-A13B-Instruct).
|
| 661 |
+
|
| 662 |
+
Like Glm45Detector but uses ``<tool_calls>`` (plural) as the tool start token.
|
| 663 |
+
"""
|
| 664 |
+
|
| 665 |
+
def __init__(
|
| 666 |
+
self,
|
| 667 |
+
stream_reasoning: bool = True,
|
| 668 |
+
force_reasoning: bool = False,
|
| 669 |
+
continue_final_message: bool = False,
|
| 670 |
+
previous_content: str = "",
|
| 671 |
+
tokenizer=None,
|
| 672 |
+
force_nonempty_content: bool = False,
|
| 673 |
+
):
|
| 674 |
+
t = resolve_hunyuan_tokens(tokenizer)
|
| 675 |
+
think_open = t["think"]
|
| 676 |
+
think_close = (
|
| 677 |
+
"</" + think_open[1:] if think_open.startswith("<") else think_open
|
| 678 |
+
)
|
| 679 |
+
super().__init__(
|
| 680 |
+
think_open,
|
| 681 |
+
think_close,
|
| 682 |
+
force_reasoning=force_reasoning,
|
| 683 |
+
stream_reasoning=stream_reasoning,
|
| 684 |
+
tool_start_token=t["tool_calls"],
|
| 685 |
+
continue_final_message=continue_final_message,
|
| 686 |
+
previous_content=previous_content,
|
| 687 |
+
force_nonempty_content=force_nonempty_content,
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
class Gemma4Detector(BaseReasoningFormatDetector):
|
| 692 |
+
"""Gemma4 reasoning detector."""
|
| 693 |
+
|
| 694 |
+
def __init__(
|
| 695 |
+
self,
|
| 696 |
+
stream_reasoning: bool = True,
|
| 697 |
+
force_reasoning: bool = False,
|
| 698 |
+
continue_final_message: bool = False,
|
| 699 |
+
previous_content: str = "",
|
| 700 |
+
force_nonempty_content: bool = False,
|
| 701 |
+
):
|
| 702 |
+
super().__init__(
|
| 703 |
+
"<|channel>",
|
| 704 |
+
"<channel|>",
|
| 705 |
+
force_reasoning=force_reasoning,
|
| 706 |
+
stream_reasoning=stream_reasoning,
|
| 707 |
+
continue_final_message=continue_final_message,
|
| 708 |
+
previous_content=previous_content,
|
| 709 |
+
reasoning_default="explicit_enable_thinking",
|
| 710 |
+
force_nonempty_content=force_nonempty_content,
|
| 711 |
+
)
|
| 712 |
+
self.think_start_self_label = "thought\n"
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
_INKLING_CONTENT_KINDS = {
|
| 716 |
+
CONTENT_THINKING: "reasoning",
|
| 717 |
+
CONTENT_TEXT: "content",
|
| 718 |
+
}
|
| 719 |
+
_INKLING_END_TOKENS = {
|
| 720 |
+
CONTENT_MODEL_END_SAMPLING,
|
| 721 |
+
END_MESSAGE,
|
| 722 |
+
}
|
| 723 |
+
_INKLING_CONTROL_TOKENS = INKLING_CONTROL_TOKENS
|
| 724 |
+
_INKLING_CONTROL_RE = re.compile(
|
| 725 |
+
"|".join(re.escape(t) for t in sorted(_INKLING_CONTROL_TOKENS))
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
class InklingDetector(BaseReasoningFormatDetector):
|
| 730 |
+
"""Detector for Inkling typed content blocks."""
|
| 731 |
+
|
| 732 |
+
# Parse the model's sequence of typed content blocks, for example:
|
| 733 |
+
# <|message_model|><|content_thinking|>reasoning<|end_message|>
|
| 734 |
+
# <|message_model|><|content_text|>visible answer<|end_message|>
|
| 735 |
+
# <|content_model_end_sampling|>
|
| 736 |
+
# Special tokens must decode literally so thinking and visible text can be
|
| 737 |
+
# routed to their respective response fields.
|
| 738 |
+
def __init__(
|
| 739 |
+
self,
|
| 740 |
+
stream_reasoning: bool = True,
|
| 741 |
+
force_reasoning: bool = False,
|
| 742 |
+
continue_final_message: bool = False,
|
| 743 |
+
previous_content: str = "",
|
| 744 |
+
force_nonempty_content: bool = False,
|
| 745 |
+
):
|
| 746 |
+
del force_nonempty_content
|
| 747 |
+
super().__init__(
|
| 748 |
+
CONTENT_THINKING,
|
| 749 |
+
END_MESSAGE,
|
| 750 |
+
force_reasoning=force_reasoning,
|
| 751 |
+
stream_reasoning=stream_reasoning,
|
| 752 |
+
continue_final_message=continue_final_message,
|
| 753 |
+
previous_content=previous_content,
|
| 754 |
+
thinks_internally=False,
|
| 755 |
+
reasoning_default="always",
|
| 756 |
+
)
|
| 757 |
+
|
| 758 |
+
self._kind: str | None = None
|
| 759 |
+
self._pending_header = ""
|
| 760 |
+
self._pending_reasoning = ""
|
| 761 |
+
|
| 762 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 763 |
+
self._buffer = ""
|
| 764 |
+
self._kind = None
|
| 765 |
+
self._pending_header = ""
|
| 766 |
+
self._pending_reasoning = ""
|
| 767 |
+
ret = self._parse_blocks(text)
|
| 768 |
+
if self._kind == "reasoning" and not self.stream_reasoning:
|
| 769 |
+
ret.reasoning_text += self._pending_reasoning
|
| 770 |
+
self._kind = None
|
| 771 |
+
self._pending_header = ""
|
| 772 |
+
self._pending_reasoning = ""
|
| 773 |
+
return ret
|
| 774 |
+
|
| 775 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 776 |
+
text = self._buffer + new_text
|
| 777 |
+
partial_len = self._partial_control_length(text)
|
| 778 |
+
if partial_len:
|
| 779 |
+
self._buffer = text[-partial_len:]
|
| 780 |
+
text = text[:-partial_len]
|
| 781 |
+
else:
|
| 782 |
+
self._buffer = ""
|
| 783 |
+
return self._parse_blocks(text)
|
| 784 |
+
|
| 785 |
+
def finish(self) -> StreamingParseResult:
|
| 786 |
+
# Flush reasoning buffered under stream_reasoning=False when the stream
|
| 787 |
+
# ends before a control/end token closes the block (e.g. max_tokens cut
|
| 788 |
+
# a thinking block short). Mirrors the non-streaming flush in
|
| 789 |
+
# detect_and_parse; without it the trailing reasoning trace is dropped.
|
| 790 |
+
reasoning_text = ""
|
| 791 |
+
if self._kind == "reasoning" and not self.stream_reasoning:
|
| 792 |
+
reasoning_text = self._pending_reasoning
|
| 793 |
+
self._buffer = ""
|
| 794 |
+
self._pending_reasoning = ""
|
| 795 |
+
self._pending_header = ""
|
| 796 |
+
self._kind = None
|
| 797 |
+
return StreamingParseResult(reasoning_text=reasoning_text)
|
| 798 |
+
|
| 799 |
+
@staticmethod
|
| 800 |
+
def _partial_control_length(text: str) -> int:
|
| 801 |
+
max_token_len = max(map(len, _INKLING_CONTROL_TOKENS))
|
| 802 |
+
for length in range(min(len(text), max_token_len - 1), 0, -1):
|
| 803 |
+
suffix = text[-length:]
|
| 804 |
+
if any(
|
| 805 |
+
len(suffix) < len(token) and token.startswith(suffix)
|
| 806 |
+
for token in _INKLING_CONTROL_TOKENS
|
| 807 |
+
):
|
| 808 |
+
return length
|
| 809 |
+
return 0
|
| 810 |
+
|
| 811 |
+
def _parse_blocks(self, text: str) -> StreamingParseResult:
|
| 812 |
+
reasoning: list[str] = []
|
| 813 |
+
content: list[str] = []
|
| 814 |
+
saw_control = False
|
| 815 |
+
pos = 0
|
| 816 |
+
|
| 817 |
+
def emit(text: str) -> None:
|
| 818 |
+
if self._kind == "reasoning":
|
| 819 |
+
if self.stream_reasoning:
|
| 820 |
+
reasoning.append(text)
|
| 821 |
+
else:
|
| 822 |
+
self._pending_reasoning += text
|
| 823 |
+
elif self._kind == "content":
|
| 824 |
+
content.append(text)
|
| 825 |
+
elif self._kind == "tool":
|
| 826 |
+
content.append(text)
|
| 827 |
+
elif self._kind == "header":
|
| 828 |
+
self._pending_header += text
|
| 829 |
+
elif text:
|
| 830 |
+
# No open block — e.g. a continue_final_message stream resuming
|
| 831 |
+
# mid text block. Route to visible content, matching the
|
| 832 |
+
# no-control-token path below.
|
| 833 |
+
content.append(text)
|
| 834 |
+
|
| 835 |
+
def flush_reasoning() -> None:
|
| 836 |
+
if self._kind == "reasoning" and not self.stream_reasoning:
|
| 837 |
+
reasoning.append(self._pending_reasoning)
|
| 838 |
+
self._pending_reasoning = ""
|
| 839 |
+
|
| 840 |
+
for match in _INKLING_CONTROL_RE.finditer(text):
|
| 841 |
+
saw_control = True
|
| 842 |
+
emit(text[pos : match.start()])
|
| 843 |
+
|
| 844 |
+
token = match.group(0)
|
| 845 |
+
pos = match.end()
|
| 846 |
+
if token == MESSAGE_MODEL:
|
| 847 |
+
if self._kind in (None, "header"):
|
| 848 |
+
flush_reasoning()
|
| 849 |
+
self._pending_header = ""
|
| 850 |
+
self._kind = "header"
|
| 851 |
+
else:
|
| 852 |
+
# Inside an open block a decoded <|message_model|> string
|
| 853 |
+
# is payload the model wrote (e.g. quoting the protocol) —
|
| 854 |
+
# a real header can only follow an end token. Preserve it
|
| 855 |
+
# instead of rerouting the rest of the block into a header.
|
| 856 |
+
emit(token)
|
| 857 |
+
elif token == CONTENT_INVOKE_TOOL_JSON:
|
| 858 |
+
flush_reasoning()
|
| 859 |
+
if self._kind == "header":
|
| 860 |
+
content.extend(
|
| 861 |
+
(MESSAGE_MODEL, self._pending_header, CONTENT_INVOKE_TOOL_JSON)
|
| 862 |
+
)
|
| 863 |
+
self._pending_header = ""
|
| 864 |
+
else:
|
| 865 |
+
content.append(token)
|
| 866 |
+
self._kind = "tool"
|
| 867 |
+
elif self._kind == "tool":
|
| 868 |
+
content.append(token)
|
| 869 |
+
if token in _INKLING_END_TOKENS:
|
| 870 |
+
self._kind = None
|
| 871 |
+
elif token in _INKLING_CONTENT_KINDS:
|
| 872 |
+
flush_reasoning()
|
| 873 |
+
self._pending_header = ""
|
| 874 |
+
self._kind = _INKLING_CONTENT_KINDS[token]
|
| 875 |
+
elif token in _INKLING_END_TOKENS:
|
| 876 |
+
flush_reasoning()
|
| 877 |
+
self._pending_header = ""
|
| 878 |
+
self._kind = None
|
| 879 |
+
|
| 880 |
+
tail = text[pos:]
|
| 881 |
+
if saw_control or self._kind is not None:
|
| 882 |
+
emit(tail)
|
| 883 |
+
else:
|
| 884 |
+
content.append(text)
|
| 885 |
+
|
| 886 |
+
return StreamingParseResult(
|
| 887 |
+
normal_text="".join(content),
|
| 888 |
+
reasoning_text="".join(reasoning),
|
| 889 |
+
)
|
| 890 |
+
|
| 891 |
+
|
| 892 |
+
class _DeepSeekV3Detector(Qwen3Detector):
|
| 893 |
+
"""DeepSeek-V3 reuses Qwen3 tokens but requires explicit thinking=True to enable."""
|
| 894 |
+
|
| 895 |
+
def __init__(self, **kwargs):
|
| 896 |
+
super().__init__(**kwargs)
|
| 897 |
+
self.reasoning_default = "explicit_thinking"
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
class _MimoDetector(Qwen3Detector):
|
| 901 |
+
"""MIMO reuses Qwen3 tokens but requires explicit enable_thinking=True to enable."""
|
| 902 |
+
|
| 903 |
+
def __init__(self, **kwargs):
|
| 904 |
+
super().__init__(**kwargs)
|
| 905 |
+
self.reasoning_default = "explicit_enable_thinking"
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
class _PoolsideV1Detector(Qwen3Detector):
|
| 909 |
+
"""Poolside v1 (Laguna-XS.2) reuses Qwen3 <think> tokens but the HF chat template
|
| 910 |
+
defaults `enable_thinking=False`; reasoning is opt-in via `enable_thinking=True`."""
|
| 911 |
+
|
| 912 |
+
def __init__(self, **kwargs):
|
| 913 |
+
super().__init__(**kwargs)
|
| 914 |
+
self.reasoning_default = "explicit_enable_thinking"
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
class Apertus2509Detector(BaseReasoningFormatDetector):
|
| 918 |
+
"""
|
| 919 |
+
Detector for Apertus 2509 models
|
| 920 |
+
|
| 921 |
+
Reasoning blocks are delimited by:
|
| 922 |
+
<|inner_prefix|> ... <|inner_suffix|>
|
| 923 |
+
"""
|
| 924 |
+
|
| 925 |
+
def __init__(
|
| 926 |
+
self,
|
| 927 |
+
stream_reasoning: bool = True,
|
| 928 |
+
force_reasoning: bool = False,
|
| 929 |
+
continue_final_message: bool = False,
|
| 930 |
+
previous_content: str = "",
|
| 931 |
+
force_nonempty_content: bool = False,
|
| 932 |
+
):
|
| 933 |
+
super().__init__(
|
| 934 |
+
"<|inner_prefix|>",
|
| 935 |
+
"<|inner_suffix|>",
|
| 936 |
+
force_reasoning=False,
|
| 937 |
+
stream_reasoning=stream_reasoning,
|
| 938 |
+
continue_final_message=continue_final_message,
|
| 939 |
+
previous_content=previous_content,
|
| 940 |
+
force_nonempty_content=force_nonempty_content,
|
| 941 |
+
)
|
| 942 |
+
self._force_reasoning = force_reasoning
|
| 943 |
+
self._tool_start_token = "<|tools_prefix|>["
|
| 944 |
+
self._tool_end_token = "<|tools_suffix|>"
|
| 945 |
+
self._reasoning_acc: str = ""
|
| 946 |
+
self._in_inner_tool: bool = False
|
| 947 |
+
|
| 948 |
+
@staticmethod
|
| 949 |
+
def _ends_with_partial_token(buffer: str, token: str) -> int:
|
| 950 |
+
for i in range(1, min(len(buffer) + 1, len(token))):
|
| 951 |
+
if token.startswith(buffer[-i:]):
|
| 952 |
+
return i
|
| 953 |
+
return 0
|
| 954 |
+
|
| 955 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 956 |
+
blocks = self.detect_and_parse_block_sequence(text)
|
| 957 |
+
reasoning_parts = [t for k, t in blocks if k == "reasoning"]
|
| 958 |
+
text_parts = [t for k, t in blocks if k == "text"]
|
| 959 |
+
ret = StreamingParseResult(
|
| 960 |
+
normal_text="".join(text_parts),
|
| 961 |
+
reasoning_text="".join(reasoning_parts),
|
| 962 |
+
)
|
| 963 |
+
return self._maybe_apply_force_nonempty_content(ret)
|
| 964 |
+
|
| 965 |
+
def detect_and_parse_block_sequence(self, text: str) -> list[tuple[str, str]]:
|
| 966 |
+
"""Return an ordered sequence of blocks: [("reasoning"|"text", content), ...]"""
|
| 967 |
+
start_tok = self.think_start_token
|
| 968 |
+
end_tok = self.think_end_token
|
| 969 |
+
blocks: list[tuple[str, str]] = []
|
| 970 |
+
cursor = 0
|
| 971 |
+
|
| 972 |
+
# continue_final_message can resume inside an existing inner
|
| 973 |
+
if self._in_reasoning:
|
| 974 |
+
if (e := text.find(end_tok, cursor)) == -1:
|
| 975 |
+
blocks.extend(self._split_inner_reasoning(text[cursor:]))
|
| 976 |
+
blocks.append(("text", ""))
|
| 977 |
+
return blocks
|
| 978 |
+
blocks.extend(self._split_inner_reasoning(text[cursor:e]))
|
| 979 |
+
cursor = e + len(end_tok)
|
| 980 |
+
|
| 981 |
+
while True:
|
| 982 |
+
if (s := text.find(start_tok, cursor)) == -1:
|
| 983 |
+
# Always include the trailing text block (may be empty)
|
| 984 |
+
blocks.append(("text", text[cursor:]))
|
| 985 |
+
break
|
| 986 |
+
if s > cursor:
|
| 987 |
+
blocks.append(("text", text[cursor:s]))
|
| 988 |
+
|
| 989 |
+
cursor = s + len(start_tok)
|
| 990 |
+
if (e := text.find(end_tok, cursor)) == -1:
|
| 991 |
+
blocks.extend(self._split_inner_reasoning(text[cursor:]))
|
| 992 |
+
blocks.append(("text", ""))
|
| 993 |
+
break
|
| 994 |
+
blocks.extend(self._split_inner_reasoning(text[cursor:e]))
|
| 995 |
+
cursor = e + len(end_tok)
|
| 996 |
+
|
| 997 |
+
last_idx = len(blocks) - 1
|
| 998 |
+
blocks = [
|
| 999 |
+
(k, t)
|
| 1000 |
+
for i, (k, t) in enumerate(blocks)
|
| 1001 |
+
if not (k == "text" and t == "" and i != last_idx)
|
| 1002 |
+
]
|
| 1003 |
+
|
| 1004 |
+
return blocks
|
| 1005 |
+
|
| 1006 |
+
def _split_inner_reasoning(self, inner_text: str) -> list[tuple[str, str]]:
|
| 1007 |
+
"""
|
| 1008 |
+
Split content inside <|inner_prefix|>...<|inner_suffix|> into:
|
| 1009 |
+
- ("reasoning", <thoughts text>)
|
| 1010 |
+
- ("text", <|tools_prefix|>[...]<|tools_suffix|>) for any tool calls inside reasoning
|
| 1011 |
+
"""
|
| 1012 |
+
tool_start = self._tool_start_token
|
| 1013 |
+
tool_end = self._tool_end_token
|
| 1014 |
+
out: list[tuple[str, str]] = []
|
| 1015 |
+
cursor = 0
|
| 1016 |
+
|
| 1017 |
+
while True:
|
| 1018 |
+
if (s := inner_text.find(tool_start, cursor)) == -1:
|
| 1019 |
+
if (tail := inner_text[cursor:]) != "":
|
| 1020 |
+
out.append(("reasoning", tail))
|
| 1021 |
+
break
|
| 1022 |
+
if s > cursor:
|
| 1023 |
+
out.append(("reasoning", inner_text[cursor:s]))
|
| 1024 |
+
|
| 1025 |
+
if (e := inner_text.find(tool_end, s)) == -1:
|
| 1026 |
+
out.append(("text", inner_text[s:]))
|
| 1027 |
+
break
|
| 1028 |
+
|
| 1029 |
+
out.append(("text", inner_text[s : e + len(tool_end)]))
|
| 1030 |
+
cursor = e + len(tool_end)
|
| 1031 |
+
|
| 1032 |
+
return out
|
| 1033 |
+
|
| 1034 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 1035 |
+
self._buffer += new_text
|
| 1036 |
+
|
| 1037 |
+
out_reasoning = ""
|
| 1038 |
+
out_normal = ""
|
| 1039 |
+
|
| 1040 |
+
start_tok = self.think_start_token
|
| 1041 |
+
end_tok = self.think_end_token
|
| 1042 |
+
tool_start = self._tool_start_token
|
| 1043 |
+
tool_end = self._tool_end_token
|
| 1044 |
+
|
| 1045 |
+
while True:
|
| 1046 |
+
if not self._in_reasoning:
|
| 1047 |
+
if (s := self._buffer.find(start_tok)) == -1:
|
| 1048 |
+
if partial := self._ends_with_partial_token(
|
| 1049 |
+
self._buffer, start_tok
|
| 1050 |
+
):
|
| 1051 |
+
out_normal += self._buffer[:-partial]
|
| 1052 |
+
self._buffer = self._buffer[-partial:]
|
| 1053 |
+
else:
|
| 1054 |
+
out_normal += self._buffer
|
| 1055 |
+
self._buffer = ""
|
| 1056 |
+
return StreamingParseResult(
|
| 1057 |
+
normal_text=out_normal, reasoning_text=out_reasoning
|
| 1058 |
+
)
|
| 1059 |
+
|
| 1060 |
+
out_normal += self._buffer[:s]
|
| 1061 |
+
self._buffer = self._buffer[s + len(start_tok) :]
|
| 1062 |
+
self._in_reasoning = True
|
| 1063 |
+
self._reasoning_acc = ""
|
| 1064 |
+
self._in_inner_tool = False
|
| 1065 |
+
continue
|
| 1066 |
+
|
| 1067 |
+
if self._in_inner_tool:
|
| 1068 |
+
if (end_pos := self._buffer.find(tool_end)) == -1:
|
| 1069 |
+
if (
|
| 1070 |
+
hold := self._ends_with_partial_token(self._buffer, tool_end)
|
| 1071 |
+
) != 0:
|
| 1072 |
+
out_normal += self._buffer[:-hold]
|
| 1073 |
+
self._buffer = self._buffer[-hold:]
|
| 1074 |
+
else:
|
| 1075 |
+
out_normal += self._buffer
|
| 1076 |
+
self._buffer = ""
|
| 1077 |
+
return StreamingParseResult(
|
| 1078 |
+
normal_text=out_normal, reasoning_text=out_reasoning
|
| 1079 |
+
)
|
| 1080 |
+
|
| 1081 |
+
out_normal += self._buffer[: end_pos + len(tool_end)]
|
| 1082 |
+
self._buffer = self._buffer[end_pos + len(tool_end) :]
|
| 1083 |
+
self._in_inner_tool = False
|
| 1084 |
+
continue
|
| 1085 |
+
|
| 1086 |
+
pos_tool = self._buffer.find(tool_start)
|
| 1087 |
+
pos_end = self._buffer.find(end_tok)
|
| 1088 |
+
|
| 1089 |
+
if pos_tool == -1 and pos_end == -1:
|
| 1090 |
+
if self.stream_reasoning:
|
| 1091 |
+
if (
|
| 1092 |
+
hold := max(
|
| 1093 |
+
self._ends_with_partial_token(self._buffer, end_tok),
|
| 1094 |
+
self._ends_with_partial_token(self._buffer, tool_start),
|
| 1095 |
+
)
|
| 1096 |
+
) != 0:
|
| 1097 |
+
out_reasoning += self._buffer[:-hold]
|
| 1098 |
+
self._buffer = self._buffer[-hold:]
|
| 1099 |
+
else:
|
| 1100 |
+
out_reasoning += self._buffer
|
| 1101 |
+
self._buffer = ""
|
| 1102 |
+
return StreamingParseResult(
|
| 1103 |
+
normal_text=out_normal, reasoning_text=out_reasoning
|
| 1104 |
+
)
|
| 1105 |
+
|
| 1106 |
+
next_pos = min(p for p in [pos_tool, pos_end] if p != -1)
|
| 1107 |
+
|
| 1108 |
+
if pos_end != -1 and pos_end == next_pos:
|
| 1109 |
+
reasoning_chunk = self._buffer[:pos_end]
|
| 1110 |
+
if self.stream_reasoning:
|
| 1111 |
+
out_reasoning += reasoning_chunk
|
| 1112 |
+
else:
|
| 1113 |
+
self._reasoning_acc += reasoning_chunk
|
| 1114 |
+
out_reasoning += self._reasoning_acc
|
| 1115 |
+
self._reasoning_acc = ""
|
| 1116 |
+
self._buffer = self._buffer[pos_end + len(end_tok) :]
|
| 1117 |
+
self._in_reasoning = False
|
| 1118 |
+
continue
|
| 1119 |
+
|
| 1120 |
+
reasoning_chunk = self._buffer[:pos_tool]
|
| 1121 |
+
if self.stream_reasoning:
|
| 1122 |
+
out_reasoning += reasoning_chunk
|
| 1123 |
+
else:
|
| 1124 |
+
self._reasoning_acc += reasoning_chunk
|
| 1125 |
+
self._buffer = self._buffer[pos_tool:]
|
| 1126 |
+
self._in_inner_tool = True
|
| 1127 |
+
continue
|
| 1128 |
+
|
| 1129 |
+
|
| 1130 |
+
class CohereCommand4Detector(BaseReasoningFormatDetector):
|
| 1131 |
+
"""Detector for Cohere Command4 / Command-A family (incl. cohere2_moe and
|
| 1132 |
+
cohere2_vision Command-A-Plus).
|
| 1133 |
+
|
| 1134 |
+
Generated format (the assistant prefix in the chat template already emits
|
| 1135 |
+
``<|START_THINKING|>`` when ``reasoning=True``, so the *generated* text
|
| 1136 |
+
typically begins inside the thinking block):
|
| 1137 |
+
|
| 1138 |
+
thinking_content<|END_THINKING|><|START_TEXT|>final_answer<|END_TEXT|>
|
| 1139 |
+
|
| 1140 |
+
When ``reasoning=False`` the chat template emits both START/END_THINKING
|
| 1141 |
+
in the prefix and the generated text is just::
|
| 1142 |
+
|
| 1143 |
+
<|START_TEXT|>final_answer<|END_TEXT|>
|
| 1144 |
+
|
| 1145 |
+
This detector returns:
|
| 1146 |
+
- ``reasoning_text`` = the thinking block (between START_THINKING and
|
| 1147 |
+
END_THINKING, with the START tag stripped if the model echoed it).
|
| 1148 |
+
- ``normal_text`` = the content between ``<|START_TEXT|>`` and
|
| 1149 |
+
``<|END_TEXT|>``, with both markers stripped. If no ``<|START_TEXT|>``
|
| 1150 |
+
appears (the model exhausted max_new_tokens still inside thinking),
|
| 1151 |
+
``normal_text`` is the empty string.
|
| 1152 |
+
|
| 1153 |
+
Matches the public token names from the model's
|
| 1154 |
+
``special_tokens_map.json`` (``<|START_THINKING|>`` etc.).
|
| 1155 |
+
"""
|
| 1156 |
+
|
| 1157 |
+
TEXT_START_TOKEN = "<|START_TEXT|>"
|
| 1158 |
+
TEXT_END_TOKEN = "<|END_TEXT|>"
|
| 1159 |
+
# When the model decides to call tools instead of producing a final text
|
| 1160 |
+
# block, it emits an action block instead of a text block. The reasoning
|
| 1161 |
+
# parser must leave that block intact so the downstream tool-call parser
|
| 1162 |
+
# can pick it up.
|
| 1163 |
+
ACTION_START_TOKEN = "<|START_ACTION|>"
|
| 1164 |
+
|
| 1165 |
+
def __init__(
|
| 1166 |
+
self,
|
| 1167 |
+
stream_reasoning: bool = True,
|
| 1168 |
+
force_reasoning: bool = True,
|
| 1169 |
+
continue_final_message: bool = False,
|
| 1170 |
+
previous_content: str = "",
|
| 1171 |
+
force_nonempty_content: bool = False,
|
| 1172 |
+
):
|
| 1173 |
+
# The chat template puts <|START_THINKING|> in the assistant prefix
|
| 1174 |
+
# when reasoning is enabled, so the *generated* text usually starts
|
| 1175 |
+
# already inside thinking. ``force_reasoning=True`` makes the base
|
| 1176 |
+
# detector treat the leading bytes as reasoning even though the
|
| 1177 |
+
# generated stream typically does not echo <|START_THINKING|>.
|
| 1178 |
+
super().__init__(
|
| 1179 |
+
think_start_token="<|START_THINKING|>",
|
| 1180 |
+
think_end_token="<|END_THINKING|>",
|
| 1181 |
+
force_reasoning=force_reasoning,
|
| 1182 |
+
stream_reasoning=stream_reasoning,
|
| 1183 |
+
continue_final_message=continue_final_message,
|
| 1184 |
+
previous_content=previous_content,
|
| 1185 |
+
force_nonempty_content=force_nonempty_content,
|
| 1186 |
+
)
|
| 1187 |
+
# Streaming state machine. The model emits, in order:
|
| 1188 |
+
# 1. reasoning (between START_THINKING [in prefix] and END_THINKING)
|
| 1189 |
+
# 2. either ``<|START_TEXT|>...<|END_TEXT|>`` (final answer) or
|
| 1190 |
+
# ``<|START_ACTION|>...<|END_ACTION|>`` (tool calls) -- never both.
|
| 1191 |
+
# When ``reasoning=False`` the chat template emits both START/END
|
| 1192 |
+
# thinking in the prefix and step 1 is empty; the generated stream
|
| 1193 |
+
# then starts directly with the text or action block.
|
| 1194 |
+
self._reasoning_done = False
|
| 1195 |
+
self._saw_text_start = False
|
| 1196 |
+
self._saw_text_end = False
|
| 1197 |
+
self._in_action_mode = False
|
| 1198 |
+
|
| 1199 |
+
@classmethod
|
| 1200 |
+
def _strip_text_markers(cls, raw: str) -> str:
|
| 1201 |
+
"""Extract the substring between ``<|START_TEXT|>`` and
|
| 1202 |
+
``<|END_TEXT|>``. If ``<|START_TEXT|>`` is absent but a
|
| 1203 |
+
``<|START_ACTION|>`` block is present, the model produced a tool
|
| 1204 |
+
call instead of a text answer -- return the raw text untouched so
|
| 1205 |
+
the downstream tool-call parser can pick up the action block. If
|
| 1206 |
+
neither marker is present (ran out of tokens still inside
|
| 1207 |
+
thinking) return ``""``. If ``<|END_TEXT|>`` is absent (stop token
|
| 1208 |
+
or max_new_tokens cut the stream off inside the text block) return
|
| 1209 |
+
everything after ``<|START_TEXT|>``.
|
| 1210 |
+
"""
|
| 1211 |
+
if not raw:
|
| 1212 |
+
return ""
|
| 1213 |
+
s = raw.find(cls.TEXT_START_TOKEN)
|
| 1214 |
+
if s == -1:
|
| 1215 |
+
if cls.ACTION_START_TOKEN in raw:
|
| 1216 |
+
return raw
|
| 1217 |
+
return ""
|
| 1218 |
+
s += len(cls.TEXT_START_TOKEN)
|
| 1219 |
+
tail = raw[s:]
|
| 1220 |
+
e = tail.find(cls.TEXT_END_TOKEN)
|
| 1221 |
+
if e == -1:
|
| 1222 |
+
return tail
|
| 1223 |
+
return tail[:e]
|
| 1224 |
+
|
| 1225 |
+
def detect_and_parse(self, text: str) -> StreamingParseResult:
|
| 1226 |
+
# Direct parse: split on the (single) ``<|END_THINKING|>`` token if
|
| 1227 |
+
# present. Anything before is reasoning, anything after is the
|
| 1228 |
+
# final-text block. If no END_THINKING but a START_TEXT exists,
|
| 1229 |
+
# we're in the reasoning=False case (chat template emitted both
|
| 1230 |
+
# START/END thinking in the prefix; the model only generated the
|
| 1231 |
+
# text block). Otherwise the model exhausted tokens still thinking
|
| 1232 |
+
# and ``normal_text`` ends up empty -- matching the convention of
|
| 1233 |
+
# the other detectors in this module (DeepSeekR1, Qwen3, ...). The
|
| 1234 |
+
# empty content is propagated as ``message.content = None`` by
|
| 1235 |
+
# serving_chat, and downstream code is expected to treat that as
|
| 1236 |
+
# "no answer" rather than falling back to ``reasoning_content``.
|
| 1237 |
+
end_think_idx = text.find(self.think_end_token)
|
| 1238 |
+
text_start_idx = text.find(self.TEXT_START_TOKEN)
|
| 1239 |
+
action_start_idx = text.find(self.ACTION_START_TOKEN)
|
| 1240 |
+
if end_think_idx != -1:
|
| 1241 |
+
reasoning = text[:end_think_idx]
|
| 1242 |
+
rest = text[end_think_idx + len(self.think_end_token) :]
|
| 1243 |
+
elif text_start_idx != -1:
|
| 1244 |
+
reasoning = text[:text_start_idx]
|
| 1245 |
+
rest = text[text_start_idx:]
|
| 1246 |
+
elif action_start_idx != -1:
|
| 1247 |
+
# reasoning=False + tool call: chat template emitted both
|
| 1248 |
+
# START/END thinking in the prefix, the model only generated
|
| 1249 |
+
# an action block. Treat the prefix before the action block as
|
| 1250 |
+
# (probably empty) reasoning so the action block reaches the
|
| 1251 |
+
# tool-call parser intact.
|
| 1252 |
+
reasoning = text[:action_start_idx]
|
| 1253 |
+
rest = text[action_start_idx:]
|
| 1254 |
+
else:
|
| 1255 |
+
reasoning = text
|
| 1256 |
+
rest = ""
|
| 1257 |
+
|
| 1258 |
+
# Some checkpoints echo the START_THINKING token even though the
|
| 1259 |
+
# chat template put it in the prefix; drop it if so.
|
| 1260 |
+
think_start_text = self.think_start_token + self.think_start_self_label
|
| 1261 |
+
if reasoning.startswith(think_start_text):
|
| 1262 |
+
reasoning = reasoning[len(think_start_text) :]
|
| 1263 |
+
|
| 1264 |
+
return self._maybe_apply_force_nonempty_content(
|
| 1265 |
+
StreamingParseResult(
|
| 1266 |
+
normal_text=self._strip_text_markers(rest),
|
| 1267 |
+
reasoning_text=reasoning,
|
| 1268 |
+
)
|
| 1269 |
+
)
|
| 1270 |
+
|
| 1271 |
+
def parse_streaming_increment(self, new_text: str) -> StreamingParseResult:
|
| 1272 |
+
"""Streaming parse. Custom state machine -- we don't reuse the base
|
| 1273 |
+
class because Cohere's "reasoning=False" path (the model emits no
|
| 1274 |
+
``<|END_THINKING|>``, just goes straight to a text or action block)
|
| 1275 |
+
is fundamentally incompatible with the base detector's
|
| 1276 |
+
``force_reasoning`` semantics."""
|
| 1277 |
+
self._buffer += new_text
|
| 1278 |
+
buf = self._buffer
|
| 1279 |
+
|
| 1280 |
+
if not self._reasoning_done:
|
| 1281 |
+
# Look for any marker that ends reasoning: an explicit
|
| 1282 |
+
# END_THINKING, or an implicit transition via the start of the
|
| 1283 |
+
# final-text or action block (reasoning=False case).
|
| 1284 |
+
markers = (
|
| 1285 |
+
(self.think_end_token, "think_end"),
|
| 1286 |
+
(self.TEXT_START_TOKEN, "text"),
|
| 1287 |
+
(self.ACTION_START_TOKEN, "action"),
|
| 1288 |
+
)
|
| 1289 |
+
first_pos = None
|
| 1290 |
+
first_marker = None
|
| 1291 |
+
first_kind = None
|
| 1292 |
+
for marker_text, kind in markers:
|
| 1293 |
+
p = buf.find(marker_text)
|
| 1294 |
+
if p != -1 and (first_pos is None or p < first_pos):
|
| 1295 |
+
first_pos, first_marker, first_kind = p, marker_text, kind
|
| 1296 |
+
if first_pos is None:
|
| 1297 |
+
# No marker seen yet. Stream the reasoning prefix, but keep
|
| 1298 |
+
# enough tail in the buffer to recognise a marker split
|
| 1299 |
+
# across chunk boundaries.
|
| 1300 |
+
if not self.stream_reasoning:
|
| 1301 |
+
return StreamingParseResult()
|
| 1302 |
+
max_keep = max(len(m) for m, _ in markers) - 1
|
| 1303 |
+
if len(buf) > max_keep:
|
| 1304 |
+
head = buf[:-max_keep]
|
| 1305 |
+
self._buffer = buf[-max_keep:]
|
| 1306 |
+
return StreamingParseResult(reasoning_text=head)
|
| 1307 |
+
return StreamingParseResult()
|
| 1308 |
+
|
| 1309 |
+
reasoning_chunk = buf[:first_pos]
|
| 1310 |
+
if first_kind == "think_end":
|
| 1311 |
+
self._buffer = buf[first_pos + len(first_marker) :]
|
| 1312 |
+
else:
|
| 1313 |
+
# Implicit reasoning-end: leave the start-of-block marker in
|
| 1314 |
+
# the buffer for the post-thinking branch below to consume.
|
| 1315 |
+
self._buffer = buf[first_pos:]
|
| 1316 |
+
self._reasoning_done = True
|
| 1317 |
+
if reasoning_chunk:
|
| 1318 |
+
return StreamingParseResult(reasoning_text=reasoning_chunk)
|
| 1319 |
+
buf = self._buffer
|
| 1320 |
+
|
| 1321 |
+
# Reasoning is closed. Decide between text-stripping and
|
| 1322 |
+
# action-passthrough on first sight of a marker.
|
| 1323 |
+
if self._in_action_mode:
|
| 1324 |
+
if not buf:
|
| 1325 |
+
return StreamingParseResult()
|
| 1326 |
+
self._buffer = ""
|
| 1327 |
+
return StreamingParseResult(normal_text=buf)
|
| 1328 |
+
|
| 1329 |
+
if not self._saw_text_start:
|
| 1330 |
+
s_text = buf.find(self.TEXT_START_TOKEN)
|
| 1331 |
+
s_action = buf.find(self.ACTION_START_TOKEN)
|
| 1332 |
+
picks = [
|
| 1333 |
+
(p, k) for p, k in ((s_text, "text"), (s_action, "action")) if p != -1
|
| 1334 |
+
]
|
| 1335 |
+
if not picks:
|
| 1336 |
+
max_keep = (
|
| 1337 |
+
max(len(self.TEXT_START_TOKEN), len(self.ACTION_START_TOKEN)) - 1
|
| 1338 |
+
)
|
| 1339 |
+
if len(buf) > max_keep:
|
| 1340 |
+
self._buffer = buf[-max_keep:]
|
| 1341 |
+
return StreamingParseResult()
|
| 1342 |
+
picks.sort()
|
| 1343 |
+
first_pos, first_kind = picks[0]
|
| 1344 |
+
if first_kind == "action":
|
| 1345 |
+
self._in_action_mode = True
|
| 1346 |
+
out_normal = buf[first_pos:]
|
| 1347 |
+
self._buffer = ""
|
| 1348 |
+
return StreamingParseResult(normal_text=out_normal)
|
| 1349 |
+
# Found <|START_TEXT|>. Drop everything up to and including the
|
| 1350 |
+
# marker -- text content streams next.
|
| 1351 |
+
self._buffer = buf[first_pos + len(self.TEXT_START_TOKEN) :]
|
| 1352 |
+
self._saw_text_start = True
|
| 1353 |
+
buf = self._buffer
|
| 1354 |
+
|
| 1355 |
+
if self._saw_text_start and not self._saw_text_end:
|
| 1356 |
+
e = buf.find(self.TEXT_END_TOKEN)
|
| 1357 |
+
if e == -1:
|
| 1358 |
+
# Emit everything except a possible partial END_TEXT tail.
|
| 1359 |
+
keep = len(self.TEXT_END_TOKEN) - 1
|
| 1360 |
+
if len(buf) > keep:
|
| 1361 |
+
out_normal = buf[:-keep]
|
| 1362 |
+
self._buffer = buf[-keep:]
|
| 1363 |
+
return StreamingParseResult(normal_text=out_normal)
|
| 1364 |
+
return StreamingParseResult()
|
| 1365 |
+
out_normal = buf[:e]
|
| 1366 |
+
self._buffer = buf[e + len(self.TEXT_END_TOKEN) :]
|
| 1367 |
+
self._saw_text_end = True
|
| 1368 |
+
return StreamingParseResult(normal_text=out_normal)
|
| 1369 |
+
|
| 1370 |
+
return StreamingParseResult()
|
| 1371 |
+
|
| 1372 |
+
|
| 1373 |
+
class ReasoningParser:
|
| 1374 |
+
"""
|
| 1375 |
+
Parser that handles both streaming and non-streaming scenarios for extracting
|
| 1376 |
+
reasoning content from model outputs.
|
| 1377 |
+
|
| 1378 |
+
Args:
|
| 1379 |
+
model_type (str): Type of model to parse reasoning from
|
| 1380 |
+
stream_reasoning (bool): If False, accumulates reasoning content until complete.
|
| 1381 |
+
If True, streams reasoning content as it arrives.
|
| 1382 |
+
"""
|
| 1383 |
+
|
| 1384 |
+
DetectorMap: Dict[str, Type[BaseReasoningFormatDetector]] = {
|
| 1385 |
+
"apertus2509": Apertus2509Detector,
|
| 1386 |
+
"deepseek-r1": DeepSeekR1Detector,
|
| 1387 |
+
"deepseek-v3": _DeepSeekV3Detector,
|
| 1388 |
+
"agnes": _DeepSeekV3Detector, # alias: Agnes uses the deepseek-v4 format
|
| 1389 |
+
"deepseek-v4": _DeepSeekV3Detector,
|
| 1390 |
+
"glm45": Glm45Detector,
|
| 1391 |
+
"hunyuan": HunyuanDetector,
|
| 1392 |
+
"gpt-oss": GptOssDetector,
|
| 1393 |
+
"kimi": KimiDetector,
|
| 1394 |
+
"kimi_k2": KimiK2Detector,
|
| 1395 |
+
"mimo": _MimoDetector,
|
| 1396 |
+
"poolside_v1": _PoolsideV1Detector,
|
| 1397 |
+
"qwen3": Qwen3Detector,
|
| 1398 |
+
"qwen3-thinking": Qwen3Detector,
|
| 1399 |
+
"minimax": Qwen3Detector,
|
| 1400 |
+
"minimax-append-think": MiniMaxAppendThinkDetector,
|
| 1401 |
+
"minimax-m3": MiniMaxM3Detector,
|
| 1402 |
+
"step3": DeepSeekR1Detector,
|
| 1403 |
+
"step3p5": DeepSeekR1Detector,
|
| 1404 |
+
"mistral": MistralDetector,
|
| 1405 |
+
"nemotron_3": Nemotron3Detector,
|
| 1406 |
+
"interns1": Qwen3Detector,
|
| 1407 |
+
"gemma4": Gemma4Detector,
|
| 1408 |
+
"inkling": InklingDetector,
|
| 1409 |
+
"cohere_command4": CohereCommand4Detector,
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
def __init__(
|
| 1413 |
+
self,
|
| 1414 |
+
model_type: Optional[str] = None,
|
| 1415 |
+
stream_reasoning: bool = True,
|
| 1416 |
+
force_reasoning: Optional[bool] = None,
|
| 1417 |
+
request: ChatCompletionRequest = None,
|
| 1418 |
+
tokenizer=None,
|
| 1419 |
+
):
|
| 1420 |
+
if not model_type:
|
| 1421 |
+
raise ValueError("Model type must be specified")
|
| 1422 |
+
|
| 1423 |
+
detector_class = self.DetectorMap.get(model_type.lower())
|
| 1424 |
+
if not detector_class:
|
| 1425 |
+
raise ValueError(f"Unsupported model type: {model_type}")
|
| 1426 |
+
|
| 1427 |
+
chat_template_kwargs = getattr(request, "chat_template_kwargs", None) or {}
|
| 1428 |
+
|
| 1429 |
+
# Special cases where we override force_reasoning
|
| 1430 |
+
if model_type.lower() in {
|
| 1431 |
+
"qwen3-thinking",
|
| 1432 |
+
"gpt-oss",
|
| 1433 |
+
"minimax",
|
| 1434 |
+
}:
|
| 1435 |
+
force_reasoning = True
|
| 1436 |
+
|
| 1437 |
+
# M3 consumes the <mm:think> start tag only for thinking_mode=enabled
|
| 1438 |
+
# (absent from output → must force); mirror serving_chat's M3 branch.
|
| 1439 |
+
if model_type.lower() == "minimax-m3" and force_reasoning is None:
|
| 1440 |
+
force_reasoning = chat_template_kwargs.get("thinking_mode") == "enabled"
|
| 1441 |
+
|
| 1442 |
+
# Only pass force_reasoning if explicitly set, let detectors use their defaults
|
| 1443 |
+
kwargs = {"stream_reasoning": stream_reasoning}
|
| 1444 |
+
if force_reasoning is not None:
|
| 1445 |
+
kwargs["force_reasoning"] = force_reasoning
|
| 1446 |
+
|
| 1447 |
+
if (
|
| 1448 |
+
request is not None
|
| 1449 |
+
and isinstance(request, ChatCompletionRequest)
|
| 1450 |
+
and request.continue_final_message
|
| 1451 |
+
and request.messages[-1].role == "assistant"
|
| 1452 |
+
):
|
| 1453 |
+
kwargs["continue_final_message"] = True
|
| 1454 |
+
kwargs["previous_content"] = request.messages[-1].content
|
| 1455 |
+
|
| 1456 |
+
if chat_template_kwargs.get("force_nonempty_content") is True:
|
| 1457 |
+
kwargs["force_nonempty_content"] = True
|
| 1458 |
+
|
| 1459 |
+
if tokenizer is not None:
|
| 1460 |
+
sig = inspect.signature(detector_class)
|
| 1461 |
+
if "tokenizer" in sig.parameters:
|
| 1462 |
+
kwargs["tokenizer"] = tokenizer
|
| 1463 |
+
|
| 1464 |
+
self.detector = detector_class(**kwargs)
|
| 1465 |
+
|
| 1466 |
+
def parse_non_stream(self, full_text: str) -> Tuple[Optional[str], Optional[str]]:
|
| 1467 |
+
"""Non-streaming call: one-time parsing"""
|
| 1468 |
+
ret = self.detector.detect_and_parse(full_text)
|
| 1469 |
+
return ret.reasoning_text, ret.normal_text
|
| 1470 |
+
|
| 1471 |
+
def parse_non_stream_blocks(self, full_text: str) -> list[dict]:
|
| 1472 |
+
"""Non-streaming call: return an ordered sequence of reasoning/text blocks"""
|
| 1473 |
+
if hasattr(self.detector, "detect_and_parse_block_sequence"):
|
| 1474 |
+
seq = self.detector.detect_and_parse_block_sequence(full_text)
|
| 1475 |
+
return [{"type": k, "text": t} for k, t in seq]
|
| 1476 |
+
|
| 1477 |
+
ret = self.detector.detect_and_parse(full_text)
|
| 1478 |
+
blocks: list[dict] = []
|
| 1479 |
+
if ret.reasoning_text:
|
| 1480 |
+
blocks.append({"type": "reasoning", "text": ret.reasoning_text})
|
| 1481 |
+
blocks.append({"type": "text", "text": ret.normal_text or ""})
|
| 1482 |
+
return blocks
|
| 1483 |
+
|
| 1484 |
+
def parse_stream_chunk(
|
| 1485 |
+
self, chunk_text: str
|
| 1486 |
+
) -> Tuple[Optional[str], Optional[str]]:
|
| 1487 |
+
"""Streaming call: incremental parsing"""
|
| 1488 |
+
ret = self.detector.parse_streaming_increment(chunk_text)
|
| 1489 |
+
return ret.reasoning_text, ret.normal_text
|
| 1490 |
+
|
| 1491 |
+
def parse_stream_end(self) -> Tuple[Optional[str], Optional[str]]:
|
| 1492 |
+
"""Streaming call: flush any detector-specific buffered state once
|
| 1493 |
+
the stream ends."""
|
| 1494 |
+
ret = self.detector.finish()
|
| 1495 |
+
return ret.reasoning_text, ret.normal_text
|
sglang_patch/srt/parser/template_detection.py
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2026 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
"""
|
| 15 |
+
Template detection utilities for auto-detecting reasoning and tool-call parsers.
|
| 16 |
+
|
| 17 |
+
Provides rule-based detection of reasoning mode, reasoning parser, and tool-call
|
| 18 |
+
parser from chat templates and tokenizer vocabularies.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import logging
|
| 22 |
+
import os
|
| 23 |
+
import re
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
from typing import Callable, Optional, Tuple
|
| 26 |
+
|
| 27 |
+
import jinja2
|
| 28 |
+
import jinja2.ext
|
| 29 |
+
import jinja2.sandbox
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class TemplateDetectionContext:
|
| 36 |
+
template: str
|
| 37 |
+
reasoning_config: Optional["ReasoningToggleConfig"]
|
| 38 |
+
force_reasoning: bool
|
| 39 |
+
vocab: set[str]
|
| 40 |
+
|
| 41 |
+
def has_text(self, needle: str) -> bool:
|
| 42 |
+
return needle in self.template
|
| 43 |
+
|
| 44 |
+
def has_vocab(self, token: str) -> bool:
|
| 45 |
+
return token in self.vocab
|
| 46 |
+
|
| 47 |
+
def has_pattern(self, pattern: str, flags: int = 0) -> bool:
|
| 48 |
+
return re.search(pattern, self.template, flags) is not None
|
| 49 |
+
|
| 50 |
+
def has_vocab_pattern(self, pattern: str) -> bool:
|
| 51 |
+
compiled = re.compile(pattern)
|
| 52 |
+
return any(isinstance(tok, str) and compiled.search(tok) for tok in self.vocab)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass(frozen=True)
|
| 56 |
+
class DetectionRule:
|
| 57 |
+
name: str
|
| 58 |
+
value: object
|
| 59 |
+
predicate: Callable[[TemplateDetectionContext], bool]
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@dataclass(frozen=True)
|
| 63 |
+
class ReasoningToggleConfig:
|
| 64 |
+
toggle_param: Optional[str] = None
|
| 65 |
+
default_enabled: Optional[bool] = None
|
| 66 |
+
special_case: Optional[str] = None
|
| 67 |
+
effort_kwarg: Optional[str] = None
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def always_on(self) -> bool:
|
| 71 |
+
return self.special_case == "always"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------------------------------------
|
| 75 |
+
# Reasoning mode rules (detect toggle config from template)
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
REASONING_MODE_RULES = (
|
| 79 |
+
DetectionRule(
|
| 80 |
+
name="gpt_oss_channel_markers",
|
| 81 |
+
value=ReasoningToggleConfig(special_case="always"),
|
| 82 |
+
predicate=lambda ctx: ctx.has_text("<|channel|>"),
|
| 83 |
+
),
|
| 84 |
+
DetectionRule(
|
| 85 |
+
name="force_reasoning_pattern",
|
| 86 |
+
value=ReasoningToggleConfig(special_case="always"),
|
| 87 |
+
predicate=lambda ctx: ctx.has_pattern(r"<\|im_start\|>assistant\\n<think>\\n")
|
| 88 |
+
and not ctx.has_text("enable_thinking")
|
| 89 |
+
and not ctx.has_text("thinking"),
|
| 90 |
+
),
|
| 91 |
+
DetectionRule(
|
| 92 |
+
name="mistral_reasoning_effort",
|
| 93 |
+
value=ReasoningToggleConfig(special_case="mistral"),
|
| 94 |
+
predicate=lambda ctx: ctx.has_text("reasoning_effort")
|
| 95 |
+
and ctx.has_text("[THINK]"),
|
| 96 |
+
),
|
| 97 |
+
DetectionRule(
|
| 98 |
+
name="explicit_enable_thinking_default_false",
|
| 99 |
+
value=ReasoningToggleConfig(
|
| 100 |
+
toggle_param="enable_thinking", default_enabled=False
|
| 101 |
+
),
|
| 102 |
+
predicate=lambda ctx: ctx.has_pattern(
|
| 103 |
+
r"{%\s*if\s+not\s+enable_thinking\s+is\s+defined\s*%}.*?"
|
| 104 |
+
r"{%\s*set\s+enable_thinking\s*=\s*(?:false|False)\s*%}",
|
| 105 |
+
re.DOTALL,
|
| 106 |
+
),
|
| 107 |
+
),
|
| 108 |
+
DetectionRule(
|
| 109 |
+
name="nemotron_3_super_low_effort",
|
| 110 |
+
value=ReasoningToggleConfig(
|
| 111 |
+
toggle_param="enable_thinking",
|
| 112 |
+
default_enabled=True,
|
| 113 |
+
effort_kwarg="low_effort",
|
| 114 |
+
),
|
| 115 |
+
predicate=lambda ctx: ctx.has_text("low_effort")
|
| 116 |
+
and ctx.has_text("truncate_history_thinking"),
|
| 117 |
+
),
|
| 118 |
+
DetectionRule(
|
| 119 |
+
name="enable_thinking_default_true",
|
| 120 |
+
value=ReasoningToggleConfig(
|
| 121 |
+
toggle_param="enable_thinking", default_enabled=True
|
| 122 |
+
),
|
| 123 |
+
predicate=lambda ctx: ctx.has_pattern(
|
| 124 |
+
r"{%\s*if\s+not\s+enable_thinking\s+is\s+defined\s*%}.*?"
|
| 125 |
+
r"{%\s*set\s+enable_thinking\s*=\s*(?:true|True)\s*%}",
|
| 126 |
+
re.DOTALL,
|
| 127 |
+
)
|
| 128 |
+
or ctx.has_pattern(
|
| 129 |
+
r"set\s+enable_thinking\s*=\s*enable_thinking\s+if\s+enable_thinking\s+is\s+defined\s+else\s+(?:true|True)"
|
| 130 |
+
)
|
| 131 |
+
or ctx.has_pattern(
|
| 132 |
+
r"enable_thinking\s+is\s+defined\s+and\s+(?:enable_thinking\s+is\s+false|not\s+enable_thinking)"
|
| 133 |
+
)
|
| 134 |
+
or ctx.has_pattern(
|
| 135 |
+
r"enable_thinking\s+is\s+not\s+defined\s+or\s+enable_thinking"
|
| 136 |
+
)
|
| 137 |
+
or ctx.has_pattern(r"namespace\([^)]*enable_thinking\s*=\s*true"),
|
| 138 |
+
),
|
| 139 |
+
DetectionRule(
|
| 140 |
+
name="explicit_thinking_default_false",
|
| 141 |
+
value=ReasoningToggleConfig(toggle_param="thinking", default_enabled=False),
|
| 142 |
+
predicate=lambda ctx: ctx.has_pattern(
|
| 143 |
+
r"{%\s*if\s+not\s+thinking\s+is\s+defined\s*%}.*?"
|
| 144 |
+
r"{%\s*set\s+thinking\s*=\s*(?:false|False)\s*%}",
|
| 145 |
+
re.DOTALL,
|
| 146 |
+
),
|
| 147 |
+
),
|
| 148 |
+
DetectionRule(
|
| 149 |
+
name="thinking_default_true",
|
| 150 |
+
value=ReasoningToggleConfig(toggle_param="thinking", default_enabled=True),
|
| 151 |
+
predicate=lambda ctx: ctx.has_pattern(
|
| 152 |
+
r"{%\s*if\s+not\s+thinking\s+is\s+defined\s*%}.*?"
|
| 153 |
+
r"{%\s*set\s+thinking\s*=\s*(?:true|True)\s*%}",
|
| 154 |
+
re.DOTALL,
|
| 155 |
+
)
|
| 156 |
+
or ctx.has_pattern(
|
| 157 |
+
r"set\s+thinking\s*=\s*thinking\s+if\s+thinking\s+is\s+defined\s+else\s+(?:true|True)"
|
| 158 |
+
)
|
| 159 |
+
or ctx.has_pattern(
|
| 160 |
+
r"thinking\s+is\s+defined\s+and\s+(?:thinking\s+is\s+false|not\s+thinking)"
|
| 161 |
+
)
|
| 162 |
+
or ctx.has_pattern(r"thinking\s+is\s+not\s+defined\s+or\s+thinking")
|
| 163 |
+
or ctx.has_pattern(r"namespace\([^)]*thinking\s*=\s*true"),
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
# Shared predicates for model-family detection
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _is_apertus2509(ctx):
|
| 174 |
+
return ctx.has_vocab("<|inner_prefix|>")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _is_gemma4(ctx):
|
| 178 |
+
return ctx.has_text("<|channel>")
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _is_kimi(ctx):
|
| 182 |
+
return ctx.has_text("◁think▷")
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _is_interns1(ctx):
|
| 186 |
+
return ctx.has_text("default_thinking_sys") and ctx.reasoning_config == (
|
| 187 |
+
ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True)
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _is_mistral(ctx):
|
| 192 |
+
return (
|
| 193 |
+
ctx.reasoning_config is not None
|
| 194 |
+
and ctx.reasoning_config.special_case == "mistral"
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _is_gpt_oss(ctx):
|
| 199 |
+
return ctx.has_text("<|channel|>")
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def _is_kimi_k2(ctx):
|
| 203 |
+
return ctx.has_vocab("<|tool_calls_section_begin|>")
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _is_nemotron_3(ctx):
|
| 207 |
+
return ctx.has_text("truncate_history_thinking") and (
|
| 208 |
+
ctx.reasoning_config is not None
|
| 209 |
+
and ctx.reasoning_config.toggle_param == "enable_thinking"
|
| 210 |
+
and ctx.reasoning_config.default_enabled is True
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _is_glm45(ctx):
|
| 215 |
+
return (
|
| 216 |
+
(
|
| 217 |
+
ctx.has_text("[gMASK]<sop>")
|
| 218 |
+
or ctx.has_pattern(r"(?<!<)/nothink")
|
| 219 |
+
or ctx.has_pattern(r"(?<!<)/think")
|
| 220 |
+
)
|
| 221 |
+
and ctx.has_vocab("<tool_call>")
|
| 222 |
+
and ctx.reasoning_config
|
| 223 |
+
== ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=True)
|
| 224 |
+
and (ctx.has_vocab("<|user|>") or ctx.has_vocab("<|endoftext|>"))
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _is_glm47(ctx):
|
| 229 |
+
return _is_glm45(ctx) and ctx.has_pattern(
|
| 230 |
+
r"\{\{[-\s]*['\"]<tool_call>['\"]\s*\+\s*tc\.name"
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _is_xml_kv_tool_call(ctx):
|
| 235 |
+
# Structural signature for the GLM-4.5 / GLM-4.6 style tool-call format
|
| 236 |
+
# (`<tool_call>name<arg_key>k</arg_key>\n<arg_value>v</arg_value>...</tool_call>`).
|
| 237 |
+
# Matches any model whose tokenizer carries `<arg_key>` and `<arg_value>` as
|
| 238 |
+
# added tokens — e.g., inclusionAI/Ring-2.6, which borrows GLM's tool-call
|
| 239 |
+
# format but doesn't share the `[gMASK]<sop>` / `enable_thinking` family
|
| 240 |
+
# signature checked by `_is_glm45`.
|
| 241 |
+
return ctx.has_vocab("<arg_key>") and ctx.has_vocab("<arg_value>")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _is_deepseek_v31(ctx):
|
| 245 |
+
return ctx.has_text("<|tool▁calls▁begin|>") and ctx.has_text("<|tool▁sep|>")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _is_deepseek_v32(ctx):
|
| 249 |
+
return ctx.has_text("<|DSML|function_calls>")
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _is_deepseek_v4(ctx):
|
| 253 |
+
return ctx.has_text("<|DSML|tool_calls>")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def _is_hunyuan(ctx):
|
| 257 |
+
# The shipping Hy3 tokenizer appends a shared suffix to each special token
|
| 258 |
+
# (e.g. ``<tool_calls:opensource>``), so match the bare or suffixed form.
|
| 259 |
+
tc = ctx.has_text("<tool_calls>") or ctx.has_vocab_pattern(
|
| 260 |
+
r"^<tool_calls(?::[^>]+)?>$"
|
| 261 |
+
)
|
| 262 |
+
sep = ctx.has_text("<tool_sep>") or ctx.has_vocab_pattern(
|
| 263 |
+
r"^<tool_sep(?::[^>]+)?>$"
|
| 264 |
+
)
|
| 265 |
+
return (tc and sep) or (
|
| 266 |
+
ctx.has_text("reasoning_effort") and ctx.has_text("interleaved_thinking")
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def _is_poolside_v1(ctx):
|
| 271 |
+
has_poolside_tool_format = (
|
| 272 |
+
ctx.has_text("unescaped XML-like object")
|
| 273 |
+
and ctx.has_text("<tool_call>function-name")
|
| 274 |
+
and ctx.has_text("<arg_key>")
|
| 275 |
+
and ctx.has_text("<arg_value>")
|
| 276 |
+
)
|
| 277 |
+
return has_poolside_tool_format or (
|
| 278 |
+
ctx.reasoning_config
|
| 279 |
+
== ReasoningToggleConfig(toggle_param="enable_thinking", default_enabled=False)
|
| 280 |
+
and not _is_hunyuan(ctx)
|
| 281 |
+
and (ctx.has_text("<arg_key>") or ctx.has_vocab("<arg_key>"))
|
| 282 |
+
and (ctx.has_text("<arg_value>") or ctx.has_vocab("<arg_value>"))
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _is_mimo(ctx):
|
| 287 |
+
return ctx.reasoning_config == ReasoningToggleConfig(
|
| 288 |
+
toggle_param="enable_thinking", default_enabled=False
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def _is_minimax(ctx):
|
| 293 |
+
return ctx.has_text("<minimax:tool_call>")
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def _is_minimax_m3(ctx):
|
| 297 |
+
return ctx.has_text("<mm:think>") or ctx.has_text("]<]minimax[>[")
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _is_minicpm5(ctx):
|
| 301 |
+
if ctx.has_vocab("<function") and ctx.has_vocab("<param"):
|
| 302 |
+
return True
|
| 303 |
+
return ctx.has_pattern(r"<function\s+name=") and ctx.has_pattern(r"<param\s+name=")
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def _is_lfm2(ctx):
|
| 307 |
+
return (
|
| 308 |
+
ctx.has_text("<|tool_call_start|>") or ctx.has_vocab("<|tool_call_start|>")
|
| 309 |
+
) and (ctx.has_text("<|tool_call_end|>") or ctx.has_vocab("<|tool_call_end|>"))
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _is_step3p5(ctx):
|
| 313 |
+
return ctx.has_pattern(r"Step-?3(?:\.|p)?[57]", re.IGNORECASE) or (
|
| 314 |
+
ctx.has_text("reasoning_effort")
|
| 315 |
+
and ctx.has_text("Reasoning: ")
|
| 316 |
+
and _is_qwen3_coder(ctx)
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def _is_step3(ctx):
|
| 321 |
+
return ctx.has_text("<steptml:invoke") or (
|
| 322 |
+
ctx.has_text("<|tool_calls_begin|>") and ctx.has_text("<|tool_sep|>")
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def _is_qwen3_coder(ctx):
|
| 327 |
+
return ctx.has_text("<function=") and ctx.has_text("<parameter=")
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def _is_qwen3(ctx):
|
| 331 |
+
return ctx.reasoning_config == ReasoningToggleConfig(
|
| 332 |
+
toggle_param="enable_thinking", default_enabled=True
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def _is_deepseek_v3(ctx):
|
| 337 |
+
return ctx.reasoning_config == ReasoningToggleConfig(
|
| 338 |
+
toggle_param="thinking", default_enabled=False
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def _is_deepseek_r1(ctx):
|
| 343 |
+
return ctx.force_reasoning
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _is_deepseek_r1_think_tags(ctx):
|
| 347 |
+
return not _is_lfm2(ctx) and (ctx.has_text("<think>") or ctx.has_text("</think>"))
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
# ---------------------------------------------------------------------------
|
| 351 |
+
# Reasoning parser rules
|
| 352 |
+
# ---------------------------------------------------------------------------
|
| 353 |
+
|
| 354 |
+
REASONING_PARSER_RULES = (
|
| 355 |
+
DetectionRule(name="apertus2509", value="apertus2509", predicate=_is_apertus2509),
|
| 356 |
+
DetectionRule(name="gemma4", value="gemma4", predicate=_is_gemma4),
|
| 357 |
+
DetectionRule(name="kimi", value="kimi", predicate=_is_kimi),
|
| 358 |
+
DetectionRule(name="interns1", value="interns1", predicate=_is_interns1),
|
| 359 |
+
DetectionRule(name="mistral", value="mistral", predicate=_is_mistral),
|
| 360 |
+
DetectionRule(name="gpt_oss", value="gpt-oss", predicate=_is_gpt_oss),
|
| 361 |
+
DetectionRule(name="kimi_k2", value="kimi_k2", predicate=_is_kimi_k2),
|
| 362 |
+
DetectionRule(name="nemotron_3", value="nemotron_3", predicate=_is_nemotron_3),
|
| 363 |
+
DetectionRule(name="glm45", value="glm45", predicate=_is_glm45),
|
| 364 |
+
DetectionRule(name="hunyuan", value="hunyuan", predicate=_is_hunyuan),
|
| 365 |
+
DetectionRule(name="poolside_v1", value="poolside_v1", predicate=_is_poolside_v1),
|
| 366 |
+
DetectionRule(name="mimo", value="mimo", predicate=_is_mimo),
|
| 367 |
+
DetectionRule(name="minimax_m3", value="minimax-m3", predicate=_is_minimax_m3),
|
| 368 |
+
DetectionRule(name="minimax", value="minimax", predicate=_is_minimax),
|
| 369 |
+
DetectionRule(name="step3p5", value="step3p5", predicate=_is_step3p5),
|
| 370 |
+
DetectionRule(name="step3", value="step3", predicate=_is_step3),
|
| 371 |
+
DetectionRule(name="qwen3", value="qwen3", predicate=_is_qwen3),
|
| 372 |
+
DetectionRule(name="deepseek_v4", value="deepseek-v4", predicate=_is_deepseek_v4),
|
| 373 |
+
DetectionRule(name="deepseek_v3", value="deepseek-v3", predicate=_is_deepseek_v3),
|
| 374 |
+
DetectionRule(
|
| 375 |
+
name="deepseek_r1_force", value="deepseek-r1", predicate=_is_deepseek_r1
|
| 376 |
+
),
|
| 377 |
+
DetectionRule(
|
| 378 |
+
name="deepseek_r1_think_tags",
|
| 379 |
+
value="deepseek-r1",
|
| 380 |
+
predicate=_is_deepseek_r1_think_tags,
|
| 381 |
+
),
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# ---------------------------------------------------------------------------
|
| 385 |
+
# Tool-call parser rules (reuse shared predicates, different values)
|
| 386 |
+
# ---------------------------------------------------------------------------
|
| 387 |
+
|
| 388 |
+
TOOL_CALL_PARSER_RULES = (
|
| 389 |
+
DetectionRule(name="apertus2509", value="apertus2509", predicate=_is_apertus2509),
|
| 390 |
+
DetectionRule(name="gemma4", value="gemma4", predicate=_is_gemma4),
|
| 391 |
+
DetectionRule(name="gpt_oss", value="gpt-oss", predicate=_is_gpt_oss),
|
| 392 |
+
DetectionRule(name="kimi_k2", value="kimi_k2", predicate=_is_kimi_k2),
|
| 393 |
+
DetectionRule(name="minimax_m3", value="minimax-m3", predicate=_is_minimax_m3),
|
| 394 |
+
DetectionRule(name="minimax", value="minimax-m2", predicate=_is_minimax),
|
| 395 |
+
DetectionRule(name="interns1", value="interns1", predicate=_is_interns1),
|
| 396 |
+
DetectionRule(name="mistral", value="mistral", predicate=_is_mistral),
|
| 397 |
+
DetectionRule(name="deepseek_v4", value="deepseekv4", predicate=_is_deepseek_v4),
|
| 398 |
+
DetectionRule(name="deepseek_v32", value="deepseekv32", predicate=_is_deepseek_v32),
|
| 399 |
+
DetectionRule(name="deepseek_v31", value="deepseekv31", predicate=_is_deepseek_v31),
|
| 400 |
+
DetectionRule(name="lfm2", value="lfm2", predicate=_is_lfm2),
|
| 401 |
+
DetectionRule(name="glm47", value="glm47", predicate=_is_glm47),
|
| 402 |
+
DetectionRule(name="glm45", value="glm45", predicate=_is_glm45),
|
| 403 |
+
DetectionRule(name="minicpm5", value="minicpm5", predicate=_is_minicpm5),
|
| 404 |
+
DetectionRule(name="hunyuan", value="hunyuan", predicate=_is_hunyuan),
|
| 405 |
+
DetectionRule(name="poolside_v1", value="poolside_v1", predicate=_is_poolside_v1),
|
| 406 |
+
DetectionRule(name="step3p5", value="step3p5", predicate=_is_step3p5),
|
| 407 |
+
DetectionRule(name="step3", value="step3", predicate=_is_step3),
|
| 408 |
+
DetectionRule(
|
| 409 |
+
name="xml_kv_tool_call", value="glm45", predicate=_is_xml_kv_tool_call
|
| 410 |
+
),
|
| 411 |
+
DetectionRule(name="mimo", value="mimo", predicate=_is_mimo),
|
| 412 |
+
DetectionRule(name="qwen3_coder", value="qwen3_coder", predicate=_is_qwen3_coder),
|
| 413 |
+
DetectionRule(name="qwen", value="qwen", predicate=_is_qwen3),
|
| 414 |
+
DetectionRule(name="deepseek_v3", value="deepseekv3", predicate=_is_deepseek_v3),
|
| 415 |
+
DetectionRule(name="deepseek_r1", value="deepseekv3", predicate=_is_deepseek_r1),
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ---------------------------------------------------------------------------
|
| 420 |
+
# Detection functions
|
| 421 |
+
# ---------------------------------------------------------------------------
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def build_detection_context(
|
| 425 |
+
template: Optional[str],
|
| 426 |
+
tokenizer,
|
| 427 |
+
reasoning_config: Optional[ReasoningToggleConfig] = None,
|
| 428 |
+
force_reasoning: bool = False,
|
| 429 |
+
) -> Optional[TemplateDetectionContext]:
|
| 430 |
+
if template is None:
|
| 431 |
+
return None
|
| 432 |
+
vocab = set()
|
| 433 |
+
if tokenizer is not None:
|
| 434 |
+
try:
|
| 435 |
+
vocab = set(tokenizer.get_vocab().keys())
|
| 436 |
+
except Exception as e:
|
| 437 |
+
logger.warning(
|
| 438 |
+
"Failed to load tokenizer vocab for template detection: %s. "
|
| 439 |
+
"Vocab-dependent detection rules will be skipped.",
|
| 440 |
+
e,
|
| 441 |
+
)
|
| 442 |
+
return TemplateDetectionContext(
|
| 443 |
+
template=template,
|
| 444 |
+
reasoning_config=reasoning_config,
|
| 445 |
+
force_reasoning=force_reasoning,
|
| 446 |
+
vocab=vocab,
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def match_rules(
|
| 451 |
+
ctx: TemplateDetectionContext,
|
| 452 |
+
rules: Tuple[DetectionRule, ...],
|
| 453 |
+
label: str,
|
| 454 |
+
) -> Optional[str]:
|
| 455 |
+
for rule in rules:
|
| 456 |
+
try:
|
| 457 |
+
if rule.predicate(ctx):
|
| 458 |
+
return rule.value
|
| 459 |
+
except Exception as e:
|
| 460 |
+
logger.warning(
|
| 461 |
+
"Detection rule '%s' for %s raised an exception: %s. Skipping.",
|
| 462 |
+
rule.name,
|
| 463 |
+
label,
|
| 464 |
+
e,
|
| 465 |
+
exc_info=True,
|
| 466 |
+
)
|
| 467 |
+
return None
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def detect_reasoning_pattern(
|
| 471 |
+
template: Optional[str],
|
| 472 |
+
) -> Tuple[bool, Optional[ReasoningToggleConfig]]:
|
| 473 |
+
"""Detect if the chat template contains reasoning/thinking patterns."""
|
| 474 |
+
if template is None:
|
| 475 |
+
return False, None
|
| 476 |
+
|
| 477 |
+
ctx = TemplateDetectionContext(
|
| 478 |
+
template=template,
|
| 479 |
+
reasoning_config=None,
|
| 480 |
+
force_reasoning=False,
|
| 481 |
+
vocab=set(),
|
| 482 |
+
)
|
| 483 |
+
for rule in REASONING_MODE_RULES:
|
| 484 |
+
if rule.predicate(ctx):
|
| 485 |
+
return rule.value.always_on, rule.value
|
| 486 |
+
|
| 487 |
+
return False, None
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def detect_reasoning_parser(
|
| 491 |
+
template: Optional[str],
|
| 492 |
+
tokenizer,
|
| 493 |
+
reasoning_config: Optional[ReasoningToggleConfig] = None,
|
| 494 |
+
force_reasoning: bool = False,
|
| 495 |
+
) -> Optional[str]:
|
| 496 |
+
"""Auto-detect which reasoning parser to use from the chat template."""
|
| 497 |
+
ctx = build_detection_context(
|
| 498 |
+
template, tokenizer, reasoning_config, force_reasoning
|
| 499 |
+
)
|
| 500 |
+
if ctx is None:
|
| 501 |
+
return None
|
| 502 |
+
return match_rules(ctx, REASONING_PARSER_RULES, "reasoning parser")
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
def detect_tool_call_parser(
|
| 506 |
+
template: Optional[str],
|
| 507 |
+
tokenizer,
|
| 508 |
+
reasoning_config: Optional[ReasoningToggleConfig] = None,
|
| 509 |
+
force_reasoning: bool = False,
|
| 510 |
+
) -> Optional[str]:
|
| 511 |
+
"""Auto-detect which tool-call parser to use from the chat template."""
|
| 512 |
+
ctx = build_detection_context(
|
| 513 |
+
template, tokenizer, reasoning_config, force_reasoning
|
| 514 |
+
)
|
| 515 |
+
if ctx is None:
|
| 516 |
+
return None
|
| 517 |
+
return match_rules(ctx, TOOL_CALL_PARSER_RULES, "tool-call parser")
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def detect_inline_system_support(chat_template: Optional[str]) -> bool:
|
| 521 |
+
"""True if mid-conversation ``role: "system"`` renders inline; False if the
|
| 522 |
+
template raises or silently drops it (then merge into the leading block).
|
| 523 |
+
|
| 524 |
+
The probe requires the second system's sentinel to appear in the output —
|
| 525 |
+
not raising isn't enough, since some templates ignore non-leading system."""
|
| 526 |
+
if not chat_template:
|
| 527 |
+
return False
|
| 528 |
+
sentinel = "__sglang_inline_system_sentinel__"
|
| 529 |
+
try:
|
| 530 |
+
env = jinja2.sandbox.ImmutableSandboxedEnvironment(
|
| 531 |
+
trim_blocks=True,
|
| 532 |
+
lstrip_blocks=True,
|
| 533 |
+
extensions=[jinja2.ext.loopcontrols],
|
| 534 |
+
)
|
| 535 |
+
rendered = env.from_string(chat_template).render(
|
| 536 |
+
messages=[
|
| 537 |
+
{"role": "system", "content": "t"},
|
| 538 |
+
{"role": "user", "content": "t"},
|
| 539 |
+
{"role": "system", "content": sentinel},
|
| 540 |
+
{"role": "user", "content": "t"},
|
| 541 |
+
],
|
| 542 |
+
add_generation_prompt=False,
|
| 543 |
+
)
|
| 544 |
+
return sentinel in rendered
|
| 545 |
+
except jinja2.TemplateError:
|
| 546 |
+
return False
|
| 547 |
+
except Exception:
|
| 548 |
+
return False
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def _resolve_auto_parser(
|
| 552 |
+
server_args,
|
| 553 |
+
attr: str,
|
| 554 |
+
ctx: TemplateDetectionContext,
|
| 555 |
+
rules: Tuple[DetectionRule, ...],
|
| 556 |
+
label: str,
|
| 557 |
+
) -> None:
|
| 558 |
+
"""Resolve a single auto parser, updating server_args in place."""
|
| 559 |
+
detected = match_rules(ctx, rules, label)
|
| 560 |
+
if detected:
|
| 561 |
+
server_args.override(source="template-detection", **{attr: detected})
|
| 562 |
+
logger.info(
|
| 563 |
+
f"Auto-detected --{attr.replace('_', '-')} as '{detected}' from chat template"
|
| 564 |
+
)
|
| 565 |
+
else:
|
| 566 |
+
logger.warning(
|
| 567 |
+
f"--{attr.replace('_', '-')}=auto specified but could not detect "
|
| 568 |
+
f"{label} from chat template. Disabling {label}."
|
| 569 |
+
)
|
| 570 |
+
server_args.override(source="template-detection", **{attr: None})
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def _load_explicit_jinja_template(chat_template_arg: Optional[str]) -> Optional[str]:
|
| 574 |
+
if not chat_template_arg or not isinstance(chat_template_arg, str):
|
| 575 |
+
return None
|
| 576 |
+
if not chat_template_arg.endswith(".jinja") or not os.path.exists(
|
| 577 |
+
chat_template_arg
|
| 578 |
+
):
|
| 579 |
+
return None
|
| 580 |
+
with open(chat_template_arg, encoding="utf-8") as f:
|
| 581 |
+
return f.read().replace("\\n", "\n")
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def _disable_auto_parser(server_args, attr: str, label: str) -> None:
|
| 585 |
+
logger.warning(
|
| 586 |
+
f"--{attr.replace('_', '-')}=auto specified but could not detect "
|
| 587 |
+
f"{label} from chat template. Disabling {label}."
|
| 588 |
+
)
|
| 589 |
+
server_args.override(source="template-detection", **{attr: None})
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def _resolve_architecture_auto_parsers(server_args) -> None:
|
| 593 |
+
from sglang.srt.utils.hf_transformers_utils import get_config
|
| 594 |
+
|
| 595 |
+
config = get_config(
|
| 596 |
+
server_args.model_path,
|
| 597 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 598 |
+
revision=getattr(server_args, "revision", None),
|
| 599 |
+
model_config_parser=getattr(server_args, "model_config_parser", "auto"),
|
| 600 |
+
)
|
| 601 |
+
architectures = getattr(config, "architectures", None) or []
|
| 602 |
+
arch = architectures[0] if architectures else ""
|
| 603 |
+
|
| 604 |
+
if "Agnes" in arch:
|
| 605 |
+
reasoning_parser, tool_call_parser = "deepseek-v4", "deepseekv4"
|
| 606 |
+
elif "DeepseekV3" in arch:
|
| 607 |
+
reasoning_parser, tool_call_parser = "deepseek-v3", "deepseekv32"
|
| 608 |
+
else:
|
| 609 |
+
return
|
| 610 |
+
|
| 611 |
+
for attr, detected in (
|
| 612 |
+
("reasoning_parser", reasoning_parser),
|
| 613 |
+
("tool_call_parser", tool_call_parser),
|
| 614 |
+
):
|
| 615 |
+
if getattr(server_args, attr) == "auto":
|
| 616 |
+
server_args.override(source="template-detection", **{attr: detected})
|
| 617 |
+
logger.info(
|
| 618 |
+
f"Auto-detected --{attr.replace('_', '-')} as '{detected}' "
|
| 619 |
+
f"from model architecture '{arch}'"
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def resolve_auto_parsers(server_args) -> None:
|
| 624 |
+
"""Resolve --reasoning-parser=auto and --tool-call-parser=auto before scheduler.
|
| 625 |
+
|
| 626 |
+
This performs a lightweight tokenizer load to detect parsers from the chat
|
| 627 |
+
template. Called early in engine init before scheduler subprocesses are spawned.
|
| 628 |
+
"""
|
| 629 |
+
needs_reasoning = server_args.reasoning_parser == "auto"
|
| 630 |
+
needs_tool_call = server_args.tool_call_parser == "auto"
|
| 631 |
+
|
| 632 |
+
if not needs_reasoning and not needs_tool_call:
|
| 633 |
+
return
|
| 634 |
+
|
| 635 |
+
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
| 636 |
+
|
| 637 |
+
chat_template_arg = getattr(server_args, "chat_template", None)
|
| 638 |
+
try:
|
| 639 |
+
explicit_jinja_template = _load_explicit_jinja_template(chat_template_arg)
|
| 640 |
+
except Exception as e:
|
| 641 |
+
logger.warning("Failed to load explicit Jinja chat template: %s", e)
|
| 642 |
+
explicit_jinja_template = None
|
| 643 |
+
has_explicit_template_without_detection = (
|
| 644 |
+
chat_template_arg is not None and explicit_jinja_template is None
|
| 645 |
+
)
|
| 646 |
+
|
| 647 |
+
tokenizer = None
|
| 648 |
+
try:
|
| 649 |
+
tokenizer = get_tokenizer(
|
| 650 |
+
server_args.model_path,
|
| 651 |
+
trust_remote_code=server_args.trust_remote_code,
|
| 652 |
+
)
|
| 653 |
+
except Exception as e:
|
| 654 |
+
logger.warning(f"Failed to load tokenizer for auto-detection: {e}")
|
| 655 |
+
|
| 656 |
+
template = explicit_jinja_template
|
| 657 |
+
if template is None and tokenizer is not None:
|
| 658 |
+
template = getattr(tokenizer, "chat_template", None)
|
| 659 |
+
|
| 660 |
+
force_reasoning, reasoning_config = detect_reasoning_pattern(template)
|
| 661 |
+
ctx = build_detection_context(
|
| 662 |
+
template, tokenizer, reasoning_config, force_reasoning
|
| 663 |
+
)
|
| 664 |
+
if ctx is None:
|
| 665 |
+
if has_explicit_template_without_detection:
|
| 666 |
+
logger.warning(
|
| 667 |
+
"--chat-template=%s is explicit but is not a readable Jinja template, so "
|
| 668 |
+
"parser auto-detection from chat template is not available.",
|
| 669 |
+
chat_template_arg,
|
| 670 |
+
)
|
| 671 |
+
else:
|
| 672 |
+
try:
|
| 673 |
+
_resolve_architecture_auto_parsers(server_args)
|
| 674 |
+
except Exception as e:
|
| 675 |
+
logger.warning(
|
| 676 |
+
"Failed to load model config for architecture-based auto-detection: %s",
|
| 677 |
+
e,
|
| 678 |
+
)
|
| 679 |
+
if needs_reasoning:
|
| 680 |
+
if server_args.reasoning_parser == "auto":
|
| 681 |
+
_disable_auto_parser(
|
| 682 |
+
server_args, "reasoning_parser", "reasoning parser"
|
| 683 |
+
)
|
| 684 |
+
if needs_tool_call:
|
| 685 |
+
if server_args.tool_call_parser == "auto":
|
| 686 |
+
_disable_auto_parser(
|
| 687 |
+
server_args, "tool_call_parser", "tool-call parser"
|
| 688 |
+
)
|
| 689 |
+
return
|
| 690 |
+
|
| 691 |
+
if needs_reasoning:
|
| 692 |
+
_resolve_auto_parser(
|
| 693 |
+
server_args,
|
| 694 |
+
"reasoning_parser",
|
| 695 |
+
ctx,
|
| 696 |
+
REASONING_PARSER_RULES,
|
| 697 |
+
"reasoning parser",
|
| 698 |
+
)
|
| 699 |
+
|
| 700 |
+
if needs_tool_call:
|
| 701 |
+
_resolve_auto_parser(
|
| 702 |
+
server_args,
|
| 703 |
+
"tool_call_parser",
|
| 704 |
+
ctx,
|
| 705 |
+
TOOL_CALL_PARSER_RULES,
|
| 706 |
+
"tool-call parser",
|
| 707 |
+
)
|
sglang_patch/srt/server_args.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
sglang_patch/srt/utils/hf_transformers/common.py
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023-2024 SGLang Team
|
| 2 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 |
+
# you may not use this file except in compliance with the License.
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
#
|
| 6 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 7 |
+
#
|
| 8 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 9 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 |
+
# See the License for the specific language governing permissions and
|
| 12 |
+
# limitations under the License.
|
| 13 |
+
# ==============================================================================
|
| 14 |
+
"""Shared helpers used by config, tokenizer, and processor modules."""
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any, Dict, Optional, Type, Union
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
from huggingface_hub import snapshot_download
|
| 23 |
+
|
| 24 |
+
from sglang.srt.configs import (
|
| 25 |
+
AfmoeConfig,
|
| 26 |
+
BailingHybridConfig,
|
| 27 |
+
ChatGLMConfig,
|
| 28 |
+
DbrxConfig,
|
| 29 |
+
DeepseekVL2Config,
|
| 30 |
+
DotsOCRConfig,
|
| 31 |
+
DotsVLMConfig,
|
| 32 |
+
ExaoneConfig,
|
| 33 |
+
FalconH1Config,
|
| 34 |
+
GraniteMoeHybridConfig,
|
| 35 |
+
InklingAudioConfig,
|
| 36 |
+
InklingMMConfig,
|
| 37 |
+
InklingModelConfig,
|
| 38 |
+
InklingVisionConfig,
|
| 39 |
+
InternS2PreviewConfig,
|
| 40 |
+
JetNemotronConfig,
|
| 41 |
+
JetVLMConfig,
|
| 42 |
+
KimiK25Config,
|
| 43 |
+
KimiLinearConfig,
|
| 44 |
+
KimiVLConfig,
|
| 45 |
+
LagunaConfig,
|
| 46 |
+
LocateAnythingConfig,
|
| 47 |
+
LongcatFlashConfig,
|
| 48 |
+
MiniCPMV4_6Config,
|
| 49 |
+
MiniCPMV4_6VisionConfig,
|
| 50 |
+
MiniMaxM3VLConfig,
|
| 51 |
+
MultiModalityConfig,
|
| 52 |
+
NemotronH_Nano_Omni_Reasoning_V3_Config,
|
| 53 |
+
NemotronH_Nano_VL_V2_Config,
|
| 54 |
+
NemotronHConfig,
|
| 55 |
+
NemotronHPuzzleConfig,
|
| 56 |
+
Olmo3Config,
|
| 57 |
+
Qwen3_5Config,
|
| 58 |
+
Qwen3_5MoeConfig,
|
| 59 |
+
Qwen3NextConfig,
|
| 60 |
+
Step3p5Config,
|
| 61 |
+
Step3p7Config,
|
| 62 |
+
Step3VLConfig,
|
| 63 |
+
)
|
| 64 |
+
from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config
|
| 65 |
+
from sglang.srt.configs.internvl import InternVLChatConfig
|
| 66 |
+
from sglang.srt.utils import get_bool_env_var, logger, lru_cache_frozenset
|
| 67 |
+
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
|
| 68 |
+
|
| 69 |
+
from ..hf_transformers_patches import normalize_rope_scaling_compat
|
| 70 |
+
|
| 71 |
+
if get_bool_env_var("SGLANG_USE_MODELSCOPE"):
|
| 72 |
+
from modelscope import AutoConfig, GenerationConfig
|
| 73 |
+
else:
|
| 74 |
+
from transformers import AutoConfig, GenerationConfig
|
| 75 |
+
|
| 76 |
+
from transformers import PretrainedConfig
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
# Config registry
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
|
| 82 |
+
_CONFIG_REGISTRY: Dict[str, Type[PretrainedConfig]] = {
|
| 83 |
+
cls.model_type: cls
|
| 84 |
+
for cls in [
|
| 85 |
+
AfmoeConfig,
|
| 86 |
+
BailingHybridConfig,
|
| 87 |
+
ChatGLMConfig,
|
| 88 |
+
DbrxConfig,
|
| 89 |
+
ExaoneConfig,
|
| 90 |
+
DeepseekVL2Config,
|
| 91 |
+
MultiModalityConfig,
|
| 92 |
+
KimiVLConfig,
|
| 93 |
+
LocateAnythingConfig,
|
| 94 |
+
InternVLChatConfig,
|
| 95 |
+
LagunaConfig,
|
| 96 |
+
Step3VLConfig,
|
| 97 |
+
LongcatFlashConfig,
|
| 98 |
+
Olmo3Config,
|
| 99 |
+
KimiLinearConfig,
|
| 100 |
+
Qwen3NextConfig,
|
| 101 |
+
FalconH1Config,
|
| 102 |
+
GraniteMoeHybridConfig,
|
| 103 |
+
DotsVLMConfig,
|
| 104 |
+
DotsOCRConfig,
|
| 105 |
+
NemotronH_Nano_VL_V2_Config,
|
| 106 |
+
NemotronH_Nano_Omni_Reasoning_V3_Config,
|
| 107 |
+
NemotronHConfig,
|
| 108 |
+
NemotronHPuzzleConfig,
|
| 109 |
+
DeepseekVLV2Config,
|
| 110 |
+
Qwen3_5Config,
|
| 111 |
+
Qwen3_5MoeConfig,
|
| 112 |
+
InternS2PreviewConfig,
|
| 113 |
+
JetNemotronConfig,
|
| 114 |
+
JetVLMConfig,
|
| 115 |
+
KimiK25Config,
|
| 116 |
+
Step3p5Config,
|
| 117 |
+
Step3p7Config,
|
| 118 |
+
MiniCPMV4_6Config,
|
| 119 |
+
MiniCPMV4_6VisionConfig,
|
| 120 |
+
InklingModelConfig,
|
| 121 |
+
InklingAudioConfig,
|
| 122 |
+
InklingVisionConfig,
|
| 123 |
+
InklingMMConfig,
|
| 124 |
+
MiniMaxM3VLConfig,
|
| 125 |
+
]
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
# DeepSeek V3.2 / V4 reuse the V3 config schema. Subclass the upstream
|
| 129 |
+
# transformers class with each model_type so AutoConfig.register passes its
|
| 130 |
+
# consistency check (which requires class.model_type == registered key).
|
| 131 |
+
# Default-value divergences (e.g. V4's topk_group) are handled in
|
| 132 |
+
# model_config.py post-load.
|
| 133 |
+
try:
|
| 134 |
+
from transformers import DeepseekV3Config as _HFDeepseekV3Config
|
| 135 |
+
|
| 136 |
+
class _DeepseekV32ConfigAlias(_HFDeepseekV3Config):
|
| 137 |
+
model_type = "deepseek_v32"
|
| 138 |
+
|
| 139 |
+
class _AgnesConfigAlias(_HFDeepseekV3Config):
|
| 140 |
+
model_type = "agnes"
|
| 141 |
+
|
| 142 |
+
_CONFIG_REGISTRY["deepseek_v32"] = _DeepseekV32ConfigAlias
|
| 143 |
+
_CONFIG_REGISTRY["agnes"] = _AgnesConfigAlias
|
| 144 |
+
|
| 145 |
+
# For kimi_k25_eagle3
|
| 146 |
+
class _KimiK2ConfigAlias(_HFDeepseekV3Config):
|
| 147 |
+
model_type = "kimi_k2"
|
| 148 |
+
|
| 149 |
+
_CONFIG_REGISTRY["kimi_k2"] = _KimiK2ConfigAlias
|
| 150 |
+
except ImportError:
|
| 151 |
+
pass
|
| 152 |
+
|
| 153 |
+
# Newer transformers versions (>=5.10.2) expose MellumConfig directly,
|
| 154 |
+
# but fallback to Qwen3MoeConfig for older versions.
|
| 155 |
+
try:
|
| 156 |
+
import transformers as _hf_transformers
|
| 157 |
+
|
| 158 |
+
_HFMellumConfig = getattr(_hf_transformers, "MellumConfig", None)
|
| 159 |
+
|
| 160 |
+
if _HFMellumConfig is not None:
|
| 161 |
+
_CONFIG_REGISTRY["mellum"] = _HFMellumConfig
|
| 162 |
+
else:
|
| 163 |
+
from transformers import Qwen3MoeConfig as _HFQwen3MoeConfig
|
| 164 |
+
|
| 165 |
+
class _MellumConfigAlias(_HFQwen3MoeConfig):
|
| 166 |
+
model_type = "mellum"
|
| 167 |
+
|
| 168 |
+
def __post_init__(self, **kwargs):
|
| 169 |
+
# Qwen3MoeConfig.__post_init__ wipes sliding_window unless
|
| 170 |
+
# use_sliding_window=True. Mellum gates sliding attention
|
| 171 |
+
# per-layer via layer_types, so preserve sliding_window
|
| 172 |
+
# regardless of the legacy use_sliding_window flag.
|
| 173 |
+
sliding_window = getattr(self, "sliding_window", None)
|
| 174 |
+
super().__post_init__(**kwargs)
|
| 175 |
+
self.sliding_window = sliding_window
|
| 176 |
+
|
| 177 |
+
_CONFIG_REGISTRY["mellum"] = _MellumConfigAlias
|
| 178 |
+
|
| 179 |
+
except ImportError:
|
| 180 |
+
pass
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
from transformers import Gemma4Config as _HFGemma4Config
|
| 185 |
+
|
| 186 |
+
class _Gemma4UnifiedConfigAlias(_HFGemma4Config):
|
| 187 |
+
model_type = "gemma4_unified"
|
| 188 |
+
|
| 189 |
+
_CONFIG_REGISTRY["gemma4_unified"] = _Gemma4UnifiedConfigAlias
|
| 190 |
+
except ImportError:
|
| 191 |
+
pass
|
| 192 |
+
|
| 193 |
+
for name, cls in _CONFIG_REGISTRY.items():
|
| 194 |
+
try:
|
| 195 |
+
AutoConfig.register(name, cls)
|
| 196 |
+
except ValueError as e:
|
| 197 |
+
err = str(e).lower()
|
| 198 |
+
if "already registered" not in err and "already used" not in err:
|
| 199 |
+
logger.warning("Failed to register config %s: %s", name, e)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ---------------------------------------------------------------------------
|
| 203 |
+
# Download / path helpers
|
| 204 |
+
# ---------------------------------------------------------------------------
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def download_from_hf(
|
| 208 |
+
model_path: str,
|
| 209 |
+
allow_patterns: Optional[Union[str, list]] = None,
|
| 210 |
+
):
|
| 211 |
+
if os.path.exists(model_path):
|
| 212 |
+
return model_path
|
| 213 |
+
|
| 214 |
+
if not allow_patterns:
|
| 215 |
+
allow_patterns = ["*.json", "*.bin", "*.model"]
|
| 216 |
+
|
| 217 |
+
return snapshot_download(model_path, allow_patterns=allow_patterns)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def resolve_runai_obj_uri(model_name_or_path: str) -> str:
|
| 221 |
+
if is_runai_obj_uri(model_name_or_path):
|
| 222 |
+
return ObjectStorageModel.get_path(model_name_or_path)
|
| 223 |
+
return model_name_or_path
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _resolve_local_or_cached_file(model_name_or_path, filename, revision=None):
|
| 227 |
+
"""Resolve a file from a local directory or HF hub cache (no network)."""
|
| 228 |
+
local_path = Path(model_name_or_path) / filename
|
| 229 |
+
if local_path.is_file():
|
| 230 |
+
return str(local_path)
|
| 231 |
+
from huggingface_hub import hf_hub_download
|
| 232 |
+
|
| 233 |
+
return hf_hub_download(
|
| 234 |
+
model_name_or_path, filename, revision=revision, local_files_only=True
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _cached_file_exists(model_name_or_path, filename, revision=None) -> bool:
|
| 239 |
+
"""Whether *filename* is available locally or in the HF cache (no network)."""
|
| 240 |
+
try:
|
| 241 |
+
_resolve_local_or_cached_file(model_name_or_path, filename, revision)
|
| 242 |
+
return True
|
| 243 |
+
except Exception:
|
| 244 |
+
return False
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _remote_file_exists(repo_id, filename, revision=None) -> bool:
|
| 248 |
+
"""Whether *filename* exists on the HF hub (HEAD request only, no download).
|
| 249 |
+
|
| 250 |
+
Returns False on any error (offline, gated, network, invalid id) so callers
|
| 251 |
+
fall back to their default path instead of crashing.
|
| 252 |
+
"""
|
| 253 |
+
from huggingface_hub.constants import HF_HUB_OFFLINE
|
| 254 |
+
|
| 255 |
+
if HF_HUB_OFFLINE:
|
| 256 |
+
return False
|
| 257 |
+
try:
|
| 258 |
+
from huggingface_hub import HfApi
|
| 259 |
+
|
| 260 |
+
return HfApi().file_exists(repo_id, filename, revision=revision)
|
| 261 |
+
except Exception:
|
| 262 |
+
return False
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def check_gguf_file(model: Union[str, os.PathLike]) -> bool:
|
| 266 |
+
model = Path(model)
|
| 267 |
+
if not model.is_file():
|
| 268 |
+
return False
|
| 269 |
+
elif model.suffix == ".gguf":
|
| 270 |
+
return True
|
| 271 |
+
|
| 272 |
+
with open(model, "rb") as f:
|
| 273 |
+
header = f.read(4)
|
| 274 |
+
return header == b"GGUF"
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
# ---------------------------------------------------------------------------
|
| 278 |
+
# Rope / text config helpers
|
| 279 |
+
# ---------------------------------------------------------------------------
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def get_rope_config(config):
|
| 283 |
+
"""Get (rope_theta, rope_params) from config, supporting both v4 and v5.
|
| 284 |
+
|
| 285 |
+
Trust-remote-code configs or parent configs passed to sub-models may not
|
| 286 |
+
have the v5 ``rope_parameters`` property, so we fall back to the v4-style
|
| 287 |
+
``config.rope_theta`` / ``config.rope_scaling`` attributes.
|
| 288 |
+
|
| 289 |
+
Returns:
|
| 290 |
+
(rope_theta, rope_params): In v5, rope_params is the full
|
| 291 |
+
rope_parameters dict (which subsumes rope_scaling and includes
|
| 292 |
+
rope_theta). In v4, rope_params is the rope_scaling dict or None.
|
| 293 |
+
"""
|
| 294 |
+
rope_params = getattr(config, "rope_parameters", None)
|
| 295 |
+
if rope_params is not None:
|
| 296 |
+
return rope_params["rope_theta"], rope_params
|
| 297 |
+
return getattr(config, "rope_theta", 10000), getattr(config, "rope_scaling", None)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _patch_text_config(parent_config: PretrainedConfig, text_config):
|
| 301 |
+
"""Synchronize standard attributes between parent config and text sub-config.
|
| 302 |
+
|
| 303 |
+
In transformers v5, the "untangle config" refactor removed automatic
|
| 304 |
+
inheritance of top-level PretrainedConfig attributes (pad_token_id,
|
| 305 |
+
tie_word_embeddings, etc.) from sub-configs. Downstream code expects
|
| 306 |
+
these attributes to be present on both configs (some models pass the
|
| 307 |
+
parent directly to the language model, others pass the text sub-config),
|
| 308 |
+
so we propagate in both directions when an attribute is missing.
|
| 309 |
+
(See https://github.com/huggingface/transformers/pull/41541)
|
| 310 |
+
"""
|
| 311 |
+
_ATTRS_TO_PROPAGATE = [
|
| 312 |
+
"pad_token_id",
|
| 313 |
+
"bos_token_id",
|
| 314 |
+
"eos_token_id",
|
| 315 |
+
"tie_word_embeddings",
|
| 316 |
+
]
|
| 317 |
+
for attr in _ATTRS_TO_PROPAGATE:
|
| 318 |
+
parent_has = hasattr(parent_config, attr)
|
| 319 |
+
text_has = hasattr(text_config, attr)
|
| 320 |
+
if parent_has and not text_has:
|
| 321 |
+
setattr(text_config, attr, getattr(parent_config, attr))
|
| 322 |
+
elif text_has and not parent_has:
|
| 323 |
+
setattr(parent_config, attr, getattr(text_config, attr))
|
| 324 |
+
return text_config
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def get_hf_text_config(config: PretrainedConfig):
|
| 328 |
+
"""Get the "sub" config relevant to llm for multi modal models.
|
| 329 |
+
No op for pure text models.
|
| 330 |
+
"""
|
| 331 |
+
if config.architectures is not None:
|
| 332 |
+
class_name = config.architectures[0]
|
| 333 |
+
if class_name.startswith("Llava") and class_name.endswith("ForCausalLM"):
|
| 334 |
+
# We support non-hf version of llava models, so we do not want to
|
| 335 |
+
# read the wrong values from the unused default text_config.
|
| 336 |
+
# NOTE(HandH1998): We set `torch_dtype` of config to `torch.float16` for the weights, as
|
| 337 |
+
# `torch.float16` is default used for image features in `python/sglang/srt/models/llava.py`.
|
| 338 |
+
setattr(config, "dtype", torch.float16)
|
| 339 |
+
return config
|
| 340 |
+
|
| 341 |
+
text_config = None
|
| 342 |
+
|
| 343 |
+
# Some models (e.g. DeepSeek-OCR) store sub-configs as plain dicts.
|
| 344 |
+
# Convert to PretrainedConfig early so hasattr() checks and asserts work.
|
| 345 |
+
parent_dtype = getattr(config, "dtype", None)
|
| 346 |
+
for _attr in ("text_config", "llm_config", "language_config", "thinker_config"):
|
| 347 |
+
_sub = getattr(config, _attr, None)
|
| 348 |
+
if isinstance(_sub, dict):
|
| 349 |
+
_converted = PretrainedConfig(**_sub)
|
| 350 |
+
if getattr(_converted, "dtype", None) is None and parent_dtype is not None:
|
| 351 |
+
_converted.dtype = parent_dtype
|
| 352 |
+
setattr(config, _attr, _converted)
|
| 353 |
+
elif _sub is not None and parent_dtype is not None:
|
| 354 |
+
# transformers v5 multimodal configs (e.g. Mistral3Config) carry
|
| 355 |
+
# `dtype` only on the top-level config, leaving the sub-configs at
|
| 356 |
+
# None. Without this, _get_and_verify_dtype falls back to float32
|
| 357 |
+
# and then "auto" downcasts to float16, which overflows the Pixtral
|
| 358 |
+
# vision tower on real images and produces NaN features.
|
| 359 |
+
if getattr(_sub, "dtype", None) is None:
|
| 360 |
+
_sub.dtype = parent_dtype
|
| 361 |
+
|
| 362 |
+
# Priority: thinker_config > llm_config > language_config > text_config
|
| 363 |
+
if hasattr(config, "thinker_config"):
|
| 364 |
+
# qwen2.5 omni
|
| 365 |
+
thinker_config = config.thinker_config
|
| 366 |
+
if hasattr(thinker_config, "text_config"):
|
| 367 |
+
setattr(
|
| 368 |
+
thinker_config.text_config,
|
| 369 |
+
"dtype",
|
| 370 |
+
getattr(thinker_config, "dtype", None),
|
| 371 |
+
)
|
| 372 |
+
text_config = thinker_config.text_config
|
| 373 |
+
else:
|
| 374 |
+
text_config = thinker_config
|
| 375 |
+
elif hasattr(config, "llm_config"):
|
| 376 |
+
# PointsV1.5 Chat Model
|
| 377 |
+
assert hasattr(config.llm_config, "num_attention_heads")
|
| 378 |
+
text_config = config.llm_config
|
| 379 |
+
elif hasattr(config, "language_config"):
|
| 380 |
+
text_config = config.language_config
|
| 381 |
+
elif hasattr(config, "text_config"):
|
| 382 |
+
# The code operates under the assumption that text_config should have
|
| 383 |
+
# `num_attention_heads` (among others). Assert here to fail early
|
| 384 |
+
# if transformers config doesn't align with this assumption.
|
| 385 |
+
assert hasattr(config.text_config, "num_attention_heads")
|
| 386 |
+
text_config = config.text_config
|
| 387 |
+
|
| 388 |
+
# Ensure rope_scaling dicts have "type" for remote-code compat (v5).
|
| 389 |
+
normalize_rope_scaling_compat(config)
|
| 390 |
+
|
| 391 |
+
if text_config is not None:
|
| 392 |
+
return _patch_text_config(config, text_config)
|
| 393 |
+
return config
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
# ---------------------------------------------------------------------------
|
| 397 |
+
# Model-specific helpers
|
| 398 |
+
# ---------------------------------------------------------------------------
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def _ensure_sub_configs(config: PretrainedConfig, *attr_names: str) -> None:
|
| 402 |
+
"""Convert dict-valued sub-configs to proper AutoConfig objects in-place."""
|
| 403 |
+
for attr in attr_names:
|
| 404 |
+
sub = getattr(config, attr, None)
|
| 405 |
+
if sub is not None and isinstance(sub, dict):
|
| 406 |
+
setattr(config, attr, AutoConfig.for_model(**sub))
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def _is_deepseek_ocr_model(config: PretrainedConfig) -> bool:
|
| 410 |
+
# TODO: Remove this workaround once AutoConfig correctly identifies deepseek-ocr.
|
| 411 |
+
# Hugging Face's AutoConfig currently misidentifies it as deepseekvl2.
|
| 412 |
+
auto_map = getattr(config, "auto_map", None) or {}
|
| 413 |
+
return auto_map.get("AutoModel") == "modeling_deepseekocr.DeepseekOCRForCausalLM"
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def _is_deepseek_ocr2_model(config: PretrainedConfig) -> bool:
|
| 417 |
+
auto_map = getattr(config, "auto_map", None) or {}
|
| 418 |
+
return auto_map.get("AutoModel") == "modeling_deepseekocr2.DeepseekOCR2ForCausalLM"
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _override_v_head_dim_if_zero(config: PretrainedConfig, patch: int = 128) -> None:
|
| 422 |
+
patched = False
|
| 423 |
+
for attr in ("text_config", "language_config"):
|
| 424 |
+
sub = getattr(config, attr, None)
|
| 425 |
+
if sub is None:
|
| 426 |
+
continue
|
| 427 |
+
if isinstance(sub, dict):
|
| 428 |
+
if sub.get("v_head_dim") == 0:
|
| 429 |
+
sub["v_head_dim"] = patch
|
| 430 |
+
patched = True
|
| 431 |
+
elif getattr(sub, "v_head_dim", None) == 0:
|
| 432 |
+
sub.v_head_dim = patch
|
| 433 |
+
patched = True
|
| 434 |
+
if patched:
|
| 435 |
+
logger.warning(
|
| 436 |
+
f"Overriding v_head_dim from 0 to {patch} to avoid potential issues."
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
# ---------------------------------------------------------------------------
|
| 441 |
+
# Context length / generation config / sparse attention
|
| 442 |
+
# ---------------------------------------------------------------------------
|
| 443 |
+
|
| 444 |
+
# Models don't use the same configuration key for determining the maximum
|
| 445 |
+
# context length. Store them here so we can sanely check them.
|
| 446 |
+
# NOTE: The ordering here is important. Some models have two of these and we
|
| 447 |
+
# have a preference for which value gets used.
|
| 448 |
+
CONTEXT_LENGTH_KEYS = [
|
| 449 |
+
"max_sequence_length",
|
| 450 |
+
"seq_length",
|
| 451 |
+
"max_seq_len",
|
| 452 |
+
"model_max_length",
|
| 453 |
+
"max_position_embeddings",
|
| 454 |
+
]
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def get_context_length(config):
|
| 458 |
+
"""Get the context length of a model from a huggingface model configs."""
|
| 459 |
+
text_config = config
|
| 460 |
+
rope_scaling = getattr(text_config, "rope_scaling", None)
|
| 461 |
+
if rope_scaling:
|
| 462 |
+
rope_scaling_factor = rope_scaling.get("factor", 1)
|
| 463 |
+
if "original_max_position_embeddings" in rope_scaling:
|
| 464 |
+
rope_scaling_factor = 1
|
| 465 |
+
if rope_scaling.get("rope_type", None) == "llama3":
|
| 466 |
+
rope_scaling_factor = 1
|
| 467 |
+
else:
|
| 468 |
+
rope_scaling_factor = 1
|
| 469 |
+
|
| 470 |
+
for key in CONTEXT_LENGTH_KEYS:
|
| 471 |
+
val = getattr(text_config, key, None)
|
| 472 |
+
if val is not None:
|
| 473 |
+
return int(rope_scaling_factor * val)
|
| 474 |
+
return 2048
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
@lru_cache_frozenset(maxsize=32)
|
| 478 |
+
def get_generation_config(
|
| 479 |
+
model: str,
|
| 480 |
+
trust_remote_code: bool,
|
| 481 |
+
revision: Optional[str] = None,
|
| 482 |
+
**kwargs,
|
| 483 |
+
):
|
| 484 |
+
try:
|
| 485 |
+
return GenerationConfig.from_pretrained(
|
| 486 |
+
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
|
| 487 |
+
)
|
| 488 |
+
except FileNotFoundError:
|
| 489 |
+
return None
|
| 490 |
+
except OSError as e:
|
| 491 |
+
logger.warning(
|
| 492 |
+
"Failed to load generation config for %s: %s. "
|
| 493 |
+
"Proceeding without generation config.",
|
| 494 |
+
model,
|
| 495 |
+
e,
|
| 496 |
+
)
|
| 497 |
+
return None
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
# Qwen-1M related
|
| 501 |
+
def get_sparse_attention_config(
|
| 502 |
+
model: str,
|
| 503 |
+
sparse_attention_config_filename: str = "sparse_attention_config.json",
|
| 504 |
+
) -> Dict[str, Any]:
|
| 505 |
+
is_local = os.path.isdir(model)
|
| 506 |
+
if not is_local:
|
| 507 |
+
model = download_from_hf(model, allow_patterns=["*.json"])
|
| 508 |
+
|
| 509 |
+
config_file = os.path.join(model, sparse_attention_config_filename)
|
| 510 |
+
if not os.path.exists(config_file):
|
| 511 |
+
return {}
|
| 512 |
+
|
| 513 |
+
with open(config_file) as f:
|
| 514 |
+
config = json.load(f)
|
| 515 |
+
return config
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# ---------------------------------------------------------------------------
|
| 519 |
+
# Tokenizer / processor helpers
|
| 520 |
+
# ---------------------------------------------------------------------------
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
# Some models don't have an available processor, e.g.: InternVL
|
| 524 |
+
def get_tokenizer_from_processor(processor):
|
| 525 |
+
from transformers import PreTrainedTokenizerBase
|
| 526 |
+
|
| 527 |
+
if isinstance(processor, PreTrainedTokenizerBase):
|
| 528 |
+
return processor
|
| 529 |
+
return processor.tokenizer
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
# Turn-final markers that some checkpoints ship without EOS metadata:
|
| 533 |
+
# <|eom_id|> (Llama-3 tool use) and <|content_model_end_sampling|> (Inkling,
|
| 534 |
+
# whose bundled tokenizer config leaves eos_token unset).
|
| 535 |
+
_ADDITIONAL_STOP_TOKEN_TEXTS = ("<|eom_id|>", "<|content_model_end_sampling|>")
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
def attach_additional_stop_token_ids(tokenizer):
|
| 539 |
+
added = tokenizer.get_added_vocab()
|
| 540 |
+
stop_ids = {added[text] for text in _ADDITIONAL_STOP_TOKEN_TEXTS if text in added}
|
| 541 |
+
tokenizer.additional_stop_token_ids = stop_ids or None
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_bos_token": false,
|
| 3 |
+
"add_eos_token": false,
|
| 4 |
+
"bos_token": {
|
| 5 |
+
"__type": "AddedToken",
|
| 6 |
+
"content": "<|begin▁of▁sentence|>",
|
| 7 |
+
"lstrip": false,
|
| 8 |
+
"normalized": true,
|
| 9 |
+
"rstrip": false,
|
| 10 |
+
"single_word": false
|
| 11 |
+
},
|
| 12 |
+
"clean_up_tokenization_spaces": false,
|
| 13 |
+
"eos_token": {
|
| 14 |
+
"__type": "AddedToken",
|
| 15 |
+
"content": "<|end▁of▁sentence|>",
|
| 16 |
+
"lstrip": false,
|
| 17 |
+
"normalized": true,
|
| 18 |
+
"rstrip": false,
|
| 19 |
+
"single_word": false
|
| 20 |
+
},
|
| 21 |
+
"legacy": true,
|
| 22 |
+
"model_max_length": 1048576,
|
| 23 |
+
"pad_token": {
|
| 24 |
+
"__type": "AddedToken",
|
| 25 |
+
"content": "<|end▁of▁sentence|>",
|
| 26 |
+
"lstrip": false,
|
| 27 |
+
"normalized": true,
|
| 28 |
+
"rstrip": false,
|
| 29 |
+
"single_word": false
|
| 30 |
+
},
|
| 31 |
+
"sp_model_kwargs": {},
|
| 32 |
+
"unk_token": null,
|
| 33 |
+
"tokenizer_class": "PreTrainedTokenizerFast"
|
| 34 |
+
}
|