Agnes-AI commited on
Commit
e6b37e4
·
verified ·
1 Parent(s): edebd87

Upload folder using huggingface_hub

Browse files
Files changed (48) hide show
  1. model-00016-of-00035.safetensors +3 -0
  2. model-00017-of-00035.safetensors +3 -0
  3. model-00018-of-00035.safetensors +3 -0
  4. model-00019-of-00035.safetensors +3 -0
  5. model-00020-of-00035.safetensors +3 -0
  6. model-00021-of-00035.safetensors +3 -0
  7. model-00022-of-00035.safetensors +3 -0
  8. model-00023-of-00035.safetensors +3 -0
  9. model-00024-of-00035.safetensors +3 -0
  10. model-00025-of-00035.safetensors +3 -0
  11. model-00026-of-00035.safetensors +3 -0
  12. model-00027-of-00035.safetensors +3 -0
  13. model-00028-of-00035.safetensors +3 -0
  14. model-00029-of-00035.safetensors +3 -0
  15. model-00030-of-00035.safetensors +3 -0
  16. model-00031-of-00035.safetensors +3 -0
  17. model-00032-of-00035.safetensors +3 -0
  18. model-00033-of-00035.safetensors +3 -0
  19. model-00034-of-00035.safetensors +3 -0
  20. model-00035-of-00035.safetensors +3 -0
  21. model-00036-of-00036.safetensors +3 -0
  22. model-parallel-ffn.safetensors +3 -0
  23. model.safetensors.index.json +0 -0
  24. modeling_agnes.py +1784 -0
  25. serve.sh +21 -0
  26. sglang_patch/kernels/ops/layernorm/mhc_head.py +151 -0
  27. sglang_patch/srt/arg_groups/overrides.py +2230 -0
  28. sglang_patch/srt/arg_groups/speculative_hook.py +792 -0
  29. sglang_patch/srt/batch_overlap/operations_strategy.py +378 -0
  30. sglang_patch/srt/configs/agnes.py +110 -0
  31. sglang_patch/srt/configs/model_config.py +2055 -0
  32. sglang_patch/srt/entrypoints/openai/chat_encoding.py +97 -0
  33. sglang_patch/srt/entrypoints/openai/serving_chat.py +2264 -0
  34. sglang_patch/srt/function_call/function_call_parser.py +286 -0
  35. sglang_patch/srt/layers/attention/dsv4/compressor.py +480 -0
  36. sglang_patch/srt/layers/attention/dsv4/indexer.py +887 -0
  37. sglang_patch/srt/layers/quantization/modelslim/modelslim.py +485 -0
  38. sglang_patch/srt/mem_cache/kv_cache_configurator.py +1867 -0
  39. sglang_patch/srt/models/agnes.py +0 -0
  40. sglang_patch/srt/models/agnes_dspark.py +892 -0
  41. sglang_patch/srt/models/agnes_nextn.py +283 -0
  42. sglang_patch/srt/models/deepseek_v2.py +0 -0
  43. sglang_patch/srt/parser/reasoning_parser.py +1495 -0
  44. sglang_patch/srt/parser/template_detection.py +707 -0
  45. sglang_patch/srt/server_args.py +0 -0
  46. sglang_patch/srt/utils/hf_transformers/common.py +541 -0
  47. tokenizer.json +0 -0
  48. 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
+ }