[Model] Laguna-XS.2 Model Support (#24204)

This commit is contained in:
Jimmy Shong
2026-05-09 05:43:13 +08:00
committed by GitHub
parent 7b707c9222
commit 096ad02b06
12 changed files with 2023 additions and 0 deletions
+2
View File
@@ -15,6 +15,7 @@ from sglang.srt.configs.kimi_k25 import KimiK25Config
from sglang.srt.configs.kimi_linear import KimiLinearConfig
from sglang.srt.configs.kimi_vl import KimiVLConfig
from sglang.srt.configs.kimi_vl_moonvit import MoonViTConfig
from sglang.srt.configs.laguna import LagunaConfig
from sglang.srt.configs.lfm2 import Lfm2Config
from sglang.srt.configs.lfm2_moe import Lfm2MoeConfig
from sglang.srt.configs.lfm2_vl import Lfm2VlConfig
@@ -52,6 +53,7 @@ __all__ = [
"Olmo3Config",
"KimiLinearConfig",
"KimiK25Config",
"LagunaConfig",
"Qwen3NextConfig",
"Qwen3_5Config",
"Qwen3_5MoeConfig",
+209
View File
@@ -0,0 +1,209 @@
# coding=utf-8
# Copyright 2023-2026 SGLang Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
"""Laguna (poolside/Laguna-XS.2) model configuration."""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import logging
logger = logging.get_logger(__name__)
def _first_not_none(*candidates: Any) -> Any:
"""First non-None candidate. Unlike `a or b`, preserves falsy values."""
return next((c for c in candidates if c is not None), None)
def _to_sglang_rope_scaling(rope_params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""HF per-layer rope dict → SGLang `get_rope` `rope_scaling`. None means plain RoPE."""
if not rope_params:
return None
rope_type = rope_params.get("rope_type") or rope_params.get("type")
if rope_type in (None, "default"):
return None
out: Dict[str, Any] = {"rope_type": rope_type}
pass_through = (
"factor",
"original_max_position_embeddings",
"beta_fast",
"beta_slow",
"extrapolation_factor",
"truncate",
"low_freq_factor",
"high_freq_factor",
"mscale",
"mscale_all_dim",
"short_factor",
"long_factor",
"short_mscale",
"long_mscale",
)
for key in pass_through:
if key in rope_params:
out[key] = rope_params[key]
if "attention_factor" in rope_params:
# HF spells it attention_factor; SGLang's factory reads attn_factor.
out["attn_factor"] = rope_params["attention_factor"]
return out
class LagunaConfig(PretrainedConfig):
model_type = "laguna"
keys_to_ignore_at_inference = ["past_key_values"]
def __init__(
self,
vocab_size: int = 100352,
hidden_size: int = 2048,
intermediate_size: int = 8192,
num_hidden_layers: int = 40,
num_attention_heads: int = 48,
num_key_value_heads: int = 8,
head_dim: int = 128,
hidden_act: str = "silu",
max_position_embeddings: int = 131072,
initializer_range: float = 0.02,
rms_norm_eps: float = 1e-6,
use_cache: bool = True,
tie_word_embeddings: bool = False,
attention_bias: bool = False,
attention_dropout: float = 0.0,
sliding_window: int = 512,
layer_types: Optional[List[str]] = None,
mlp_layer_types: Optional[List[str]] = None,
num_attention_heads_per_layer: Optional[List[int]] = None,
num_experts: int = 256,
num_experts_per_tok: int = 8,
moe_intermediate_size: int = 512,
shared_expert_intermediate_size: int = 512,
moe_routed_scaling_factor: float = 1.0,
moe_router_logit_softcapping: float = 0.0,
moe_apply_router_weight_on_input: bool = False,
# Per-layer-type rope dict; nested under "full_attention" / "sliding_attention".
rope_parameters: Optional[Dict[str, Any]] = None,
partial_rotary_factor: Optional[float] = None,
rope_theta: Optional[float] = None,
rope_scaling: Optional[Dict[str, Any]] = None,
bos_token_id: Optional[int] = 2,
eos_token_id: Optional[Any] = None,
pad_token_id: Optional[int] = 9,
**kwargs,
):
super().__init__(
tie_word_embeddings=tie_word_embeddings,
bos_token_id=bos_token_id,
eos_token_id=eos_token_id,
pad_token_id=pad_token_id,
**kwargs,
)
self.vocab_size = vocab_size
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_key_value_heads = num_key_value_heads
self.head_dim = head_dim
self.hidden_act = hidden_act
self.max_position_embeddings = max_position_embeddings
self.initializer_range = initializer_range
self.rms_norm_eps = rms_norm_eps
self.use_cache = use_cache
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
self.sliding_window = sliding_window
self.num_experts = num_experts
self.num_experts_per_tok = num_experts_per_tok
self.moe_intermediate_size = moe_intermediate_size
self.shared_expert_intermediate_size = shared_expert_intermediate_size
self.moe_routed_scaling_factor = moe_routed_scaling_factor
self.moe_router_logit_softcapping = moe_router_logit_softcapping
self.moe_apply_router_weight_on_input = moe_apply_router_weight_on_input
# Synthesise per-layer schedules when the caller omits them so the model
# file can index by layer_id without per-call guards.
self.layer_types = (
list(layer_types)
if layer_types
else [
"full_attention" if i % 4 == 0 else "sliding_attention"
for i in range(num_hidden_layers)
]
)
self.mlp_layer_types = (
list(mlp_layer_types)
if mlp_layer_types
else (["dense"] + ["sparse"] * (num_hidden_layers - 1))
)
self.num_attention_heads_per_layer = (
list(num_attention_heads_per_layer)
if (num_attention_heads_per_layer)
else [num_attention_heads] * num_hidden_layers
)
# SGLang's hybrid-SWA core reads `swa_*` KV/head_dim from hf_text_config.
# Per-layer Q-head count is read directly from num_attention_heads_per_layer.
# Pure-SWA models would have no full_attention layer, but the synthesized
# default above always plants one at index 0; let .index() raise if a
# caller passes an all-sliding layer_types — silent fallback would wire
# the SWA head count into a "full" attribute and corrupt downstream sizes.
full_idx = self.layer_types.index("full_attention")
self.num_attention_heads = self.num_attention_heads_per_layer[full_idx]
self.swa_num_key_value_heads = num_key_value_heads
self.swa_head_dim = head_dim
self.swa_v_head_dim = head_dim
# Released checkpoint nests rope_parameters under layer-type keys.
rp = rope_parameters if isinstance(rope_parameters, dict) else {}
full_rp = rp.get("full_attention") or {}
swa_rp = rp.get("sliding_attention") or {}
# transformers v5 aliases `rope_scaling` ↔ `rope_parameters` on
# PretrainedConfig — writing one clobbers the other. Keep the nested
# form on those two slots (so HF's reference modeling code can index
# rope_parameters[layer_type] when invoked via trust_remote_code) and
# publish our SGLang-shaped flat rope dicts under different names.
self.rope_parameters = rope_parameters
self.rope_theta = _first_not_none(
full_rp.get("rope_theta"), rope_theta, 10000.0
)
self.partial_rotary_factor = _first_not_none(
full_rp.get("partial_rotary_factor"), partial_rotary_factor, 1.0
)
self.full_rope_scaling = _first_not_none(
_to_sglang_rope_scaling(full_rp), rope_scaling
)
self.swa_rope_theta = _first_not_none(swa_rp.get("rope_theta"), self.rope_theta)
self.swa_partial_rotary_factor = _first_not_none(
swa_rp.get("partial_rotary_factor"), self.partial_rotary_factor
)
self.swa_rope_scaling = _to_sglang_rope_scaling(swa_rp)
# DeepSeek-style aliases consumed by cross-cutting infra outside this
# model file: `lora/mem_pool.py` and `lora/utils.py` read
# `n_routed_experts` / `n_shared_experts` / `first_k_dense_replace`,
# `elastic_ep/expert_backup_*` reads `n_routed_experts`. The
# hardcoded `n_shared_experts=1` and `norm_topk_prob=True` reflect
# Laguna's fixed architecture (one shared expert, sigmoid-renormalized
# top-k routing).
self.n_routed_experts = num_experts
self.n_shared_experts = 1
self.routed_scaling_factor = moe_routed_scaling_factor
self.norm_topk_prob = True
self.first_k_dense_replace = (
self.mlp_layer_types.index("sparse")
if "sparse" in self.mlp_layer_types
else num_hidden_layers
)
@@ -1660,6 +1660,7 @@ def is_hybrid_swa_model(model_architectures: List[str]):
"Step3p5MTP",
"Gemma4ForCausalLM",
"Gemma4ForConditionalGeneration",
"LagunaForCausalLM",
}
return any(arch in hybrid_swa_archs for arch in model_architectures)
@@ -1721,6 +1722,14 @@ def get_hybrid_layer_ids(
full_attention_layer_ids = [
i for i, x in enumerate(layer_types) if x == "full_attention"
]
elif "LagunaForCausalLM" in model_architectures:
layer_types = getattr(hf_text_config, "layer_types", [])
swa_attention_layer_ids = [
i for i, x in enumerate(layer_types) if x == "sliding_attention"
]
full_attention_layer_ids = [
i for i, x in enumerate(layer_types) if x == "full_attention"
]
else:
swa_attention_layer_ids = None
full_attention_layer_ids = None
@@ -30,6 +30,7 @@ from sglang.srt.function_call.llama32_detector import Llama32Detector
from sglang.srt.function_call.mimo_detector import MiMoDetector
from sglang.srt.function_call.minimax_m2 import MinimaxM2Detector
from sglang.srt.function_call.mistral_detector import MistralDetector
from sglang.srt.function_call.poolside_v1_detector import PoolsideV1Detector
from sglang.srt.function_call.pythonic_detector import PythonicDetector
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
from sglang.srt.function_call.qwen25_detector import Qwen25Detector
@@ -66,6 +67,7 @@ class FunctionCallParser:
"llama3": Llama32Detector,
"mimo": MiMoDetector,
"mistral": MistralDetector,
"poolside_v1": PoolsideV1Detector,
"pythonic": PythonicDetector,
"qwen": Qwen25Detector,
"qwen25": Qwen25Detector,
@@ -0,0 +1,452 @@
import ast
import json
import re
from enum import Enum, auto
from typing import Any, List, Optional
from sglang.srt.entrypoints.openai.protocol import Tool
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
from sglang.srt.function_call.core_types import (
StreamingParseResult,
StructureInfo,
ToolCallItem,
_GetInfoFunc,
)
class _ParseState(Enum):
"""5 FSM states for the streaming parser.
Entry guard: READING_VALUE is reachable only from READING_KEY, so the
"stray <arg_value> before <tool_call>" bug class is structurally
impossible.
Exit guard: both READING_KEY and READING_VALUE recover on `</tool_call>`
by closing the active call (orphan key dropped if any). READING_VALUE
additionally recovers on `<arg_key>` by replacing the orphan pending key
with the new one. Both guards match the (regex-tightened) non-streaming
path. Without them, malformed inputs would leave the FSM stuck in
READING_VALUE and mis-attribute subsequent values to stale state.
"""
OUTSIDE = auto()
READING_NAME = auto()
READING_KEY = auto()
READING_VALUE = auto()
DRAINING = auto()
class PoolsideV1Detector(BaseFormatDetector):
"""
Detector for poolside Laguna-XS.2 (poolside_v1 series) tool-call wire format.
Wire format:
<tool_call>{name}\\n
<arg_key>{key}</arg_key>\\n
<arg_value>{val}</arg_value>\\n
...
</tool_call>
String values are emitted as raw text; non-strings are JSON-encoded by
the chat template. The parser does schema-based type coercion to round-trip
them: schema type `string` keeps the raw value; other types attempt
`json.loads` and fall back to `ast.literal_eval`, then to the raw string.
"""
# Wire-format tag tokens — constants, not per-instance.
tool_call_start_token = "<tool_call>"
tool_call_end_token = "</tool_call>"
arg_key_start = "<arg_key>"
arg_key_end = "</arg_key>"
arg_value_start = "<arg_value>"
arg_value_end = "</arg_value>"
tool_call_regex = re.compile(r"<tool_call>(.*?)</tool_call>", re.DOTALL)
# Key uses [^<]*? to prevent the non-greedy `.*?` from backtracking
# across an `</arg_key>` boundary on malformed inputs like
# `<arg_key>K1</arg_key><arg_key>K2</arg_key><arg_value>V</arg_value>`
# — without the `[^<]` constraint, the regex matches the entire orphan
# span as a single key (`K1</arg_key><arg_key>K2`). Param names never
# contain `<` in practice, so this is safe. The value side keeps `.*?`
# because legitimate values can contain `<` (HTML, paths, etc.); the
# `</arg_value>` boundary is anchored enough.
arg_pair_regex = re.compile(
r"<arg_key>([^<]*?)</arg_key>\s*<arg_value>(.*?)</arg_value>",
re.DOTALL,
)
_partial_tag_prefixes = (
tool_call_start_token,
tool_call_end_token,
arg_key_start,
arg_key_end,
arg_value_start,
arg_value_end,
)
def __init__(self):
super().__init__()
self.parsed_pos: int = 0
self._state: _ParseState = _ParseState.OUTSIDE
self.current_func_name: Optional[str] = None
self.current_pending_key: Optional[str] = None
self.json_started: bool = False
# ---------- Helpers ----------
def _reset_call_state(self) -> None:
"""Reset per-call FSM scratch fields. Called when entering a new
<tool_call> and on </tool_call> close."""
self.current_func_name = None
self.current_pending_key = None
self.json_started = False
def _consume_arg_key(self, slice_: str) -> bool:
"""Consume `<arg_key>K</arg_key>`, set `current_pending_key` to K.
Returns True if consumed, False if `</arg_key>` hasn't arrived yet
(caller should break to wait for more bytes). Shared by READING_KEY
(well-formed: transitions to READING_VALUE) and READING_VALUE
(orphan-key-replace: stays in READING_VALUE)."""
end = slice_.find(self.arg_key_end)
if end == -1:
return False
self.current_pending_key = slice_[len(self.arg_key_start) : end].strip()
self.parsed_pos += end + len(self.arg_key_end)
return True
def _close_current_call(self, calls: List[ToolCallItem]) -> None:
"""Emit the closing `}` (or `{}` for zero-arg) for the active call,
advance past `</tool_call>`, return to OUTSIDE, and reset per-call
state. Called from both READING_KEY (the well-formed close path) and
READING_VALUE (malformed close: `<arg_key>...</arg_key></tool_call>`
with no value — orphan key is discarded, matching the regex
non-streaming path which drops unmatched <arg_key>...</arg_key>
pairs)."""
fragment = "}" if self.json_started else "{}"
calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
parameters=fragment,
)
)
self.streamed_args_for_tool[self.current_tool_id] += fragment
self.parsed_pos += len(self.tool_call_end_token)
self._state = _ParseState.OUTSIDE
self._reset_call_state()
def has_tool_call(self, text: str) -> bool:
return self.tool_call_start_token in text
@staticmethod
def _get_param_schema(
func_name: Optional[str], tools: Optional[List[Tool]]
) -> dict:
if not tools or not func_name:
return {}
for tool in tools:
try:
if (
tool.type == "function"
and tool.function.name == func_name
and isinstance(tool.function.parameters, dict)
):
return tool.function.parameters.get("properties", {})
except AttributeError:
continue
return {}
_STRING_TYPES = frozenset({"string", "str", "text", "enum"})
@staticmethod
def _convert_param_value(raw: str, schema: dict, key: str) -> Any:
"""Coerce a raw arg_value string per schema; fall back to raw on failure.
Decoder selection by schema type:
- string-like types → identity (raw text)
- no schema entry → json.loads only (conservative; don't
ast-eval untyped values)
- everything else (int,
number, bool, object, …) → json.loads, then ast.literal_eval
Each decoder result is round-tripped through `json.dumps` before being
returned; non-JSON-serializable values (sets / complex / bytes from
`ast.literal_eval`) are rejected to the next decoder, ultimately
falling through to the raw-string fallback rather than crashing the
streaming JSON emission downstream.
"""
spec = schema.get(key) if isinstance(schema, dict) else None
param_type = str(spec.get("type", "")).lower() if isinstance(spec, dict) else ""
if param_type in PoolsideV1Detector._STRING_TYPES:
return raw
decoders = (json.loads,) if not param_type else (json.loads, ast.literal_eval)
for decoder in decoders:
try:
result = decoder(raw)
# ast.literal_eval can return non-JSON-serializable values
# (sets, complex numbers); reject so json.dumps downstream
# doesn't choke.
json.dumps(result)
return result
except (ValueError, SyntaxError, TypeError):
continue
return raw
def _find_name_boundary(self, text: str) -> int:
"""Earliest of `\\n`, `<arg_key>`, `</tool_call>`. -1 if none."""
hits = (
text.find("\n"),
text.find(self.arg_key_start),
text.find(self.tool_call_end_token),
)
positive = [h for h in hits if h != -1]
return min(positive) if positive else -1
def _is_partial_tag(self, slice_: str) -> bool:
"""True if slice_ is a strict prefix of any known tag — i.e. more
bytes might complete it into a real tag."""
return any(
tag.startswith(slice_) and tag != slice_
for tag in self._partial_tag_prefixes
)
# ---------- Non-streaming ----------
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
if self.tool_call_start_token not in text:
return StreamingParseResult(normal_text=text)
tool_indices = self._get_tool_indices(tools)
first_idx = text.find(self.tool_call_start_token)
normal_text = text[:first_idx] if first_idx > 0 else ""
calls: List[ToolCallItem] = []
for body in self.tool_call_regex.findall(text):
# _find_name_boundary searches for `\n` / `<arg_key>` /
# `</tool_call>`, but the regex already stripped `</tool_call>`,
# so a no-arg call without a trailing newline
# (`<tool_call>now</tool_call>`) gives boundary == -1. Treat
# that case as "name == entire body".
boundary = self._find_name_boundary(body)
name = (body if boundary == -1 else body[:boundary]).strip()
if not name or name not in tool_indices:
continue
schema = self._get_param_schema(name, tools)
args: dict = {}
for raw_key, raw_val in self.arg_pair_regex.findall(body):
key = raw_key.strip()
# Strip at most one wrapping `\n` on each side (template adds
# them around the value); preserve newlines that are part of
# the value itself.
val = raw_val.removeprefix("\n").removesuffix("\n")
args[key] = self._convert_param_value(val, schema, key)
calls.append(
ToolCallItem(
tool_index=tool_indices[name],
name=name,
parameters=json.dumps(args, ensure_ascii=False),
)
)
return StreamingParseResult(normal_text=normal_text, calls=calls)
# ---------- Streaming ----------
def parse_streaming_increment(
self, new_text: str, tools: List[Tool]
) -> StreamingParseResult:
self._buffer += new_text
if not self._buffer:
return StreamingParseResult()
tool_indices = self._get_tool_indices(tools)
calls: List[ToolCallItem] = []
normal_text_chunks: List[str] = []
# No try/except: the FSM's invariants make the prior masked-IndexError
# class unreachable, and TypeError from json.dumps is prevented at the
# source (_convert_param_value round-trips its decoder output). If a
# real bug surfaces, let it surface.
while True:
slice_ = self._buffer[self.parsed_pos :]
if not slice_:
break
state = self._state
if state is _ParseState.OUTSIDE:
if slice_.startswith(self.tool_call_start_token):
self.parsed_pos += len(self.tool_call_start_token)
self._state = _ParseState.READING_NAME
self._reset_call_state()
continue
if slice_.startswith("<"):
if self._is_partial_tag(slice_):
break # could be a partial <tool_call>
normal_text_chunks.append("<")
self.parsed_pos += 1
continue
next_lt = slice_.find("<")
segment = slice_ if next_lt == -1 else slice_[:next_lt]
normal_text_chunks.append(segment)
self.parsed_pos += len(segment)
continue
if state is _ParseState.READING_NAME:
boundary = self._find_name_boundary(slice_)
if boundary == -1:
break # name still incoming
name = slice_[:boundary].strip()
# Consume the name and a single delimiting newline (if
# present). The other boundary types (<arg_key>,
# </tool_call>) are left for the next state. boundary may
# be 0 for a malformed `<tool_call><arg_key>...` (no
# name); the state transition below is the loop-progress
# guarantee.
consume = boundary
if boundary < len(slice_) and slice_[boundary : boundary + 1] == "\n":
consume += 1
self.parsed_pos += consume
if name and name in tool_indices:
self.current_tool_id += 1
while len(self.streamed_args_for_tool) <= self.current_tool_id:
self.streamed_args_for_tool.append("")
self.current_func_name = name
# Per-response sequential index — OpenAI clients group
# chunks by tool_index, so the name event and later
# parameter fragments must share this value.
calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
name=name,
parameters="",
)
)
self._state = _ParseState.READING_KEY
else:
# Unknown / empty name — drain to </tool_call> with no
# client-visible emission.
self._state = _ParseState.DRAINING
continue
if state is _ParseState.READING_KEY:
if slice_.startswith(self.tool_call_end_token):
self._close_current_call(calls)
continue
if slice_.startswith(self.arg_key_start):
if not self._consume_arg_key(slice_):
break # incomplete <arg_key>
self._state = _ParseState.READING_VALUE
continue
if slice_.startswith("<"):
if self._is_partial_tag(slice_):
break
# Bare '<' that's not any known tag — discard silently
# (inside a tool call, this is not normal_text).
self.parsed_pos += 1
continue
# Inter-tag whitespace / newline — discard.
next_lt = slice_.find("<")
self.parsed_pos += len(slice_) if next_lt == -1 else next_lt
continue
if state is _ParseState.READING_VALUE:
# Recover from a malformed `<arg_key>K</arg_key></tool_call>`
# (no <arg_value>) by closing the call here. Without this
# branch the FSM would stay stuck in READING_VALUE and
# mis-attribute the next call's <arg_value> to the orphan
# `current_pending_key`, silently swallowing the next call's
# name. Matches the regex non-streaming path, which drops
# unmatched <arg_key>...</arg_key> pairs.
if slice_.startswith(self.tool_call_end_token):
self._close_current_call(calls)
continue
# Recover from a malformed `<arg_key>K1</arg_key><arg_key>K2`
# (no value for K1, model went straight to a new key) by
# replacing the orphan pending_key with the new one. Stays
# in READING_VALUE so the next <arg_value> binds to K2.
# Without this branch the FSM treats the second <arg_key>
# as bare-`<` garbage and the next <arg_value> binds to
# the stale K1 — wrong-argument corruption.
if slice_.startswith(self.arg_key_start):
if not self._consume_arg_key(slice_):
break # incomplete <arg_key>
continue # stay in READING_VALUE: orphan replaced
if slice_.startswith(self.arg_value_start):
end = slice_.find(self.arg_value_end)
if end == -1:
break # incomplete <arg_value> — no partial emission
raw = (
slice_[len(self.arg_value_start) : end]
.removeprefix("\n")
.removesuffix("\n")
)
# READING_VALUE is reachable only via READING_KEY
# consuming an <arg_key>...</arg_key>, so
# current_pending_key is set by construction.
schema = self._get_param_schema(self.current_func_name, tools)
converted = self._convert_param_value(
raw, schema, self.current_pending_key
)
kv = (
f"{json.dumps(self.current_pending_key)}: "
f"{json.dumps(converted, ensure_ascii=False)}"
)
fragment = "{" + kv if not self.json_started else ", " + kv
self.json_started = True
calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
parameters=fragment,
)
)
self.streamed_args_for_tool[self.current_tool_id] += fragment
self.current_pending_key = None
self.parsed_pos += end + len(self.arg_value_end)
self._state = _ParseState.READING_KEY
continue
if slice_.startswith("<"):
if self._is_partial_tag(slice_):
break
self.parsed_pos += 1
continue
next_lt = slice_.find("<")
self.parsed_pos += len(slice_) if next_lt == -1 else next_lt
continue
if state is _ParseState.DRAINING:
end_idx = slice_.find(self.tool_call_end_token)
if end_idx != -1:
self.parsed_pos += end_idx + len(self.tool_call_end_token)
self._state = _ParseState.OUTSIDE
continue
# Hold back trailing bytes that could be a prefix of
# </tool_call>; the next chunk extends the tail.
holdback = self._ends_with_partial_token(
slice_, self.tool_call_end_token
)
self.parsed_pos += len(slice_) - holdback
break
if self.parsed_pos > 0:
self._buffer = self._buffer[self.parsed_pos :]
self.parsed_pos = 0
return StreamingParseResult(
calls=calls,
normal_text="".join(normal_text_chunks),
)
# ---------- Constrained generation ----------
def supports_structural_tag(self) -> bool:
return False
def structure_info(self) -> _GetInfoFunc:
return lambda name: StructureInfo(
begin=f"<tool_call>{name}\n",
end="</tool_call>",
trigger="<tool_call>",
)
+787
View File
@@ -0,0 +1,787 @@
# Copyright 2023-2026 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
"""Inference-only Laguna (poolside/Laguna-XS.2) model."""
from __future__ import annotations
import logging
from collections.abc import Iterable
from typing import Any, Dict, Optional, Tuple, Union
import torch
import torch.nn.functional as F
from torch import nn
from sglang.srt.configs.laguna import LagunaConfig
from sglang.srt.distributed import (
get_pp_group,
get_tensor_model_parallel_world_size,
tensor_model_parallel_all_reduce,
)
from sglang.srt.layers.activation import SiluAndMul
from sglang.srt.layers.communicator import (
LayerCommunicator,
LayerScatterModes,
)
from sglang.srt.layers.dp_attention import (
get_attention_tp_rank,
get_attention_tp_size,
is_dp_attention_enabled,
)
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.srt.layers.logits_processor import LogitsProcessor
from sglang.srt.layers.moe import should_skip_post_experts_all_reduce
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.moe.topk import TopK
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.layers.rotary_embedding import get_rope
from sglang.srt.layers.utils import PPMissingLayer, get_layer_id
from sglang.srt.layers.vocab_parallel_embedding import (
ParallelLMHead,
VocabParallelEmbedding,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.utils import apply_qk_norm
from sglang.srt.server_args import get_global_server_args
from sglang.srt.utils import LazyValue, add_prefix, make_layers
logger = logging.getLogger(__name__)
class LagunaMLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: Optional[QuantizationConfig] = None,
reduce_results: bool = True,
prefix: str = "",
) -> None:
super().__init__()
if hidden_act != "silu":
raise ValueError(
f"Unsupported activation: {hidden_act}. Only silu is supported."
)
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
prefix=add_prefix("gate_up_proj", prefix),
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
quant_config=quant_config,
reduce_results=reduce_results,
prefix=add_prefix("down_proj", prefix),
)
self.act_fn = SiluAndMul()
def forward(
self,
x: torch.Tensor,
forward_batch: Optional[ForwardBatch] = None,
should_allreduce_fusion: bool = False,
use_reduce_scatter: bool = False,
) -> torch.Tensor:
gate_up, _ = self.gate_up_proj(x)
x = self.act_fn(gate_up)
# Skip the in-block reduce when LayerCommunicator will fuse it or when
# the next layer expects reduce-scatter — otherwise we'd double-reduce.
x, _ = self.down_proj(
x,
skip_all_reduce=should_allreduce_fusion or use_reduce_scatter,
)
return x
class LagunaMoEGate(nn.Module):
def __init__(
self,
config: LagunaConfig,
prefix: str = "",
):
super().__init__()
self.weight = nn.Parameter(
torch.empty(config.num_experts, config.hidden_size, dtype=torch.float32)
)
# Released checkpoint stores this under `mlp.experts.e_score_correction_bias`
# (load_weights remaps it) but every value is 0.0; zero-init keeps us
# correct if a future checkpoint omits the tensor entirely.
self.e_score_correction_bias = nn.Parameter(
torch.zeros(config.num_experts, dtype=torch.float32),
requires_grad=False,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return F.linear(hidden_states.to(torch.float32), self.weight, None)
class LagunaMoE(nn.Module):
def __init__(
self,
config: LagunaConfig,
layer_id: int,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
):
super().__init__()
self.tp_size = get_tensor_model_parallel_world_size()
self.routed_scaling_factor = config.moe_routed_scaling_factor
self.router_logit_softcapping = getattr(
config, "moe_router_logit_softcapping", 0.0
)
if self.tp_size > config.num_experts:
raise ValueError(
f"TP size {self.tp_size} > num_experts {config.num_experts}."
)
self.gate = LagunaMoEGate(config, prefix=add_prefix("gate", prefix))
self.experts = get_moe_impl_class(quant_config)(
num_experts=config.num_experts
+ get_global_server_args().ep_num_redundant_experts,
top_k=config.num_experts_per_tok,
layer_id=layer_id,
hidden_size=config.hidden_size,
intermediate_size=config.moe_intermediate_size,
quant_config=quant_config,
reduce_results=False,
apply_router_weight_on_input=bool(config.moe_apply_router_weight_on_input),
prefix=add_prefix("experts", prefix),
)
self.topk = TopK(
top_k=config.num_experts_per_tok,
layer_id=layer_id,
renormalize=True,
use_grouped_topk=False,
scoring_func="sigmoid",
correction_bias=self.gate.e_score_correction_bias,
)
# HF safetensors key is singular `shared_expert.…`; mirror so the
# default loader picks it up without remapping.
self.shared_expert = LagunaMLP(
hidden_size=config.hidden_size,
intermediate_size=config.shared_expert_intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
reduce_results=False,
prefix=add_prefix("shared_expert", prefix),
)
def get_moe_weights(self):
return [x.data for x in self.experts.parameters()]
def forward(
self,
hidden_states: torch.Tensor,
forward_batch: Optional[ForwardBatch] = None,
should_allreduce_fusion: bool = False,
use_reduce_scatter: bool = False,
) -> torch.Tensor:
if hidden_states.shape[0] == 0:
return hidden_states
shared_out = self.shared_expert(hidden_states)
router_logits = self.gate(hidden_states)
if self.router_logit_softcapping > 0.0:
cap = self.router_logit_softcapping
router_logits = torch.tanh(router_logits / cap) * cap
topk_output = self.topk(hidden_states, router_logits)
routed_out = self.experts(hidden_states, topk_output)
# Non-grouped TopK doesn't honor apply_routed_scaling_factor_on_output,
# so scale routed manually before adding the unscaled shared expert.
if self.routed_scaling_factor != 1.0:
routed_out = routed_out * self.routed_scaling_factor
final = routed_out + shared_out
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
is_tp_path=True,
use_reduce_scatter=use_reduce_scatter,
should_allreduce_fusion=should_allreduce_fusion,
):
final = tensor_model_parallel_all_reduce(final)
return final
class LagunaAttention(nn.Module):
def __init__(
self,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
head_dim: int,
layer_id: int,
rms_norm_eps: float,
rope_theta: float,
rope_scaling: Optional[Dict[str, Any]],
partial_rotary_factor: float,
max_position_embeddings: int,
attention_bias: bool,
sliding_window_size: int,
layer_type: str,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = hidden_size
self.head_dim = head_dim
self.layer_id = layer_id
attn_tp_rank = get_attention_tp_rank()
attn_tp_size = get_attention_tp_size()
self.total_num_heads = num_heads
assert self.total_num_heads % attn_tp_size == 0
self.num_heads = self.total_num_heads // attn_tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= attn_tp_size:
assert self.total_num_kv_heads % attn_tp_size == 0
else:
assert attn_tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // attn_tp_size)
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.qkv_proj = QKVParallelLinear(
hidden_size,
self.head_dim,
self.total_num_heads,
self.total_num_kv_heads,
bias=attention_bias,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=attn_tp_size,
prefix=add_prefix("qkv_proj", prefix),
)
self.o_proj = RowParallelLinear(
self.total_num_heads * self.head_dim,
hidden_size,
bias=attention_bias,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=attn_tp_size,
reduce_results=False,
prefix=add_prefix("o_proj", prefix),
)
# Per-head softplus gate (`gating=True` in HF). Shard like Q so the
# local output dim matches `num_heads`.
self.g_proj = ColumnParallelLinear(
hidden_size,
self.total_num_heads,
bias=False,
gather_output=False,
quant_config=None,
tp_rank=attn_tp_rank,
tp_size=attn_tp_size,
prefix=add_prefix("g_proj", prefix),
)
self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.rotary_emb = get_rope(
self.head_dim,
rotary_dim=self.head_dim,
max_position=max_position_embeddings,
base=int(rope_theta),
rope_scaling=rope_scaling,
partial_rotary_factor=partial_rotary_factor,
)
assert layer_type in {"sliding_attention", "full_attention"}
use_sliding = layer_type == "sliding_attention"
self.attn = RadixAttention(
self.num_heads,
self.head_dim,
self.scaling,
num_kv_heads=self.num_kv_heads,
layer_id=layer_id,
quant_config=quant_config,
prefix=add_prefix("attn", prefix),
sliding_window_size=sliding_window_size if use_sliding else -1,
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
forward_batch: ForwardBatch,
) -> torch.Tensor:
if hidden_states.shape[0] == 0:
return hidden_states
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = apply_qk_norm(
q=q,
k=k,
q_norm=self.q_norm,
k_norm=self.k_norm,
head_dim=self.head_dim,
)
q, k = self.rotary_emb(positions, q, k)
attn_output = self.attn(q, k, v, forward_batch)
gate, _ = self.g_proj(hidden_states)
gate = F.softplus(gate.float()).to(attn_output.dtype)
attn_output = attn_output.view(-1, self.num_heads, self.head_dim)
attn_output = attn_output * gate.view(-1, self.num_heads, 1)
attn_output = attn_output.reshape(-1, self.num_heads * self.head_dim)
output, _ = self.o_proj(attn_output)
return output
class LagunaDecoderLayer(nn.Module):
def __init__(
self,
config: LagunaConfig,
layer_id: int,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.layer_id = layer_id
self.hidden_size = config.hidden_size
layer_types = config.layer_types
layer_type = layer_types[layer_id]
is_swa = layer_type == "sliding_attention"
layer_num_heads = config.num_attention_heads_per_layer[layer_id]
if is_swa:
rope_theta = config.swa_rope_theta
rope_scaling = config.swa_rope_scaling
partial_rotary_factor = config.swa_partial_rotary_factor
else:
rope_theta = config.rope_theta
rope_scaling = config.full_rope_scaling
partial_rotary_factor = config.partial_rotary_factor
self.self_attn = LagunaAttention(
hidden_size=self.hidden_size,
num_heads=layer_num_heads,
num_kv_heads=config.num_key_value_heads,
head_dim=config.head_dim,
layer_id=layer_id,
rms_norm_eps=config.rms_norm_eps,
rope_theta=rope_theta,
rope_scaling=rope_scaling,
partial_rotary_factor=partial_rotary_factor,
max_position_embeddings=config.max_position_embeddings,
attention_bias=config.attention_bias,
# SGLang's window is exclusive; HF's `sliding_window` is inclusive.
sliding_window_size=config.sliding_window - 1,
layer_type=layer_type,
quant_config=quant_config,
prefix=add_prefix("self_attn", prefix),
)
mlp_types = config.mlp_layer_types
self.is_layer_sparse = mlp_types[layer_id] == "sparse"
is_previous_layer_sparse = layer_id > 0 and mlp_types[layer_id - 1] == "sparse"
is_next_layer_sparse = (
layer_id + 1 < config.num_hidden_layers
and mlp_types[layer_id + 1] == "sparse"
)
if self.is_layer_sparse:
self.mlp = LagunaMoE(
config=config,
layer_id=layer_id,
quant_config=quant_config,
prefix=add_prefix("mlp", prefix),
)
else:
self.mlp = LagunaMLP(
hidden_size=self.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
reduce_results=True,
prefix=add_prefix("mlp", prefix),
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.layer_scatter_modes = LayerScatterModes.init_new(
layer_id=layer_id,
num_layers=config.num_hidden_layers,
is_layer_sparse=self.is_layer_sparse,
is_previous_layer_sparse=is_previous_layer_sparse,
is_next_layer_sparse=is_next_layer_sparse,
)
self.layer_communicator = LayerCommunicator(
layer_scatter_modes=self.layer_scatter_modes,
input_layernorm=self.input_layernorm,
post_attention_layernorm=self.post_attention_layernorm,
allow_reduce_scatter=True,
is_last_layer=(layer_id == config.num_hidden_layers - 1),
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
forward_batch: ForwardBatch,
residual: Optional[torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor]:
hidden_states, residual = self.layer_communicator.prepare_attn(
hidden_states, residual, forward_batch
)
if hidden_states.shape[0] != 0:
hidden_states = self.self_attn(
positions=positions,
hidden_states=hidden_states,
forward_batch=forward_batch,
)
hidden_states, residual = self.layer_communicator.prepare_mlp(
hidden_states, residual, forward_batch
)
should_allreduce_fusion = (
self.layer_communicator.should_fuse_mlp_allreduce_with_next_layer(
forward_batch
)
)
use_reduce_scatter = self.layer_communicator.should_use_reduce_scatter(
forward_batch
)
hidden_states = self.mlp(
hidden_states,
forward_batch=forward_batch,
should_allreduce_fusion=should_allreduce_fusion,
use_reduce_scatter=use_reduce_scatter,
)
if should_allreduce_fusion:
hidden_states._sglang_needs_allreduce_fusion = True
else:
hidden_states, residual = self.layer_communicator.postprocess_layer(
hidden_states, residual, forward_batch
)
return hidden_states, residual
class LagunaModel(nn.Module):
def __init__(
self,
config: LagunaConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
decoder_layer_type: type = LagunaDecoderLayer,
) -> None:
super().__init__()
self.config = config
self.padding_idx = getattr(config, "pad_token_id", None)
self.vocab_size = config.vocab_size
self.pp_group = get_pp_group()
if self.pp_group.is_first_rank:
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
use_attn_tp_group=is_dp_attention_enabled(),
prefix=add_prefix("embed_tokens", prefix),
)
else:
self.embed_tokens = PPMissingLayer()
decoder_layer_type = decoder_layer_type or LagunaDecoderLayer
self.layers, self.start_layer, self.end_layer = make_layers(
config.num_hidden_layers,
lambda idx, prefix: decoder_layer_type(
layer_id=idx,
config=config,
quant_config=quant_config,
prefix=prefix,
),
pp_rank=self.pp_group.rank_in_group,
pp_size=self.pp_group.world_size,
prefix=add_prefix("layers", prefix),
)
if self.pp_group.is_last_rank:
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
else:
self.norm = PPMissingLayer(return_tuple=True)
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
input_embeds: torch.Tensor = None,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> Union[torch.Tensor, PPProxyTensors]:
if self.pp_group.is_first_rank:
if input_embeds is None:
hidden_states = self.embed_tokens(input_ids)
else:
hidden_states = input_embeds
residual = None
else:
assert pp_proxy_tensors is not None
hidden_states = pp_proxy_tensors["hidden_states"]
residual = pp_proxy_tensors["residual"]
for i in range(self.start_layer, self.end_layer):
layer = self.layers[i]
hidden_states, residual = layer(
positions, hidden_states, forward_batch, residual
)
if not self.pp_group.is_last_rank:
return PPProxyTensors(
{"hidden_states": hidden_states, "residual": residual}
)
if hidden_states.shape[0] != 0:
if residual is None:
hidden_states = self.norm(hidden_states)
else:
hidden_states, _ = self.norm(hidden_states, residual)
return hidden_states
class LagunaForCausalLM(nn.Module):
fall_back_to_pt_during_load = False
packed_modules_mapping = {
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
"gate_up_proj": ["gate_proj", "up_proj"],
}
def __init__(
self,
config: LagunaConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.pp_group = get_pp_group()
self.config = config
self.model = LagunaModel(
config, quant_config=quant_config, prefix=add_prefix("model", prefix)
)
if self.pp_group.is_last_rank:
self.lm_head = ParallelLMHead(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
prefix=add_prefix("lm_head", prefix),
use_attn_tp_group=get_global_server_args().enable_dp_lm_head,
)
else:
self.lm_head = PPMissingLayer()
self.logits_processor = LogitsProcessor(config)
# Only walk this rank's local layers — out-of-range entries can be PPMissingLayer.
self._routed_experts_weights_of_layer = LazyValue(
lambda: {
layer_id: self.model.layers[layer_id].mlp.get_moe_weights()
for layer_id in range(self.start_layer, self.end_layer)
if isinstance(self.model.layers[layer_id].mlp, LagunaMoE)
}
)
@property
def routed_experts_weights_of_layer(self):
return self._routed_experts_weights_of_layer.value
@property
def start_layer(self):
return self.model.start_layer
@property
def end_layer(self):
return self.model.end_layer
@torch.no_grad()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
input_embeds: torch.Tensor = None,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> torch.Tensor:
hidden_states = self.model(
input_ids,
positions,
forward_batch,
input_embeds,
pp_proxy_tensors=pp_proxy_tensors,
)
if self.pp_group.is_last_rank:
return self.logits_processor(
input_ids, hidden_states, self.lm_head, forward_batch
)
return hidden_states
def get_input_embeddings(self) -> nn.Embedding:
return self.model.embed_tokens
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
stacked_params_mapping = [
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
]
expert_params_mapping = FusedMoE.make_expert_params_mapping(
ckpt_gate_proj_name="gate_proj",
ckpt_down_proj_name="down_proj",
ckpt_up_proj_name="up_proj",
num_experts=self.config.num_experts,
)
params_dict = dict(self.named_parameters())
# (layer, expert, shard) tuples that hit the per-expert loader,
# cross-checked against `expected` below to fail on dropped weights.
loaded_expert_shards: set[Tuple[int, int, str]] = set()
moe_layer_ids = [
i
for i, mt in enumerate(self.config.mlp_layer_types)
if mt == "sparse" and self.start_layer <= i < self.end_layer
]
for name, loaded_weight in weights:
layer_id = get_layer_id(name)
if layer_id is not None and (
layer_id < self.start_layer or layer_id >= self.end_layer
):
continue
if "rotary_emb.inv_freq" in name:
continue
if self.config.tie_word_embeddings and "lm_head.weight" in name:
continue
# HF stores the router correction bias under the experts namespace;
# our parameter lives on the gate. Remap before dispatch.
if name.endswith("mlp.experts.e_score_correction_bias"):
name = name.replace(
"mlp.experts.e_score_correction_bias",
"mlp.gate.e_score_correction_bias",
)
# Stacked dense (QKV / gate_up). The `mlp.experts.` guard stops
# `up_proj` substring from false-matching `experts.{i}.up_proj.weight`.
matched_stacked = False
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if "mlp.experts." in name:
continue
name_mapped = name.replace(weight_name, param_name)
if name_mapped.endswith(".bias") and name_mapped not in params_dict:
continue
if name_mapped not in params_dict:
continue
param = params_dict[name_mapped]
param.weight_loader(param, loaded_weight, shard_id)
matched_stacked = True
break
if matched_stacked:
continue
matched_expert = False
for param_name, weight_name, expert_id, shard_id in expert_params_mapping:
if weight_name not in name:
continue
name_mapped = name.replace(weight_name, param_name)
if name_mapped not in params_dict:
continue
param = params_dict[name_mapped]
param.weight_loader(
param,
loaded_weight,
name,
shard_id=shard_id,
expert_id=expert_id,
)
if layer_id is not None:
loaded_expert_shards.add((layer_id, expert_id, shard_id))
matched_expert = True
break
if matched_expert:
continue
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
logger.warning("Parameter %s not found in params_dict", name)
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
# If any routed-expert tensor was silently dropped (e.g. a future
# checkpoint renaming `gate_proj`, or a ckpt-vs-mapping shape mismatch),
# fail loud here instead of generating garbage.
expected = {
(layer_id, expert_id, shard_id)
for layer_id in moe_layer_ids
for expert_id in range(self.config.num_experts)
for shard_id in ("w1", "w2", "w3")
}
missing = expected - loaded_expert_shards
if missing:
sample = sorted(missing)[:5]
raise RuntimeError(
f"{len(missing)} routed-expert tensors were not loaded "
f"(sample: {sample}). Expected {len(expected)} (layers={moe_layer_ids}, "
f"num_experts={self.config.num_experts}, shards=3)."
)
def get_embed_and_head(self):
return self.model.embed_tokens.weight, self.lm_head.weight
def set_embed_and_head(self, embed, head):
del self.model.embed_tokens.weight
del self.lm_head.weight
self.model.embed_tokens.weight = embed
self.lm_head.weight = head
torch.cuda.empty_cache()
torch.cuda.synchronize()
EntryClass = LagunaForCausalLM
@@ -587,6 +587,15 @@ class _MimoDetector(Qwen3Detector):
self.reasoning_default = "explicit_enable_thinking"
class _PoolsideV1Detector(Qwen3Detector):
"""Poolside v1 (Laguna-XS.2) reuses Qwen3 <think> tokens but the HF chat template
defaults `enable_thinking=False`; reasoning is opt-in via `enable_thinking=True`."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.reasoning_default = "explicit_enable_thinking"
class ReasoningParser:
"""
Parser that handles both streaming and non-streaming scenarios for extracting
@@ -608,6 +617,7 @@ class ReasoningParser:
"kimi": KimiDetector,
"kimi_k2": KimiK2Detector,
"mimo": _MimoDetector,
"poolside_v1": _PoolsideV1Detector,
"qwen3": Qwen3Detector,
"qwen3-thinking": Qwen3Detector,
"minimax": Qwen3Detector,
@@ -37,6 +37,7 @@ from sglang.srt.configs import (
KimiK25Config,
KimiLinearConfig,
KimiVLConfig,
LagunaConfig,
LongcatFlashConfig,
MultiModalityConfig,
NemotronH_Nano_Omni_Reasoning_V3_Config,
@@ -79,6 +80,7 @@ _CONFIG_REGISTRY: Dict[str, Type[PretrainedConfig]] = {
MultiModalityConfig,
KimiVLConfig,
InternVLChatConfig,
LagunaConfig,
Step3VLConfig,
LongcatFlashConfig,
Olmo3Config,