From 8de026a781d9a8620c879f9e76ca21d31ab02be0 Mon Sep 17 00:00:00 2001 From: David Wang Date: Thu, 30 Jul 2026 22:42:08 +0000 Subject: [PATCH 01/88] dflash sliding window layer support, fix eager backend --- docs/basic_usage/training.md | 25 +++ .../algorithms/common/dflash_family_model.py | 78 ++++--- specforge/algorithms/model_providers.py | 37 +++- specforge/modeling/draft/dflash.py | 83 +++++++- .../test_dflash_eager_attention.py | 120 +++++++++++ tests/test_modeling/test_dflash_sliding.py | 192 ++++++++++++++++++ tests/test_runtime/_fixtures.py | 3 + tests/test_runtime/test_model_loading.py | 33 +++ tests/test_utils/test_dflash_losses.py | 1 + tests/test_utils/test_dflash_mask.py | 106 ++++++++-- 10 files changed, 624 insertions(+), 54 deletions(-) create mode 100644 tests/test_modeling/test_dflash_eager_attention.py create mode 100644 tests/test_modeling/test_dflash_sliding.py diff --git a/docs/basic_usage/training.md b/docs/basic_usage/training.md index 9093cc8cf..21606cf6c 100644 --- a/docs/basic_usage/training.md +++ b/docs/basic_usage/training.md @@ -154,6 +154,31 @@ model: draft_block_size: 8 # DFlash only ``` +For DFlash, configure the attention layout in the referenced draft JSON. Each +entry corresponds to one draft layer; sliding layers share one positive window: + +```json +{ + "num_hidden_layers": 5, + "layer_types": [ + "sliding_attention", + "sliding_attention", + "sliding_attention", + "sliding_attention", + "full_attention" + ], + "use_sliding_window": true, + "sliding_window": 2048 +} +``` + +Use `"full_attention"` for every entry, `"use_sliding_window": false`, and +`"sliding_window": null` for a full-only draft. The layout length must equal +`num_hidden_layers`. A layer-count override may resize a uniform layout, but a +mixed layout must be edited explicitly in the draft JSON. + +The `eager`, `sdpa`, and `flex_attention` backends support both layouts. + Domino and DSpark need their projector/head metadata, so they require an explicit draft config (or a pretrained warm-start source that contains `config.json`). The old Domino parser exposed an optional config flag, but its diff --git a/specforge/algorithms/common/dflash_family_model.py b/specforge/algorithms/common/dflash_family_model.py index 85b530641..e61578199 100644 --- a/specforge/algorithms/common/dflash_family_model.py +++ b/specforge/algorithms/common/dflash_family_model.py @@ -44,7 +44,15 @@ def compute_accept_len( return accept_prefix.sum(dim=2).float() -def create_dflash_sdpa_mask(anchor_positions, block_keep_mask, S, block_size, device): +def create_dflash_sdpa_mask( + anchor_positions, + block_keep_mask, + S, + block_size, + device, + sliding_window: Optional[int] = None, +): + """Construct a full or sliding dense boolean DFlash mask.""" B, N = anchor_positions.shape Q_LEN = N * block_size KV_LEN = S + N * block_size @@ -55,16 +63,24 @@ def create_dflash_sdpa_mask(anchor_positions, block_keep_mask, S, block_size, de ) # (1, 1, 1, KV_LEN) q_block_ids = q_indices // block_size + q_block_offsets = q_indices % block_size anchor_expanded = anchor_positions.view(B, 1, N, 1).repeat_interleave( block_size, dim=2 ) mask_context = (kv_indices < S) & (kv_indices < anchor_expanded) + if sliding_window is not None: + # The current draft token occupies one slot in the window. + context_lower_bound = anchor_expanded + q_block_offsets - (sliding_window - 1) + mask_context = mask_context & (kv_indices >= context_lower_bound) is_draft = kv_indices >= S kv_block_ids = (kv_indices - S) // block_size mask_draft = is_draft & (q_block_ids == kv_block_ids) + if sliding_window is not None: + kv_block_offsets = (kv_indices - S) % block_size + mask_draft = mask_draft & (kv_block_offsets <= q_block_offsets) valid_block = block_keep_mask.view(B, 1, N, 1).repeat_interleave(block_size, dim=2) @@ -78,21 +94,13 @@ def create_dflash_block_mask( S: int, block_size: int, device: torch.device, + sliding_window: Optional[int] = None, ): - """Construct Flex Attention BlockMask for DFlash training. - - KV: [Context (S tokens) | Block_0 | Block_1 | ... | Block_{n-1}] - Q: [Block_0 | Block_1 | ... | Block_{n-1}] - - Rules: - 1. Each block sees context strictly before its anchor (kv_idx < anchor_pos). - 2. Intra-block attention is bidirectional. - 3. Different blocks are invisible to each other. - 4. Invalid blocks (block_keep_mask=False) see nothing. - """ + """Construct a full or sliding Flex Attention mask for DFlash training.""" def dflash_mask_mod(b, h, q_idx, kv_idx): q_block_id = q_idx // block_size + q_block_offset = q_idx % block_size safe_q_block_id = q_block_id.clamp(max=N - 1) anchor_pos = anchor_positions[b, safe_q_block_id] @@ -100,10 +108,17 @@ def dflash_mask_mod(b, h, q_idx, kv_idx): # Strictly less than: matches inference where target_hidden[anchor_pos] # is not available as context. mask_context = is_context & (kv_idx < anchor_pos) + if sliding_window is not None: + # The current draft token occupies one slot in the window. + context_lower_bound = anchor_pos + q_block_offset - (sliding_window - 1) + mask_context = mask_context & (kv_idx >= context_lower_bound) is_draft = kv_idx >= S kv_block_id = (kv_idx - S) // block_size mask_draft = is_draft & (q_block_id == kv_block_id) + if sliding_window is not None: + kv_block_offset = (kv_idx - S) % block_size + mask_draft = mask_draft & (kv_block_offset <= q_block_offset) is_valid_block = block_keep_mask[b, safe_q_block_id] in_bounds = q_block_id < N @@ -287,22 +302,29 @@ def _forward_draft_blocks( draft_position_ids = self._create_position_ids(anchor_positions) full_position_ids = torch.cat([context_position_ids, draft_position_ids], dim=1) - if self.attention_backend == "flex_attention": - dflash_attn_mask = create_dflash_block_mask( - anchor_positions=anchor_positions, - block_keep_mask=block_keep_mask, - S=seq_len, - block_size=self.block_size, - device=device, - ) - else: - dflash_attn_mask = create_dflash_sdpa_mask( - anchor_positions=anchor_positions, - block_keep_mask=block_keep_mask, - S=seq_len, - block_size=self.block_size, - device=device, - ) + mask_builder = ( + create_dflash_block_mask + if self.attention_backend == "flex_attention" + else create_dflash_sdpa_mask + ) + mask_args = { + "anchor_positions": anchor_positions, + "block_keep_mask": block_keep_mask, + "S": seq_len, + "block_size": self.block_size, + "device": device, + } + full_attn_mask = mask_builder(**mask_args) + sliding_window = self.draft_model.sliding_window + dflash_attn_mask = full_attn_mask + if sliding_window is not None: + dflash_attn_mask = { + "full_attention": full_attn_mask, + "sliding_attention": mask_builder( + **mask_args, + sliding_window=sliding_window, + ), + } output_hidden = self.draft_model( position_ids=full_position_ids, diff --git a/specforge/algorithms/model_providers.py b/specforge/algorithms/model_providers.py index 61eb1acea..b48e81cb1 100644 --- a/specforge/algorithms/model_providers.py +++ b/specforge/algorithms/model_providers.py @@ -457,22 +457,41 @@ def populate_dflash_generated_config( ) payload["num_target_layers"] = target_layers payload["block_size"] = 16 + payload["layer_types"] = ["full_attention"] * int(payload["num_hidden_layers"]) + payload["sliding_window"] = None + payload["use_sliding_window"] = False payload["dflash_config"] = { "target_layer_ids": build_target_layer_ids(target_layers, 1) } def apply_dflash_overrides(cfg: Config, draft_config: Any) -> None: - if cfg.model.draft_num_hidden_layers is None: - return - from specforge.modeling.draft.dflash import build_target_layer_ids - - target_layers = int(draft_config.num_target_layers) - method_config = dict(getattr(draft_config, "dflash_config", None) or {}) - method_config["target_layer_ids"] = build_target_layer_ids( - target_layers, cfg.model.draft_num_hidden_layers + from specforge.modeling.draft.dflash import ( + build_target_layer_ids, + resolve_dflash_attention_layout, ) - draft_config.dflash_config = method_config + + requested_layers = cfg.model.draft_num_hidden_layers + if requested_layers is not None: + layer_types = draft_config.layer_types + if len(layer_types) != requested_layers: + if len(set(layer_types)) > 1: + raise ValueError( + "model.draft_num_hidden_layers cannot resize a mixed " + "DFlash layer_types layout; provide a draft config with " + "exactly the requested per-layer layout" + ) + draft_config.layer_types = [layer_types[0]] * requested_layers + + draft_config.dflash_config = { + **dict(getattr(draft_config, "dflash_config", None) or {}), + "target_layer_ids": build_target_layer_ids( + int(draft_config.num_target_layers), + requested_layers, + ), + } + + resolve_dflash_attention_layout(draft_config) __all__ = [ diff --git a/specforge/modeling/draft/dflash.py b/specforge/modeling/draft/dflash.py index 719b341ad..4fea605bd 100644 --- a/specforge/modeling/draft/dflash.py +++ b/specforge/modeling/draft/dflash.py @@ -20,6 +20,10 @@ from .dflash_kernels import DEFAULT_DFLASH_KERNELS, DFlashKernels from .registry import register_draft +FULL_ATTENTION = "full_attention" +SLIDING_ATTENTION = "sliding_attention" +_VALID_DFLASH_LAYER_TYPES = {FULL_ATTENTION, SLIDING_ATTENTION} + def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor: if temperature < 1e-5: @@ -31,6 +35,39 @@ def sample(logits: torch.Tensor, temperature: float = 0.0) -> torch.Tensor: return torch.multinomial(probs, num_samples=1).view(bsz, seq_len) +def resolve_dflash_attention_layout( + config: Qwen3Config, +) -> tuple[tuple[str, ...], Optional[int]]: + """Validate and return the configured per-layer DFlash attention layout.""" + + num_hidden_layers = config.num_hidden_layers + layer_types = tuple(config.layer_types) + + if len(layer_types) != num_hidden_layers: + raise ValueError( + "DFlash config.layer_types must contain exactly " + f"num_hidden_layers={num_hidden_layers} entries, got " + f"{len(layer_types)}" + ) + invalid = set(layer_types) - _VALID_DFLASH_LAYER_TYPES + if invalid: + raise ValueError( + "DFlash config.layer_types supports only full_attention and " + f"sliding_attention, got {sorted(invalid)}" + ) + + if SLIDING_ATTENTION not in layer_types: + return layer_types, None + + sliding_window = config.sliding_window + if sliding_window is None or sliding_window <= 0: + raise ValueError( + "DFlash sliding_attention layers require use_sliding_window=true " + "and a positive config.sliding_window" + ) + return layer_types, sliding_window + + def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): cos = cos.unsqueeze(unsqueeze_dim) sin = sin.unsqueeze(unsqueeze_dim) @@ -40,6 +77,23 @@ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): return q_embed, k_embed +def _prepare_dflash_eager_mask( + attention_mask: Optional[torch.Tensor], + dtype: torch.dtype, +) -> tuple[Optional[torch.Tensor], Optional[torch.Tensor]]: + """Convert a boolean allow-mask to eager's additive representation.""" + + if attention_mask is None or attention_mask.dtype != torch.bool: + return attention_mask, None + + valid_queries = attention_mask.any(dim=-1, keepdim=True) + # A finite minimum keeps eager softmax stable. Fully masked query rows are + # explicitly zeroed after attention so they cannot average forbidden values. + additive_mask = torch.zeros_like(attention_mask, dtype=dtype) + additive_mask.masked_fill_(~attention_mask, torch.finfo(dtype).min) + return additive_mask, valid_queries + + class Qwen3DFlashAttention(nn.Module): """Multi-headed attention from 'Attention Is All You Need' paper""" @@ -85,7 +139,7 @@ def __init__( self.k_norm = kernels.make_rms_norm(self.head_dim, config.rms_norm_eps) self.sliding_window = ( config.sliding_window - if config.layer_types[layer_idx] == "sliding_attention" + if config.layer_types[layer_idx] == SLIDING_ATTENTION else None ) @@ -121,8 +175,14 @@ def forward( if past_key_values is not None: cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs) + valid_queries = None attn_fn: Callable = eager_attention_forward - if self.config._attn_implementation != "eager": + if self.config._attn_implementation == "eager": + attention_mask, valid_queries = _prepare_dflash_eager_mask( + attention_mask, + q.dtype, + ) + else: attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] attn_output, attn_weights = attn_fn( self, @@ -135,8 +195,15 @@ def forward( sliding_window=self.sliding_window, **kwargs, ) + if valid_queries is not None and attn_weights is not None: + attn_weights = attn_weights.masked_fill(~valid_queries, 0) attn_output = attn_output.reshape(bsz, q_len, -1) attn_output = self.o_proj(attn_output) + if valid_queries is not None: + attn_output = attn_output.masked_fill( + ~valid_queries.any(dim=1), + 0, + ) return attn_output, attn_weights @@ -277,6 +344,7 @@ def __init__( ) -> None: super().__init__(config) self.config = config + self.layer_types, self.sliding_window = resolve_dflash_attention_layout(config) kernels = dflash_kernels or DEFAULT_DFLASH_KERNELS self.layers = nn.ModuleList( [ @@ -363,7 +431,7 @@ def _sample_draft_tokens( def forward( self, position_ids: torch.LongTensor, - attention_mask: Optional[torch.Tensor] = None, + attention_mask: Optional[object] = None, noise_embedding: Optional[torch.Tensor] = None, target_hidden: Optional[torch.Tensor] = None, past_key_values: Optional[Cache] = None, @@ -373,11 +441,16 @@ def forward( hidden_states = noise_embedding target_hidden = self.hidden_norm(self.fc(target_hidden)) position_embeddings = self.rotary_emb(hidden_states, position_ids) - for layer in self.layers: + for layer_type, layer in zip(self.layer_types, self.layers): + layer_attention_mask = ( + attention_mask[layer_type] + if isinstance(attention_mask, dict) + else attention_mask + ) hidden_states = layer( hidden_states=hidden_states, target_hidden=target_hidden, - attention_mask=attention_mask, + attention_mask=layer_attention_mask, position_ids=position_ids, past_key_value=past_key_values, use_cache=use_cache, diff --git a/tests/test_modeling/test_dflash_eager_attention.py b/tests/test_modeling/test_dflash_eager_attention.py new file mode 100644 index 000000000..ee1e8d60d --- /dev/null +++ b/tests/test_modeling/test_dflash_eager_attention.py @@ -0,0 +1,120 @@ +import unittest + +import torch +from torch import nn +from torch.testing import assert_close +from transformers import Qwen3Config + +from specforge.algorithms.common.dflash_family_model import create_dflash_sdpa_mask +from specforge.modeling.draft.dflash import Qwen3DFlashAttention +from specforge.modeling.draft.dflash_kernels import DFlashKernels + + +def _make_attention(layer_type, implementation, sliding_window): + config = Qwen3Config( + hidden_size=8, + intermediate_size=16, + num_attention_heads=2, + num_key_value_heads=1, + num_hidden_layers=1, + head_dim=4, + max_position_embeddings=64, + vocab_size=32, + layer_types=[layer_type], + sliding_window=sliding_window, + use_sliding_window=sliding_window is not None, + attention_bias=False, + attention_dropout=0.0, + ) + config._attn_implementation = implementation + kernels = DFlashKernels( + make_rms_norm=lambda *_: nn.Identity(), + make_mlp=lambda *_: nn.Identity(), + ) + return Qwen3DFlashAttention(config, layer_idx=0, kernels=kernels).eval() + + +def _forward(attention, hidden_states, target_hidden, attention_mask): + total_length = target_hidden.shape[1] + hidden_states.shape[1] + position_embeddings = ( + hidden_states.new_ones(1, total_length, attention.head_dim), + hidden_states.new_zeros(1, total_length, attention.head_dim), + ) + return attention( + hidden_states=hidden_states, + target_hidden=target_hidden, + position_embeddings=position_embeddings, + attention_mask=attention_mask, + ) + + +class TestDFlashEagerAttentionMasking(unittest.TestCase): + def test_eager_matches_sdpa_for_full_and_sliding_masks(self): + for layer_type, sliding_window in ( + ("full_attention", None), + ("sliding_attention", 2), + ): + with self.subTest(layer_type=layer_type): + torch.manual_seed(17) + eager = _make_attention(layer_type, "eager", sliding_window) + sdpa = _make_attention(layer_type, "sdpa", sliding_window) + sdpa.load_state_dict(eager.state_dict()) + + mask = create_dflash_sdpa_mask( + anchor_positions=torch.tensor([[2, 4]]), + block_keep_mask=torch.tensor([[True, False]]), + S=4, + block_size=2, + device=torch.device("cpu"), + sliding_window=sliding_window, + ) + eager_hidden = torch.randn(1, 4, 8, requires_grad=True) + eager_target = torch.randn(1, 4, 8, requires_grad=True) + sdpa_hidden = eager_hidden.detach().clone().requires_grad_(True) + sdpa_target = eager_target.detach().clone().requires_grad_(True) + + eager_output, eager_weights = _forward( + eager, + eager_hidden, + eager_target, + mask, + ) + sdpa_output, _ = _forward( + sdpa, + sdpa_hidden, + sdpa_target, + mask, + ) + assert_close(eager_output, sdpa_output, rtol=1e-5, atol=1e-6) + + allowed = mask.expand_as(eager_weights) + forbidden_weights = eager_weights.masked_select(~allowed) + assert_close( + forbidden_weights, + torch.zeros_like(forbidden_weights), + rtol=0, + atol=0, + ) + invalid_rows = ~mask.any(dim=-1).squeeze(1) + assert_close( + eager_output[invalid_rows], + torch.zeros_like(eager_output[invalid_rows]), + rtol=0, + atol=0, + ) + + output_grad = torch.randn_like(eager_output) + eager_grads = torch.autograd.grad( + (eager_output * output_grad).sum(), + (eager_hidden, eager_target), + ) + sdpa_grads = torch.autograd.grad( + (sdpa_output * output_grad).sum(), + (sdpa_hidden, sdpa_target), + ) + for eager_grad, sdpa_grad in zip(eager_grads, sdpa_grads): + assert_close(eager_grad, sdpa_grad, rtol=1e-5, atol=1e-6) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_modeling/test_dflash_sliding.py b/tests/test_modeling/test_dflash_sliding.py new file mode 100644 index 000000000..bdaa8e1ff --- /dev/null +++ b/tests/test_modeling/test_dflash_sliding.py @@ -0,0 +1,192 @@ +import unittest +from pathlib import Path +from unittest import mock + +import torch +from torch import nn +from transformers import Qwen3Config + +from specforge.algorithms.common.dflash_family_model import OnlineDFlashModel +from specforge.modeling.draft.dflash import ( + DFlashDraftModel, + resolve_dflash_attention_layout, +) + + +def _draft_config(layer_types, sliding_window=None): + config = Qwen3Config( + architectures=["DFlashDraftModel"], + block_size=2, + hidden_size=8, + intermediate_size=16, + num_attention_heads=2, + num_key_value_heads=1, + num_hidden_layers=len(layer_types), + num_target_layers=6, + head_dim=4, + max_position_embeddings=64, + vocab_size=32, + layer_types=list(layer_types), + sliding_window=sliding_window, + use_sliding_window=sliding_window is not None, + ) + config._attn_implementation = "sdpa" + return config + + +class _CaptureLayer(nn.Module): + def __init__(self): + super().__init__() + self.attention_mask = None + + def forward(self, *, hidden_states, attention_mask, **_): + self.attention_mask = attention_mask + return hidden_states + + +class _RotaryStub(nn.Module): + def forward(self, *_): + return (torch.empty(0), torch.empty(0)) + + +def _capture_model(layer_types, sliding_window=None): + model = DFlashDraftModel(_draft_config(layer_types, sliding_window)) + capture_layers = [_CaptureLayer() for _ in layer_types] + model.layers = nn.ModuleList(capture_layers) + model.fc = nn.Identity() + model.hidden_norm = nn.Identity() + model.norm = nn.Identity() + model.rotary_emb = _RotaryStub() + return model, capture_layers + + +def _forward(model, attention_mask): + noise_embedding = torch.randn(1, 2, model.config.hidden_size) + target_hidden = torch.randn(1, 4, model.config.hidden_size) + position_ids = torch.arange(6).unsqueeze(0) + return model( + position_ids=position_ids, + noise_embedding=noise_embedding, + target_hidden=target_hidden, + attention_mask=attention_mask, + ) + + +class TestDFlashSlidingDispatch(unittest.TestCase): + def test_full_only_model_keeps_single_mask_compatibility(self): + model, layers = _capture_model(["full_attention", "full_attention"]) + full_mask = torch.tensor([1]) + + _forward(model, full_mask) + + self.assertIs(layers[0].attention_mask, full_mask) + self.assertIs(layers[1].attention_mask, full_mask) + + def test_online_wrapper_builds_both_masks_for_hybrid_model(self): + model, layers = _capture_model( + ["sliding_attention", "full_attention"], + sliding_window=4, + ) + wrapper = OnlineDFlashModel( + draft_model=model, + target_lm_head=nn.Identity(), + target_embed_tokens=nn.Embedding(32, model.config.hidden_size), + mask_token_id=31, + block_size=2, + attention_backend="sdpa", + num_anchors=1, + ) + anchors = torch.tensor([[2]]) + keep = torch.tensor([[True]]) + noise_embedding = torch.randn(1, 2, model.config.hidden_size) + full_mask = torch.tensor([1]) + sliding_mask = torch.tensor([2]) + + with ( + mock.patch.object( + wrapper, + "_sample_anchor_positions", + return_value=(anchors, keep), + ), + mock.patch.object( + wrapper, + "_create_noise_embed", + return_value=noise_embedding, + ), + mock.patch( + "specforge.algorithms.common.dflash_family_model." + "create_dflash_sdpa_mask", + side_effect=(full_mask, sliding_mask), + ) as create_mask, + ): + wrapper._forward_draft_blocks( + input_ids=torch.ones(1, 4, dtype=torch.long), + hidden_states=torch.randn(1, 4, model.config.hidden_size), + loss_mask=torch.ones(1, 4), + ) + + self.assertEqual(create_mask.call_count, 2) + self.assertIs(layers[0].attention_mask, sliding_mask) + self.assertIs(layers[1].attention_mask, full_mask) + + +class TestDFlashSlidingConfig(unittest.TestCase): + def test_checked_in_qwen36_config_preserves_hybrid_layout(self): + config_path = ( + Path(__file__).resolve().parents[2] / "configs" / "qwen3.6-27b-dflash.json" + ) + config = Qwen3Config.from_json_file(str(config_path)) + + layer_types, sliding_window = resolve_dflash_attention_layout(config) + + self.assertEqual( + list(layer_types), + [ + "sliding_attention", + "sliding_attention", + "sliding_attention", + "sliding_attention", + "full_attention", + ], + ) + self.assertEqual(sliding_window, 2048) + + def test_configures_attention_modules_from_layer_types(self): + model = DFlashDraftModel( + _draft_config( + ["sliding_attention", "full_attention", "sliding_attention"], + sliding_window=7, + ) + ) + + self.assertEqual( + list(model.layer_types), + ["sliding_attention", "full_attention", "sliding_attention"], + ) + self.assertEqual(model.sliding_window, 7) + self.assertEqual(model.layers[0].self_attn.sliding_window, 7) + self.assertIsNone(model.layers[1].self_attn.sliding_window) + self.assertEqual(model.layers[2].self_attn.sliding_window, 7) + + def test_rejects_invalid_attention_layouts(self): + cases = ( + (["full_attention"], None), + (["full_attention", "unknown"], None), + (["sliding_attention", "full_attention"], None), + (["sliding_attention", "full_attention"], 0), + (["sliding_attention", "full_attention"], -1), + ) + for layer_types, sliding_window in cases: + with self.subTest( + layer_types=layer_types, + sliding_window=sliding_window, + ): + config = _draft_config(["full_attention", "full_attention"]) + config.layer_types = layer_types + config.sliding_window = sliding_window + with self.assertRaises(ValueError): + resolve_dflash_attention_layout(config) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_runtime/_fixtures.py b/tests/test_runtime/_fixtures.py index ad850771f..6024685a8 100644 --- a/tests/test_runtime/_fixtures.py +++ b/tests/test_runtime/_fixtures.py @@ -308,6 +308,7 @@ def build_dflash( draft_config = AutoConfig.from_pretrained(target_dir) draft_config.num_hidden_layers = draft_layers + draft_config.layer_types = ["full_attention"] * draft_layers draft_config.block_size = block_size draft_config.num_target_layers = target_layers draft_config.dflash_config = {"mask_token_id": mask_token_id} @@ -375,6 +376,7 @@ def build_domino( draft_config = AutoConfig.from_pretrained(target_dir) draft_config.num_hidden_layers = draft_layers + draft_config.layer_types = ["full_attention"] * draft_layers draft_config.block_size = block_size draft_config.num_target_layers = target_layers draft_config.dflash_config = { @@ -445,6 +447,7 @@ def build_dspark( draft_config = AutoConfig.from_pretrained(target_dir) draft_config.num_hidden_layers = draft_layers + draft_config.layer_types = ["full_attention"] * draft_layers draft_config.block_size = block_size draft_config.num_target_layers = target_layers draft_config.dflash_config = { diff --git a/tests/test_runtime/test_model_loading.py b/tests/test_runtime/test_model_loading.py index 87f053957..38182bdab 100644 --- a/tests/test_runtime/test_model_loading.py +++ b/tests/test_runtime/test_model_loading.py @@ -157,6 +157,9 @@ def test_target_derived_defaults_match_legacy_trainers(self): self.assertEqual(resolved.block_size, block_size) self.assertEqual(resolved.num_target_layers, 12) self.assertEqual(len(resolved.dflash_config["target_layer_ids"]), 1) + self.assertEqual(resolved.layer_types, ["full_attention"]) + self.assertIsNone(resolved.sliding_window) + self.assertFalse(resolved.use_sliding_window) else: self.assertEqual(resolved.draft_vocab_size, 32000) @@ -180,6 +183,36 @@ def test_dflash_typed_overrides_rebuild_capture_layers(self): self.assertEqual(resolved.num_hidden_layers, 2) self.assertEqual(resolved.block_size, 8) self.assertEqual(len(resolved.dflash_config["target_layer_ids"]), 2) + self.assertEqual( + resolved.layer_types, + ["full_attention", "full_attention"], + ) + + def test_dflash_layer_override_rejects_ambiguous_hybrid_resize(self): + with tempfile.TemporaryDirectory() as directory: + path = os.path.join(directory, "draft.json") + payload = _draft_payload("DFlashDraftModel", layers=3, block_size=16) + payload.update( + layer_types=[ + "sliding_attention", + "sliding_attention", + "full_attention", + ], + sliding_window=128, + use_sliding_window=True, + ) + with open(path, "w", encoding="utf-8") as stream: + json.dump(payload, stream) + cfg = _run_config( + "dflash", + draft_model_config=path, + draft_num_hidden_layers=2, + ) + with self.assertRaisesRegex(ValueError, "mixed DFlash layer_types"): + resolve_draft_config( + cfg, + provider=_draft_config_provider("dflash"), + ) def test_local_json_and_directory_are_equivalent_sources(self): with tempfile.TemporaryDirectory() as directory: diff --git a/tests/test_utils/test_dflash_losses.py b/tests/test_utils/test_dflash_losses.py index 3a0457044..0e31adc8c 100644 --- a/tests/test_utils/test_dflash_losses.py +++ b/tests/test_utils/test_dflash_losses.py @@ -68,6 +68,7 @@ class _FixedDraft(nn.Module): def __init__(self, hidden_size: int): super().__init__() self.hidden_size = hidden_size + self.sliding_window = None def forward(self, position_ids, noise_embedding, target_hidden, attention_mask): bsz, draft_len = noise_embedding.shape[:2] diff --git a/tests/test_utils/test_dflash_mask.py b/tests/test_utils/test_dflash_mask.py index 8db5c732c..bdcd13a69 100644 --- a/tests/test_utils/test_dflash_mask.py +++ b/tests/test_utils/test_dflash_mask.py @@ -8,8 +8,15 @@ ) -def _reference_dflash_mask(anchor_positions, block_keep_mask, S, block_size, device): - """Element-level reference mask mirroring the mask_mod inside create_dflash_block_mask. +def _reference_dflash_mask( + anchor_positions, + block_keep_mask, + S, + block_size, + device, + sliding_window=None, +): + """Element-level reference for full and sliding DFlash attention. This uses plain Python loops so correctness is obvious by inspection. """ @@ -27,12 +34,20 @@ def _reference_dflash_mask(anchor_positions, block_keep_mask, S, block_size, dev continue for kv_idx in range(KV_LEN): is_context = kv_idx < S - ctx_visible = is_context and (kv_idx < anchor_pos) + ctx_visible = is_context and kv_idx < anchor_pos is_draft = kv_idx >= S kv_block_id = (kv_idx - S) // block_size draft_visible = is_draft and (q_block_id == kv_block_id) + if sliding_window is not None: + q_offset = q_idx % block_size + kv_offset = (kv_idx - S) % block_size + ctx_visible = ctx_visible and ( + kv_idx >= anchor_pos + q_offset - (sliding_window - 1) + ) + draft_visible = draft_visible and kv_offset <= q_offset + if ctx_visible or draft_visible: mask[b, 0, q_idx, kv_idx] = True return mask @@ -44,7 +59,14 @@ def setUp(self): torch.manual_seed(42) self.device = torch.device("cuda") - def _compare_masks(self, anchor_positions, block_keep_mask, S, block_size): + def _compare_masks( + self, + anchor_positions, + block_keep_mask, + S, + block_size, + sliding_window=None, + ): """Compare create_dflash_sdpa_mask against element-level reference (ground truth).""" anchor_positions = anchor_positions.to(self.device) block_keep_mask = block_keep_mask.to(self.device) @@ -55,6 +77,7 @@ def _compare_masks(self, anchor_positions, block_keep_mask, S, block_size): S=S, block_size=block_size, device=self.device, + sliding_window=sliding_window, ) ref_mask = _reference_dflash_mask( @@ -63,6 +86,7 @@ def _compare_masks(self, anchor_positions, block_keep_mask, S, block_size): S=S, block_size=block_size, device=self.device, + sliding_window=sliding_window, ) self.assertEqual( @@ -73,12 +97,17 @@ def _compare_masks(self, anchor_positions, block_keep_mask, S, block_size): self.assertTrue( torch.equal(sdpa_mask, ref_mask), f"Mask mismatch with S={S}, block_size={block_size}, " - f"anchors={anchor_positions.tolist()}, keep={block_keep_mask.tolist()}\n" + f"sliding_window={sliding_window}, anchors={anchor_positions.tolist()}, " + f"keep={block_keep_mask.tolist()}\n" f"Diff positions: {(sdpa_mask != ref_mask).nonzero(as_tuple=False).tolist()}", ) def _compare_block_mask_consistency( - self, anchor_positions, block_keep_mask, S, block_size + self, + anchor_positions, + block_keep_mask, + S, + block_size, ): """Verify create_dflash_block_mask block-level mask is consistent with reference.""" anchor_positions = anchor_positions.to(self.device) @@ -118,12 +147,11 @@ def _compare_block_mask_consistency( k_end = min(k_start + BM_BLOCK, KV_LEN) has_nonzero = ref_int[b, q_start:q_end, k_start:k_end].any().item() block_val = dense_blocks[b, 0, qi, ki].item() - if has_nonzero: - self.assertEqual( - block_val, - 1, - f"Block ({qi},{ki}) for batch {b} should be 1 but got 0", - ) + self.assertEqual( + block_val, + int(has_nonzero), + f"Block ({qi},{ki}) for batch {b} has incorrect occupancy", + ) def test_basic_single_batch_single_block(self): """Single batch, single draft block.""" @@ -179,6 +207,41 @@ def test_block_size_1(self): block_keep_mask = torch.tensor([[True, True, True]]) self._compare_masks(anchor_positions, block_keep_mask, S=64, block_size=1) + def test_sliding_window_moves_with_query_offset(self): + """The context window advances while the draft block stays causal.""" + anchor_positions = torch.tensor([[6]]) + block_keep_mask = torch.tensor([[True]]) + mask = create_dflash_sdpa_mask( + anchor_positions=anchor_positions.to(self.device), + block_keep_mask=block_keep_mask.to(self.device), + S=12, + block_size=4, + device=self.device, + sliding_window=4, + ) + expected_visible_keys = ( + [3, 4, 5, 12], + [4, 5, 12, 13], + [5, 12, 13, 14], + [12, 13, 14, 15], + ) + for query_offset, expected in enumerate(expected_visible_keys): + with self.subTest(query_offset=query_offset): + actual = mask[0, 0, query_offset].nonzero().flatten().tolist() + self.assertEqual(actual, expected) + + def test_sliding_window_one_has_no_context_and_causal_draft(self): + """A one-token window removes context but keeps causal own-block keys.""" + anchor_positions = torch.tensor([[4, 9]]) + block_keep_mask = torch.tensor([[True, True]]) + self._compare_masks( + anchor_positions, + block_keep_mask, + S=12, + block_size=3, + sliding_window=1, + ) + def test_mixed_validity_multi_batch(self): """Multi-batch with mixed block validity patterns.""" anchor_positions = torch.tensor([[10, 40, 70, 100], [20, 50, 80, 110]]) @@ -263,6 +326,25 @@ def test_block_mask_consistency_mixed(self): anchor_positions, block_keep_mask, S=128, block_size=8 ) + def test_sliding_block_mask_matches_element_reference(self): + """Flex and dense masks implement the same sliding-layer rule.""" + anchors = torch.tensor([[6, 12]], device=self.device) + keep = torch.tensor([[True, False]], device=self.device) + mask_args = { + "anchor_positions": anchors, + "block_keep_mask": keep, + "S": 16, + "block_size": 4, + "device": self.device, + "sliding_window": 5, + } + dense_mask = create_dflash_sdpa_mask(**mask_args) + block_mask = create_dflash_block_mask(**mask_args) + q_idx = torch.arange(8, device=self.device).unsqueeze(1) + kv_idx = torch.arange(24, device=self.device).unsqueeze(0) + flex_mask = block_mask.mask_mod(0, 0, q_idx, kv_idx) + self.assertTrue(torch.equal(flex_mask, dense_mask[0, 0])) + if __name__ == "__main__": unittest.main(verbosity=2) From 7757409f014997d0180b6def3951db9e9516ca77 Mon Sep 17 00:00:00 2001 From: David Wang Date: Thu, 30 Jul 2026 23:23:03 +0000 Subject: [PATCH 02/88] make anchor sampling batch invariant and normalize loss globally across optimizer steps --- scripts/prepare_hidden_states.py | 15 +- .../algorithms/common/dflash_family_data.py | 5 + .../algorithms/common/dflash_family_model.py | 158 +++++------------- specforge/algorithms/common/providers.py | 3 + specforge/algorithms/dflash/providers.py | 2 + specforge/algorithms/domino/providers.py | 2 + specforge/algorithms/dspark/providers.py | 2 + specforge/algorithms/model_providers.py | 29 ++-- specforge/application/composition.py | 3 + specforge/data/loss_mask.py | 21 +++ specforge/data/preprocessing.py | 27 ++- specforge/data/prompt_builder.py | 17 +- specforge/eval/evaluator.py | 31 ++-- specforge/training/assembly.py | 1 + specforge/training/backend.py | 14 ++ specforge/training/controller.py | 78 ++++++++- specforge/training/disaggregated.py | 4 + specforge/training/strategies/base.py | 15 +- .../test_algorithms/test_builtin_providers.py | 13 ++ .../test_offline_capture_layout.py | 17 ++ tests/test_data/test_prompt_builder.py | 30 ++++ .../test_evaluator_aggregation.py | 26 +++ tests/test_runtime/test_seam_fixes.py | 10 ++ tests/test_runtime/test_trainer.py | 91 ++++++++++ .../test_prepare_hidden_states.py | 5 + tests/test_utils/test_dflash_losses.py | 120 ++++++++++++- 26 files changed, 584 insertions(+), 155 deletions(-) create mode 100644 specforge/data/loss_mask.py diff --git a/scripts/prepare_hidden_states.py b/scripts/prepare_hidden_states.py index 47dbb06f2..838a46211 100644 --- a/scripts/prepare_hidden_states.py +++ b/scripts/prepare_hidden_states.py @@ -50,7 +50,7 @@ from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Mapping, Optional +from typing import Callable, Dict, List, Mapping, Optional import torch import torch.distributed as dist @@ -92,6 +92,7 @@ class OfflineCapturePlan: capture_method: str capture_layers: tuple[int, ...] layout: OfflineCaptureLayout + loss_mask_filter: Optional[Callable[[object], bool]] def parse_args(): @@ -341,6 +342,7 @@ def resolve_offline_capture_plan( capture_method=resolved.capture_method, capture_layers=resolved.capture_layers, layout=resolved.layout, + loss_mask_filter=resolved.loss_mask_filter, ) @@ -826,7 +828,7 @@ def main(): ), num_proc=min(args.build_dataset_num_proc, 32), ) - if args.num_samples is not None: + if args.num_samples is not None and capture_plan.loss_mask_filter is None: dataset = dataset.select(range(args.num_samples)) # Tokenizer and cache key tokenizer = load_tokenizer( @@ -847,6 +849,15 @@ def main(): cache_key=cache_key, is_preformatted=args.is_preformatted, num_proc=args.build_dataset_num_proc, + loss_mask_filter=capture_plan.loss_mask_filter, + ) + if capture_plan.loss_mask_filter is not None and args.num_samples is not None: + eagle3_dataset = eagle3_dataset.select( + range(min(args.num_samples, len(eagle3_dataset))) + ) + if not len(eagle3_dataset): + raise ValueError( + f"no samples satisfy {capture_plan.strategy} training eligibility" ) print_with_rank(f"Dataset prepared with {len(eagle3_dataset)} samples.") diff --git a/specforge/algorithms/common/dflash_family_data.py b/specforge/algorithms/common/dflash_family_data.py index ea6251f25..99c0df9c8 100644 --- a/specforge/algorithms/common/dflash_family_data.py +++ b/specforge/algorithms/common/dflash_family_data.py @@ -5,6 +5,7 @@ from functools import partial from specforge.algorithms.common.collation import pad_and_concatenate_features +from specforge.data.loss_mask import has_consecutive_supervised_tokens NORMALIZER_ID = "dflash_family_offline_v1" DSPARK_NORMALIZER_ID = "dspark_offline_v1" @@ -56,6 +57,10 @@ def normalize_offline_sample(raw, max_len: int): f"loss_mask={loss_mask.shape[1]}, " f"hidden_states={hidden_states.shape[1]}" ) + if not has_consecutive_supervised_tokens(loss_mask[0]): + raise ValueError( + "offline DFlash-family samples require two consecutive supervised tokens" + ) return { "input_ids": input_ids, "loss_mask": loss_mask, diff --git a/specforge/algorithms/common/dflash_family_model.py b/specforge/algorithms/common/dflash_family_model.py index e61578199..69cdd0f3a 100644 --- a/specforge/algorithms/common/dflash_family_model.py +++ b/specforge/algorithms/common/dflash_family_model.py @@ -179,40 +179,35 @@ def __init__( def _sample_anchor_positions( self, seq_len: int, loss_mask: torch.Tensor, device: torch.device ) -> Tuple[torch.Tensor, torch.Tensor]: - """Randomly sample anchor positions per sample; returns (anchors, keep_mask).""" - bs = self.block_size - bsz = loss_mask.shape[0] - max_anchor = max(seq_len - bs, 0) - - valid = loss_mask[:, : max_anchor + 1] > 0.5 - valid_counts = valid.sum(dim=1) - max_n = min(self.num_anchors, int(valid_counts.max().item()) - 1) + """Sample anchors whose clean token and first target are supervised.""" - if max_n <= 0: - raise ValueError("should preprocess the data.") - - indices = ( - torch.arange(max_anchor + 1, device=device).unsqueeze(0).expand(bsz, -1) - ) - masked_indices = torch.where( - valid, indices, torch.tensor(seq_len + 1, device=device) + num_candidates = max(seq_len - 1, 0) + valid = (loss_mask[:, :num_candidates] > 0.5) & ( + loss_mask[:, 1 : num_candidates + 1] > 0.5 ) + valid_counts = valid.sum(dim=1) + width = min(self.num_anchors, int(valid_counts.max().item())) + if width == 0: + raise ValueError( + "DFlash-family training requires two consecutive supervised tokens" + ) - random_vals = torch.rand(bsz, max_anchor + 1, device=device) - random_vals = torch.where(valid, random_vals, torch.tensor(2.0, device=device)) - - _, sorted_idx = random_vals.sort(dim=1) - gathered = torch.gather(masked_indices, 1, sorted_idx) - anchors = gathered[:, :max_n].sort(dim=1).values - - keep_mask = torch.arange(max_n, device=device).unsqueeze( + random_values = torch.rand(valid.shape, device=device) + random_values.masked_fill_(~valid, 2.0) + candidates = random_values.argsort(dim=1)[:, :width] + keep_mask = torch.arange(width, device=device).unsqueeze( 0 - ) < valid_counts.unsqueeze(1).clamp(max=max_n) + ) < valid_counts.clamp(max=width).unsqueeze(1) + + sentinel = valid.shape[1] anchors = torch.where( - keep_mask, anchors, torch.tensor(0, dtype=torch.long, device=device) + keep_mask, + candidates, + torch.full_like(candidates, sentinel), ) - - return anchors, keep_mask + anchors = anchors.sort(dim=1).values + keep_mask = anchors < sentinel + return torch.where(keep_mask, anchors, 0), keep_mask def _create_position_ids(self, anchor_positions: torch.Tensor) -> torch.Tensor: """Create absolute position IDs for parallel draft blocks.""" @@ -392,7 +387,7 @@ def forward( input_ids: torch.Tensor, hidden_states: torch.Tensor, loss_mask: torch.Tensor, - ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, torch.Tensor]]: + ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, object]]: """Parallel block-wise training forward pass; returns (loss, accuracy, metrics) — same shape as Domino's forward.""" if self.attention_backend == "flex_attention" and not FLEX_ATTENTION_AVAILABLE: @@ -450,13 +445,20 @@ def forward( chunk_size=self.objective_chunk_blocks, dim=1, ) - if self.loss_type == "dflash": - loss = loss_num / (loss_den + 1e-6) - else: - loss = loss_num / float(bsz) - accuracy = correct_num / (accuracy_denom + 1e-6) - - return loss, accuracy, {"accuracy_denom": accuracy_denom.detach()} + ratio_metrics = { + "acc": (correct_num.detach(), accuracy_denom.detach()), + } + metrics: Dict[str, object] = { + "accuracy_denom": accuracy_denom.detach(), + "ratio_metrics": ratio_metrics, + } + loss_denominator = ( + loss_den if self.loss_type == "dflash" else loss_num.new_tensor(float(bsz)) + ) + loss = loss_num / loss_denominator + metrics["loss_terms"] = (loss_num, loss_denominator.detach()) + accuracy = correct_num / accuracy_denom + return loss, accuracy, metrics class OnlineDominoModel(OnlineDFlashModel): @@ -489,41 +491,6 @@ def __init__( ) self.shift_label = shift_label - def _sample_anchor_positions( - self, seq_len: int, loss_mask: torch.Tensor, device: torch.device - ) -> Tuple[torch.Tensor, torch.Tensor]: - """Randomly sample anchor positions per sample; returns (anchors, keep_mask).""" - bs = self.block_size - bsz = loss_mask.shape[0] - max_anchor = max(seq_len - bs, 0) - - valid = loss_mask[:, : max_anchor + 1] > 0.5 - valid_counts = valid.sum(dim=1) - max_n = max(1, min(self.num_anchors, int(valid_counts.max().item()) - 1)) - - indices = ( - torch.arange(max_anchor + 1, device=device).unsqueeze(0).expand(bsz, -1) - ) - masked_indices = torch.where( - valid, indices, torch.tensor(seq_len + 1, device=device) - ) - - random_vals = torch.rand(bsz, max_anchor + 1, device=device) - random_vals = torch.where(valid, random_vals, torch.tensor(2.0, device=device)) - - _, sorted_idx = random_vals.sort(dim=1) - gathered = torch.gather(masked_indices, 1, sorted_idx) - anchors = gathered[:, :max_n].sort(dim=1).values - - keep_mask = torch.arange(max_n, device=device).unsqueeze( - 0 - ) < valid_counts.unsqueeze(1).clamp(max=max_n) - anchors = torch.where( - keep_mask, anchors, torch.tensor(0, dtype=torch.long, device=device) - ) - - return anchors, keep_mask - def _build_domino_head_inputs( self, input_ids: torch.Tensor, @@ -785,55 +752,6 @@ def __init__( self.dspark_l1_loss_alpha = float(dspark_l1_loss_alpha) self.dspark_confidence_head_alpha = float(dspark_confidence_head_alpha) - def _build_anchor_candidate_mask( - self, - seq_len: int, - loss_mask: torch.Tensor, - ) -> torch.Tensor: - num_candidates = max(seq_len - 1, 0) - if num_candidates == 0: - return loss_mask[:, :0].bool() - anchor_valid = loss_mask[:, :num_candidates] > 0.5 - first_target_valid = loss_mask[:, 1 : num_candidates + 1] > 0.5 - return anchor_valid & first_target_valid - - def _sample_anchor_positions( - self, seq_len: int, loss_mask: torch.Tensor, device: torch.device - ) -> Tuple[torch.Tensor, torch.Tensor]: - """Sample only anchors with a valid first target, without dummy width.""" - - valid = self._build_anchor_candidate_mask(seq_len, loss_mask) - if valid.shape[1] == 0: - raise ValueError("DSpark needs sequences with at least two tokens") - valid_counts = valid.sum(dim=1) - width = min(self.num_anchors, int(valid_counts.max().item())) - if width <= 0: - raise ValueError( - "DSpark found no valid anchor with two consecutive loss tokens" - ) - indices = torch.arange(valid.shape[1], device=device).expand( - loss_mask.shape[0], -1 - ) - random_values = torch.rand(valid.shape, device=device) - random_values.masked_fill_(~valid, 2.0) - order = random_values.argsort(dim=1) - candidates = torch.gather(indices, 1, order)[:, :width] - keep_mask = torch.arange(width, device=device).unsqueeze( - 0 - ) < valid_counts.clamp(max=width).unsqueeze(1) - anchors = ( - torch.where( - keep_mask, - candidates, - torch.full_like(candidates, valid.shape[1]), - ) - .sort(dim=1) - .values - ) - keep_mask = anchors < valid.shape[1] - anchors = torch.where(keep_mask, anchors, torch.zeros_like(anchors)) - return anchors, keep_mask - def _build_dspark_labels_and_mask( self, input_ids: torch.Tensor, diff --git a/specforge/algorithms/common/providers.py b/specforge/algorithms/common/providers.py index 370feeca8..51c8ec795 100644 --- a/specforge/algorithms/common/providers.py +++ b/specforge/algorithms/common/providers.py @@ -361,6 +361,7 @@ class ModelProvider: needs_input_tools: Factory default_dataloader_num_workers: int allow_missing_warm_start_embedding: bool = False + loss_mask_filter: Factory | None = None def __post_init__(self) -> None: if not isinstance(self.draft_config, DraftConfigProvider): @@ -384,6 +385,8 @@ def __post_init__(self) -> None: ) if not isinstance(self.allow_missing_warm_start_embedding, bool): raise TypeError("allow_missing_warm_start_embedding must be a bool") + if self.loss_mask_filter is not None and not callable(self.loss_mask_filter): + raise TypeError("loss_mask_filter must be callable or None") @dataclass(frozen=True) diff --git a/specforge/algorithms/dflash/providers.py b/specforge/algorithms/dflash/providers.py index 564daeaea..2cc119ac9 100644 --- a/specforge/algorithms/dflash/providers.py +++ b/specforge/algorithms/dflash/providers.py @@ -34,6 +34,7 @@ FeatureMode, OfflineStorageContract, ) +from specforge.data.loss_mask import has_consecutive_supervised_tokens ALGORITHM_NAME = "dflash" DRAFT_ARCHITECTURE = "DFlashDraftModel" @@ -190,6 +191,7 @@ def algorithm_providers() -> AlgorithmProviders: minimum_loss_tokens=minimum_loss_tokens, needs_input_tools=needs_input_tools, default_dataloader_num_workers=8, + loss_mask_filter=has_consecutive_supervised_tokens, ), offline=( OfflineDataProvider( diff --git a/specforge/algorithms/domino/providers.py b/specforge/algorithms/domino/providers.py index d1b3a97a8..af6506c41 100644 --- a/specforge/algorithms/domino/providers.py +++ b/specforge/algorithms/domino/providers.py @@ -30,6 +30,7 @@ FeatureMode, OfflineStorageContract, ) +from specforge.data.loss_mask import has_consecutive_supervised_tokens ALGORITHM_NAME = "domino" DRAFT_ARCHITECTURE = "DominoDraftModel" @@ -169,6 +170,7 @@ def algorithm_providers() -> AlgorithmProviders: minimum_loss_tokens=minimum_loss_tokens, needs_input_tools=needs_input_tools, default_dataloader_num_workers=8, + loss_mask_filter=has_consecutive_supervised_tokens, ), offline=( OfflineDataProvider( diff --git a/specforge/algorithms/dspark/providers.py b/specforge/algorithms/dspark/providers.py index 153575d5d..68c37b469 100644 --- a/specforge/algorithms/dspark/providers.py +++ b/specforge/algorithms/dspark/providers.py @@ -33,6 +33,7 @@ FeatureMode, OfflineStorageContract, ) +from specforge.data.loss_mask import has_consecutive_supervised_tokens ALGORITHM_NAME = "dspark" DRAFT_ARCHITECTURE = "DSparkDraftModel" @@ -163,6 +164,7 @@ def algorithm_providers() -> AlgorithmProviders: minimum_loss_tokens=minimum_loss_tokens, needs_input_tools=needs_input_tools, default_dataloader_num_workers=8, + loss_mask_filter=has_consecutive_supervised_tokens, ), offline=( OfflineDataProvider( diff --git a/specforge/algorithms/model_providers.py b/specforge/algorithms/model_providers.py index b48e81cb1..46aba834c 100644 --- a/specforge/algorithms/model_providers.py +++ b/specforge/algorithms/model_providers.py @@ -211,6 +211,22 @@ def resolve_eagle_capture_layers( return layers +def _validate_dflash_block_size(draft_config: Any) -> None: + block_size = ( + draft_config.get("block_size") + if isinstance(draft_config, dict) + else getattr(draft_config, "block_size", None) + ) + if ( + not isinstance(block_size, int) + or isinstance(block_size, bool) + or block_size < 2 + ): + raise ValueError( + "DFlash-family draft config must define an integer block_size >= 2" + ) + + def resolve_dflash_capture_layers( _cfg: Config, draft_config: Any, _target_config: Any ) -> List[int]: @@ -318,6 +334,7 @@ def _build_dflash_family_model( ) -> AlgorithmModelParts: from specforge.modeling.target.target_utils import TargetEmbeddingsAndHead + _validate_dflash_block_size(draft_model) mask_token_id = _resolve_mask_token_id(cfg, draft_model, tokenizer) draft_model.mask_token_id = mask_token_id method_config = getattr(draft_model.config, "dflash_config", None) @@ -432,16 +449,8 @@ def domino_strategy_kwargs(cfg: Config) -> Dict[str, Any]: def dflash_min_loss_tokens(_cfg: Config, draft_config: Any) -> int: - block_size = getattr(draft_config, "block_size", None) - if ( - not isinstance(block_size, int) - or isinstance(block_size, bool) - or block_size < 1 - ): - raise ValueError( - "DFlash-family draft config must define a positive integer block_size" - ) - return 2 * block_size + _validate_dflash_block_size(draft_config) + return 2 def populate_dflash_generated_config( diff --git a/specforge/application/composition.py b/specforge/application/composition.py index 735084f16..0eb271c17 100644 --- a/specforge/application/composition.py +++ b/specforge/application/composition.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Callable from specforge.algorithms.common.providers import OfflineCaptureLayout from specforge.algorithms.registry import AlgorithmRegistration, AlgorithmRegistry @@ -26,6 +27,7 @@ class ResolvedOfflineCapture: capture_method: str capture_layers: tuple[int, ...] layout: OfflineCaptureLayout + loss_mask_filter: Callable[[object], bool] | None def bind_run(cfg: Config, algorithm: AlgorithmRegistration) -> ResolvedRun: @@ -127,6 +129,7 @@ def resolve_offline_capture( capture_method=offline.capture_layout.capture_method, capture_layers=layers, layout=offline.capture_layout, + loss_mask_filter=model_provider.loss_mask_filter, ) diff --git a/specforge/data/loss_mask.py b/specforge/data/loss_mask.py new file mode 100644 index 000000000..d52b30240 --- /dev/null +++ b/specforge/data/loss_mask.py @@ -0,0 +1,21 @@ +"""Loss-mask predicates shared by online and offline data paths.""" + +from collections.abc import Sequence +from typing import Any + + +def has_consecutive_supervised_tokens(loss_mask: Any) -> bool: + """Return whether one sample contains adjacent supervised tokens.""" + + values = loss_mask.tolist() if hasattr(loss_mask, "tolist") else list(loss_mask) + if values and isinstance(values[0], Sequence): + if len(values) != 1: + raise ValueError("expected one loss-mask row") + values = list(values[0]) + return any( + bool(current) and bool(following) + for current, following in zip(values, values[1:]) + ) + + +__all__ = ["has_consecutive_supervised_tokens"] diff --git a/specforge/data/preprocessing.py b/specforge/data/preprocessing.py index 8d8f3c090..f32f7b099 100644 --- a/specforge/data/preprocessing.py +++ b/specforge/data/preprocessing.py @@ -27,7 +27,7 @@ import re import warnings from collections import Counter -from typing import Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import torch import torch.nn.functional as F @@ -36,6 +36,7 @@ from transformers import PreTrainedTokenizer from ..distributed import get_draft_sp_group, get_sp_ring_group +from .loss_mask import has_consecutive_supervised_tokens from .parse import GeneralParser, GLMParser, HarmonyParser, ThinkingParser from .template import TEMPLATE_REGISTRY, ChatTemplate @@ -180,6 +181,7 @@ def build_eagle3_dataset( is_preformatted: Optional[bool] = False, train_only_last_turn: Optional[bool] = False, minimum_valid_tokens: Optional[int] = None, + loss_mask_filter: Optional[Callable[[object], bool]] = None, ) -> HFDataset: """ build eagle3 dataset @@ -206,12 +208,16 @@ def build_eagle3_dataset( train_only_last_turn: If True, only the last assistant turn contributes to the loss. Useful for thinking models where history may not contain thoughts. minimum_valid_tokens: If set, drops samples with fewer trainable tokens. + loss_mask_filter: Optional algorithm-owned predicate applied after + tokenization and truncation. Returns: The processed HF dataset. """ if minimum_valid_tokens is not None and minimum_valid_tokens < 0: raise ValueError("minimum_valid_tokens must be >= 0") + if loss_mask_filter is not None and not callable(loss_mask_filter): + raise TypeError("loss_mask_filter must be callable or None") # Validate chat_template requirement if chat_template is None: @@ -344,6 +350,21 @@ def has_minimum_valid_tokens(example): f"Filtered dataset by trainable tokens: {before_filter} -> {len(dataset)}" ) + if loss_mask_filter is not None: + before_filter = len(dataset) + + def has_eligible_loss_mask(example): + return loss_mask_filter(example["loss_mask"]) + + dataset = dataset.filter( + has_eligible_loss_mask, + num_proc=num_proc, + desc="Filtering samples by algorithm loss-mask eligibility", + ) + print( + f"Filtered dataset by loss-mask eligibility: {before_filter} -> {len(dataset)}" + ) + dataset.set_format(type="torch") return dataset @@ -685,6 +706,10 @@ def process_offline_dflash_sample( f"loss_mask={loss_mask.shape[1]}, " f"hidden_states={hidden_states.shape[1]}" ) + if not has_consecutive_supervised_tokens(loss_mask[0]): + raise ValueError( + "offline DFlash samples require two consecutive supervised tokens" + ) return { "input_ids": input_ids, "loss_mask": loss_mask, diff --git a/specforge/data/prompt_builder.py b/specforge/data/prompt_builder.py index 74df210cb..97c8c6411 100644 --- a/specforge/data/prompt_builder.py +++ b/specforge/data/prompt_builder.py @@ -12,7 +12,7 @@ import os from collections.abc import Iterable, Iterator, Mapping, Sequence from numbers import Integral -from typing import Any +from typing import Any, Callable PromptTaskDict = dict[str, Any] @@ -30,6 +30,7 @@ def prepare_prompt_tasks( num_proc: int | None, min_loss_tokens: int = 1, max_prompts: int | None = None, + loss_mask_filter: Callable[[Sequence[int]], bool] | None = None, ) -> list[PromptTaskDict]: """Prepare runtime prompt dictionaries from a JSONL file. @@ -48,6 +49,7 @@ def prepare_prompt_tasks( max_prompts=max_prompts, cache_dir=cache_dir, cache_key=cache_key, + loss_mask_filter=loss_mask_filter, ) path_string = os.fspath(path) first_record = next(_iter_records(path_string), None) @@ -75,6 +77,7 @@ def prepare_prompt_tasks( max_length=max_length, min_loss_tokens=min_loss_tokens, limit=limit, + loss_mask_filter=loss_mask_filter, ) return _prepare_raw_prompts( @@ -89,6 +92,7 @@ def prepare_prompt_tasks( num_proc=num_proc, min_loss_tokens=min_loss_tokens, limit=limit, + loss_mask_filter=loss_mask_filter, ) @@ -105,6 +109,7 @@ def _prepare_raw_prompts( num_proc: int | None, min_loss_tokens: int, limit: int | None, + loss_mask_filter: Callable[[Sequence[int]], bool] | None, ) -> list[PromptTaskDict]: try: from datasets import load_dataset @@ -116,7 +121,7 @@ def _prepare_raw_prompts( from .preprocessing import build_eagle3_dataset dataset = load_dataset("json", data_files=path, split="train") - if limit is not None and limit < len(dataset): + if loss_mask_filter is None and limit is not None and limit < len(dataset): dataset = dataset.select(range(limit)) processed_dataset = build_eagle3_dataset( @@ -130,6 +135,7 @@ def _prepare_raw_prompts( is_preformatted=is_preformatted, train_only_last_turn=train_only_last_turn, minimum_valid_tokens=min_loss_tokens, + loss_mask_filter=loss_mask_filter, ) rows = ( (record, f"processed dataset row {index}") @@ -140,6 +146,7 @@ def _prepare_raw_prompts( max_length=max_length, min_loss_tokens=min_loss_tokens, limit=limit, + loss_mask_filter=None, ) @@ -149,6 +156,7 @@ def _materialize_prompt_tasks( max_length: int, min_loss_tokens: int, limit: int | None, + loss_mask_filter: Callable[[Sequence[int]], bool] | None, ) -> list[PromptTaskDict]: prompts: list[PromptTaskDict] = [] for record, source in rows: @@ -173,6 +181,8 @@ def _materialize_prompt_tasks( loss_mask = loss_mask[:max_length] if sum(loss_mask) < min_loss_tokens: continue + if loss_mask_filter is not None and not loss_mask_filter(loss_mask): + continue prompts.append( { @@ -275,6 +285,7 @@ def _validate_options( max_prompts: int | None, cache_dir: str | None, cache_key: str | None, + loss_mask_filter: Callable[[Sequence[int]], bool] | None, ) -> None: if ( not isinstance(max_length, int) @@ -300,6 +311,8 @@ def _validate_options( ) if (cache_dir is None) != (cache_key is None): raise ValueError("cache_dir and cache_key must be provided together") + if loss_mask_filter is not None and not callable(loss_mask_filter): + raise TypeError("loss_mask_filter must be callable or None") __all__ = ["PromptTaskDict", "prepare_prompt_tasks"] diff --git a/specforge/eval/evaluator.py b/specforge/eval/evaluator.py index 9b10a7400..1f560f300 100644 --- a/specforge/eval/evaluator.py +++ b/specforge/eval/evaluator.py @@ -39,10 +39,10 @@ def run( ) -> Dict[str, Any]: """Run the pass; returns ``{}`` if zero batches were processed globally. - Scalar accuracy is weighted by ``metrics['accuracy_denom']`` when present, - else by the loss-token count — only approximately batch-size invariant - when the accuracy counts a different token set than the loss. In a mixed - pass, scalar batches feed avg_loss only; their accuracy is not merged. + Additive loss and accuracy terms are used directly when the strategy + provides them. Scalar fallbacks use ``metrics['accuracy_denom']`` when + present, else the loss-token count. In a mixed pass, scalar batches feed + avg_loss only; their accuracy is not merged. """ # pp rows: [correct, denom, acceptance_rate*w, ploss*w] per TTT # position, float64 so counts stay exact past 2**24. @@ -63,8 +63,13 @@ def run( if sums is None: sums = torch.zeros(7, dtype=torch.float64, device=loss.device) tokens = self._token_count(batch, m, device=sums.device) - sums[0] += loss.to(sums.device) * tokens - sums[1] += tokens + if out.loss_terms is None: + sums[0] += loss.to(sums.device) * tokens + sums[1] += tokens + else: + loss_numerator, loss_denominator = out.loss_terms + sums[0] += self._sum64(loss_numerator, sums.device) + sums[1] += self._sum64(loss_denominator, sums.device) sums[4] += 1.0 if "acc_corrects" in m and "acc_denoms" in m: @@ -86,6 +91,10 @@ def run( if "plosses" in m: pp[3] += self._stack(m["plosses"]) * w sums[6] += tokens + elif "acc" in out.ratio_metrics: + accuracy_numerator, accuracy_denominator = out.ratio_metrics["acc"] + sums[2] += self._sum64(accuracy_numerator, sums.device) + sums[3] += self._sum64(accuracy_denominator, sums.device) elif "accuracy" in m: acc = m["accuracy"] acc = ( @@ -96,11 +105,7 @@ def run( ) ) denom = m.get("accuracy_denom") - w = ( - torch.as_tensor(denom).detach().double().sum().to(sums.device) - if denom is not None - else tokens - ) + w = self._sum64(denom, sums.device) if denom is not None else tokens sums[2] += acc * w sums[3] += w @@ -162,6 +167,10 @@ def run( def _stack(values: Iterable[Any]) -> torch.Tensor: return torch.stack([torch.as_tensor(v).detach().double() for v in values]) + @staticmethod + def _sum64(value: Any, device: torch.device) -> torch.Tensor: + return torch.as_tensor(value).detach().double().sum().to(device) + @staticmethod def _comm_device() -> torch.device: """Return the bound device required by the active collective backend.""" diff --git a/specforge/training/assembly.py b/specforge/training/assembly.py index 5fa2e3091..d3b6774b5 100644 --- a/specforge/training/assembly.py +++ b/specforge/training/assembly.py @@ -391,6 +391,7 @@ def _prepare_prompts( num_proc=cfg.data.build_dataset_num_proc, min_loss_tokens=min_loss_tokens, max_prompts=cfg.data.max_prompts, + loss_mask_filter=algorithm.providers.model.loss_mask_filter, ) diff --git a/specforge/training/backend.py b/specforge/training/backend.py index 853c08730..ce9475b5c 100644 --- a/specforge/training/backend.py +++ b/specforge/training/backend.py @@ -132,6 +132,12 @@ def prepare_model(self, model: nn.Module) -> nn.Module: ... @abc.abstractmethod def backward(self, loss: torch.Tensor, *, is_boundary: bool = True) -> None: ... + def scale_gradients(self, factor: torch.Tensor) -> None: + """Scale synchronized gradients before clipping and stepping.""" + raise NotImplementedError( + f"{type(self).__name__} does not implement gradient scaling" + ) + @abc.abstractmethod def step(self) -> Optional[torch.Tensor]: ... @@ -313,6 +319,14 @@ def backward(self, loss: torch.Tensor, *, is_boundary: bool = True) -> None: with self.module.no_sync(): loss.backward() + def scale_gradients(self, factor: torch.Tensor) -> None: + if self.module is None: + raise RuntimeError("scale_gradients called before prepare_model") + with torch.no_grad(): + for parameter in self.module.parameters(): + if parameter.grad is not None: + parameter.grad.mul_(factor) + def step(self) -> Optional[torch.Tensor]: """Run the optimizer step, which clips and returns the global grad norm.""" if self.optimizer is None: diff --git a/specforge/training/controller.py b/specforge/training/controller.py index de6fcb29e..95e9c4c63 100644 --- a/specforge/training/controller.py +++ b/specforge/training/controller.py @@ -311,6 +311,7 @@ def __init__( self.backend = backend self.accumulation_steps = max(1, accumulation_steps) self._micro = 0 + self._ratio_totals: Dict[str, tuple[torch.Tensor, torch.Tensor]] = {} @property def accumulation_remainder(self) -> int: @@ -321,16 +322,79 @@ def train_step( self, batch: TrainBatch, ctx: Optional[StepContext] = None ) -> StepResult: out: StepOutput = self.strategy.forward_loss(batch, ctx) - loss = out.loss / self.accumulation_steps + loss = out.loss + ratio_metrics = dict(out.ratio_metrics) + if out.loss_terms is not None: + numerator, denominator = out.loss_terms + if numerator.numel() != 1 or denominator.numel() != 1: + raise ValueError("loss_terms must contain scalar tensors") + loss = numerator.reshape(()) + denominator = denominator.detach().reshape(()) + ratio_metrics["loss"] = ( + numerator.detach().reshape(()), + denominator, + ) + self._accumulate_ratio_metrics(ratio_metrics) + loss = loss / self.accumulation_steps self._micro += 1 # The boundary is known before backward so the backend can defer the FSDP # gradient reduction (no_sync) on non-boundary micro-steps. stepped = self._micro % self.accumulation_steps == 0 self.backend.backward(loss, is_boundary=stepped) + if stepped and out.loss_terms is not None: + self._normalize_gradients(self._ratio_totals["loss"][1]) grad_norm = self.backend.step() if stepped else None - return self._result(out, grad_norm, stepped) + result_ratio_metrics = self._ratio_totals if stepped else ratio_metrics + result = self._result( + out, + grad_norm, + stepped, + ratio_metrics=result_ratio_metrics, + ) + if stepped: + self._ratio_totals = {} + return result + + def _accumulate_ratio_metrics(self, values: Dict[str, Any]) -> None: + for name, (raw_numerator, raw_denominator) in values.items(): + numerator = torch.as_tensor(raw_numerator).detach() + denominator = torch.as_tensor(raw_denominator).detach() + previous = self._ratio_totals.get(name) + if previous is not None: + numerator = previous[0] + numerator + denominator = previous[1] + denominator + self._ratio_totals[name] = (numerator, denominator) + + def _normalize_gradients(self, local_denominator: torch.Tensor) -> None: + import torch.distributed as dist - def _result(self, out: StepOutput, grad_norm, stepped: bool) -> StepResult: + denominator = local_denominator.clone() + parallel_config = getattr(self.backend, "parallel_config", None) + process_group = getattr(parallel_config, "fsdp_process_group", None) + world_size = 1 + if dist.is_available() and dist.is_initialized(): + world_size = dist.get_world_size(group=process_group) + if world_size > 1: + dist.all_reduce( + denominator, + op=dist.ReduceOp.SUM, + group=process_group, + ) + if denominator.item() <= 0: + raise ValueError("global loss denominator must be positive") + scale = ( + denominator.new_tensor(world_size * self.accumulation_steps) / denominator + ) + self.backend.scale_gradients(scale) + + def _result( + self, + out: StepOutput, + grad_norm, + stepped: bool, + *, + ratio_metrics: Optional[Dict[str, Any]] = None, + ) -> StepResult: # EAGLE3 carries per-TTT numerators and denominators. Preserve those # positions and reduce counts before ratios; scalarizing its lists here # would both collapse the TTT structure and log one rank's local data. @@ -353,7 +417,7 @@ def _result(self, out: StepOutput, grad_norm, stepped: bool) -> StepResult: metrics: Dict[str, Any] = dict(structured or {}) metrics.update( _reduce_ratio_metrics( - out.ratio_metrics, + out.ratio_metrics if ratio_metrics is None else ratio_metrics, device=metric_device, process_group=process_group, reduce=stepped, @@ -374,7 +438,11 @@ def _result(self, out: StepOutput, grad_norm, stepped: bool) -> StepResult: # the generic trainer their algorithm-specific names. Move CPU schedule # scalars (for example Domino's lambda_base) onto the loss device before # the DP reduction so NCCL-backed runs do not all-reduce a CPU tensor. - reserved_metric_keys = _EAGLE3_STRUCTURED_METRIC_KEYS | {"accuracy", "loss"} + reserved_metric_keys = _EAGLE3_STRUCTURED_METRIC_KEYS | { + "accuracy", + "accuracy_denom", + "loss", + } for key, value in out.metrics.items(): if key in reserved_metric_keys: continue diff --git a/specforge/training/disaggregated.py b/specforge/training/disaggregated.py index 8eebf9cf2..ef03d932f 100644 --- a/specforge/training/disaggregated.py +++ b/specforge/training/disaggregated.py @@ -561,6 +561,10 @@ def _build_online( input_tools, draft_config=draft_config, ) + if not prompts: + raise ValueError( + f"no prompts satisfy {algorithm.name} training eligibility" + ) if cfg.training.total_steps is None and cfg.training.max_steps is None: schedule = _online_schedule_payload(cfg, num_prompts=len(prompts)) _write_control( diff --git a/specforge/training/strategies/base.py b/specforge/training/strategies/base.py index 60114b743..ee712a2db 100644 --- a/specforge/training/strategies/base.py +++ b/specforge/training/strategies/base.py @@ -29,11 +29,17 @@ @dataclass(frozen=True) class StepOutput: """Per-step result: loss + strategy-specific metrics, kept generic so - per-position (TTT) and single-scalar strategies share one trainer loop.""" + per-position (TTT) and single-scalar strategies share one trainer loop. + + ``loss_terms`` carries an additive objective numerator and denominator when + gradients and reported loss must be normalized across the full optimizer + window. + """ loss: torch.Tensor metrics: Dict[str, Any] ratio_metrics: Dict[str, Tuple[Any, Any]] = field(default_factory=dict) + loss_terms: Optional[Tuple[torch.Tensor, torch.Tensor]] = None @dataclass(frozen=True) @@ -441,7 +447,12 @@ def forward_loss( metrics = {"accuracy": accuracy.detach()} if "accuracy_denom" in model_metrics: metrics["accuracy_denom"] = model_metrics["accuracy_denom"] - return StepOutput(loss=loss, metrics=metrics) + return StepOutput( + loss=loss, + metrics=metrics, + ratio_metrics=model_metrics.get("ratio_metrics", {}), + loss_terms=model_metrics.get("loss_terms"), + ) def checkpoint_state_filter(self, state_dict: Dict[str, Any]) -> Dict[str, Any]: # Everything trainable lives under draft_model.; the target diff --git a/tests/test_algorithms/test_builtin_providers.py b/tests/test_algorithms/test_builtin_providers.py index 66bb669e3..5117d5bbb 100644 --- a/tests/test_algorithms/test_builtin_providers.py +++ b/tests/test_algorithms/test_builtin_providers.py @@ -55,6 +55,19 @@ def test_every_registration_pairs_contract_and_providers(self): } self.assertEqual(contract_keys, provider_keys) + def test_dflash_family_requires_a_trainable_block_size(self): + for algorithm in ("dflash", "domino", "dspark"): + minimum_loss_tokens = self.registry.resolve( + algorithm + ).providers.model.minimum_loss_tokens + with self.subTest(algorithm=algorithm): + self.assertEqual( + minimum_loss_tokens(None, SimpleNamespace(block_size=2)), + 2, + ) + with self.assertRaisesRegex(ValueError, "block_size >= 2"): + minimum_loss_tokens(None, SimpleNamespace(block_size=1)) + def test_algorithm_metadata_has_no_factories_or_topology_flags(self): field_names = {field.name for field in fields(AlgorithmSpec)} self.assertEqual( diff --git a/tests/test_algorithms/test_offline_capture_layout.py b/tests/test_algorithms/test_offline_capture_layout.py index 3acc1087d..f3499d4df 100644 --- a/tests/test_algorithms/test_offline_capture_layout.py +++ b/tests/test_algorithms/test_offline_capture_layout.py @@ -98,6 +98,23 @@ def test_materialize_preserves_arbitrary_auxiliary_layer_counts(self): record["hidden_states"].shape[-1], ) + def test_dflash_family_normalizers_require_adjacent_supervision(self): + raw = { + "input_ids": torch.tensor([1, 2, 3]), + "loss_mask": torch.tensor([1, 0, 1]), + "hidden_states": torch.randn(1, 3, 8), + "target_last_hidden_states": torch.randn(1, 3, 8), + } + for strategy in ("dflash", "domino", "dspark"): + with self.subTest(strategy=strategy): + normalizer = ( + self.registry.resolve(strategy) + .providers.offline_for("text") + .build_normalizer(3) + ) + with self.assertRaisesRegex(ValueError, "two consecutive"): + normalizer(raw) + def test_duplicate_output_names_are_rejected(self): with self.assertRaisesRegex(ValueError, "duplicate.*hidden_states"): OfflineCaptureLayout( diff --git a/tests/test_data/test_prompt_builder.py b/tests/test_data/test_prompt_builder.py index 5182618bc..6e83053c0 100644 --- a/tests/test_data/test_prompt_builder.py +++ b/tests/test_data/test_prompt_builder.py @@ -6,6 +6,7 @@ import unittest from unittest.mock import patch +from specforge.data.loss_mask import has_consecutive_supervised_tokens from specforge.data.prompt_builder import prepare_prompt_tasks @@ -86,6 +87,35 @@ def test_pre_tokenized_path_truncates_filters_and_caps(self): ], ) + def test_algorithm_loss_mask_filter_applies_after_truncation(self): + records = [ + {"input_ids": [1, 2, 3, 4, 5], "loss_mask": [1, 0, 0, 1, 1]}, + {"input_ids": [4, 5, 6, 7], "loss_mask": [0, 0, 1, 1]}, + ] + with tempfile.TemporaryDirectory() as tmp_dir: + path = os.path.join(tmp_dir, "prompts.jsonl") + _write_jsonl(path, records) + + prompts = prepare_prompt_tasks( + path, + tokenizer=None, + chat_template=None, + max_length=4, + is_preformatted=False, + train_only_last_turn=False, + cache_dir=None, + cache_key=None, + num_proc=1, + min_loss_tokens=2, + max_prompts=1, + loss_mask_filter=has_consecutive_supervised_tokens, + ) + + self.assertEqual( + prompts, + [{"payload": {"input_ids": [4, 5, 6, 7], "loss_mask": [0, 0, 1, 1]}}], + ) + def test_raw_conversations_use_lazy_dataset_preprocessing(self): raw_rows = [ {"conversations": [{"role": "user", "content": "one"}]}, diff --git a/tests/test_runtime/test_evaluator_aggregation.py b/tests/test_runtime/test_evaluator_aggregation.py index 8055b3130..b7436b357 100644 --- a/tests/test_runtime/test_evaluator_aggregation.py +++ b/tests/test_runtime/test_evaluator_aggregation.py @@ -54,6 +54,19 @@ def _scalar_out(loss, acc, tokens, denom=None): return StepOutput(loss=torch.tensor(float(loss)), metrics=metrics) +def _additive_scalar_out(loss_num, loss_den, accuracy_num, accuracy_den): + loss_num = torch.tensor(float(loss_num)) + loss_den = torch.tensor(float(loss_den)) + accuracy_num = torch.tensor(float(accuracy_num)) + accuracy_den = torch.tensor(float(accuracy_den)) + return StepOutput( + loss=loss_num / loss_den, + metrics={"accuracy": accuracy_num / accuracy_den}, + ratio_metrics={"acc": (accuracy_num, accuracy_den)}, + loss_terms=(loss_num, loss_den), + ) + + class TestEvaluatorAggregation(unittest.TestCase): def _run(self, outputs): from specforge.eval import Evaluator @@ -171,6 +184,19 @@ def test_scalar_accuracy_weighted_by_accuracy_denom(self): # loss-token weighting would skew to (0.75*10 + 0.5*50)/60 ~ 0.542 self.assertNotAlmostEqual(m["eval/avg_acc"], 32.5 / 60, places=2) + def test_additive_scalar_terms_are_partition_invariant(self): + split = self._run( + [ + _additive_scalar_out(2, 1, 1, 1), + _additive_scalar_out(30, 3, 1, 3), + ] + ) + combined = self._run([_additive_scalar_out(32, 4, 2, 4)]) + + self.assertEqual(split, combined) + self.assertEqual(split["eval/avg_loss"], 8.0) + self.assertEqual(split["eval/avg_acc"], 0.5) + def test_reports_per_position_acceptance(self): m = self._run([_step_output(1.0, corrects=[3, 2], denoms=[4, 4])]) self.assertAlmostEqual(m["eval/per_position_acc"][0], 0.75, places=6) diff --git a/tests/test_runtime/test_seam_fixes.py b/tests/test_runtime/test_seam_fixes.py index bcecc7469..278a4489a 100644 --- a/tests/test_runtime/test_seam_fixes.py +++ b/tests/test_runtime/test_seam_fixes.py @@ -106,6 +106,16 @@ def __init__(self): self.assertEqual(ignored, (model.lm_head, model.embed_tokens)) + def test_backend_scales_gradients_before_optimizer_step(self): + model = nn.Linear(2, 1, bias=False) + backend = FSDPTrainingBackend(ParallelConfig()) + backend.prepare_model(model, wrap=False) + model.weight.grad = torch.tensor([[4.0, 8.0]]) + + backend.scale_gradients(torch.tensor(0.25)) + + torch.testing.assert_close(model.weight.grad, torch.tensor([[1.0, 2.0]])) + class _FakeBackend(TrainingBackend): name = "fake" diff --git a/tests/test_runtime/test_trainer.py b/tests/test_runtime/test_trainer.py index efc8ebcfc..d82b536ae 100644 --- a/tests/test_runtime/test_trainer.py +++ b/tests/test_runtime/test_trainer.py @@ -61,6 +61,23 @@ def forward_loss(self, batch, ctx=None): return super().forward_loss(batch, ctx) +class WeightedStrategy(FakeStrategy): + def forward_loss(self, batch, ctx=None): + self.validate_batch(batch) + coefficient = batch.tensors["x"].reshape(()) + denominator = batch.tensors["denominator"].reshape(()) + correct = batch.tensors["correct"].reshape(()) + numerator = self.model.w.reshape(()) * coefficient + return StepOutput( + loss=numerator / denominator, + metrics={"accuracy": correct / denominator}, + ratio_metrics={ + "acc": (correct, denominator), + }, + loss_terms=(numerator, denominator), + ) + + class FakeBackend(TrainingBackend): name = "fake" @@ -78,6 +95,11 @@ def backward(self, loss, *, is_boundary=True): self.boundaries.append(is_boundary) loss.backward() + def scale_gradients(self, factor): + for parameter in self.model.parameters(): + if parameter.grad is not None: + parameter.grad.mul_(factor) + def step(self): self.steps += 1 return torch.tensor(1.0) @@ -103,6 +125,19 @@ def _batch(): ) +def _weighted_batch(coefficient, denominator, correct): + return TrainBatch( + sample_ids=["s"], + strategy="fake", + tensors={ + "x": torch.tensor(float(coefficient)), + "denominator": torch.tensor(float(denominator)), + "correct": torch.tensor(float(correct)), + }, + metadata={}, + ) + + class TestTrainerCore(unittest.TestCase): def test_accumulation_boundary(self): strat = FakeStrategy() @@ -148,6 +183,60 @@ def test_metrics_carry_no_mode(self): rep = core.train_step(_batch()) self.assertNotIn("mode", rep.metrics) + def test_global_loss_normalization_matches_combined_batch(self): + split_strategy = WeightedStrategy() + split_core = TrainerCore( + split_strategy, + FakeBackend(split_strategy.model), + accumulation_steps=2, + ) + split_core.train_step(_weighted_batch(2, 1, 1)) + split_result = split_core.train_step(_weighted_batch(30, 3, 1)) + + combined_strategy = WeightedStrategy() + combined_core = TrainerCore( + combined_strategy, + FakeBackend(combined_strategy.model), + ) + combined_result = combined_core.train_step(_weighted_batch(32, 4, 2)) + + torch.testing.assert_close( + split_strategy.model.w.grad, + combined_strategy.model.w.grad, + ) + self.assertEqual(split_strategy.model.w.grad.item(), 8.0) + self.assertEqual(split_result.loss, combined_result.loss) + self.assertEqual(split_result.metrics["acc"], 0.5) + self.assertEqual(combined_result.metrics["acc"], 0.5) + + def test_global_loss_normalization_compensates_rank_averaging(self): + strategy = FakeStrategy() + backend = FakeBackend(strategy.model) + backend.parallel_config = mock.Mock(fsdp_process_group="dp") + core = TrainerCore(strategy, backend) + strategy.model.w.grad = torch.tensor([9.0]) + + def add_remote_denominator(denominator, *, op, group): + self.assertEqual(op, torch.distributed.ReduceOp.SUM) + self.assertEqual(group, "dp") + denominator.add_(7.0) + + with ( + mock.patch("torch.distributed.is_available", return_value=True), + mock.patch("torch.distributed.is_initialized", return_value=True), + mock.patch("torch.distributed.get_world_size", return_value=2), + mock.patch( + "torch.distributed.all_reduce", + side_effect=add_remote_denominator, + ), + ): + core._normalize_gradients(torch.tensor(3.0)) + + torch.testing.assert_close( + strategy.model.w.grad, + torch.tensor([1.8]), + ) + def test_strategy_scalar_metrics_are_preserved(self): strat = FakeStrategy() core = TrainerCore(strat, FakeBackend(strat.model), accumulation_steps=1) @@ -157,6 +246,7 @@ def test_strategy_scalar_metrics_are_preserved(self): metrics={ "loss": torch.tensor(99.0), "accuracy": torch.tensor(0.5), + "accuracy_denom": torch.tensor(4.0), "ce_loss": torch.tensor(1.25), "lambda_base": 0.75, "non_scalar_debug": torch.tensor([1.0, 2.0]), @@ -170,6 +260,7 @@ def test_strategy_scalar_metrics_are_preserved(self): self.assertEqual(result.metrics["acc"], 0.5) self.assertEqual(result.metrics["ce_loss"], 1.25) self.assertEqual(result.metrics["lambda_base"], 0.75) + self.assertNotIn("accuracy_denom", result.metrics) self.assertNotIn("non_scalar_debug", result.metrics) def test_ratio_metrics_override_mean_of_means_accuracy(self): diff --git a/tests/test_scripts/test_prepare_hidden_states.py b/tests/test_scripts/test_prepare_hidden_states.py index d30738054..e0e742b02 100644 --- a/tests/test_scripts/test_prepare_hidden_states.py +++ b/tests/test_scripts/test_prepare_hidden_states.py @@ -115,6 +115,11 @@ def test_strategy_capture_plans_use_draft_owned_layers_and_schemas(self): ) self.assertEqual(layers, plan.capture_layers) self.assertEqual(feature_names, set(plan.layout.output_names)) + if strategy == "eagle3": + self.assertIsNone(plan.loss_mask_filter) + else: + self.assertTrue(plan.loss_mask_filter([0, 1, 1])) + self.assertFalse(plan.loss_mask_filter([1, 0, 1])) def test_build_uses_dedicated_offline_loader(self): config = SimpleNamespace(num_hidden_layers=32, dtype=None) diff --git a/tests/test_utils/test_dflash_losses.py b/tests/test_utils/test_dflash_losses.py index 0e31adc8c..c083cb398 100644 --- a/tests/test_utils/test_dflash_losses.py +++ b/tests/test_utils/test_dflash_losses.py @@ -64,6 +64,10 @@ class _DFlashDraftStub(nn.Module): OnlineDSparkModel = _dflash_module.OnlineDSparkModel +def _anchor_sampler_subject(num_anchors: int = 8): + return types.SimpleNamespace(num_anchors=num_anchors) + + class _FixedDraft(nn.Module): def __init__(self, hidden_size: int): super().__init__() @@ -318,7 +322,7 @@ def _naive_dflash_loss(neg_log_q, binary_mask, gamma): positions = torch.arange(block_size, dtype=neg_log_q.dtype).view(1, 1, -1) decay = torch.exp(-(positions - 1).clamp(min=0) / gamma) weight = weight * decay - return (neg_log_q * weight).sum() / (weight.sum() + 1e-6) + return (neg_log_q * weight).sum() / weight.sum() class TestDFlashLosses(unittest.TestCase): @@ -363,6 +367,49 @@ def test_dflash_decay_gamma_is_preserved(self): want = _naive_dflash_loss(self.neg_log_q, self.binary_mask, gamma=gamma) torch.testing.assert_close(got, want, rtol=0, atol=1e-8) + def test_dflash_exposes_additive_loss_and_accuracy_terms(self): + head = nn.Linear(4, self.logits.shape[-1], bias=False).double() + model = _make_model( + self.logits, + self.anchors, + self.keep_mask, + draft_model=_LearnableDSparkDraft(4).double(), + lm_head=head, + ) + + loss, accuracy, metrics = model( + input_ids=self.input_ids, + hidden_states=self.hidden_states, + loss_mask=self.loss_mask, + ) + + loss_num, loss_den = metrics["loss_terms"] + self.assertTrue(loss_num.requires_grad) + torch.testing.assert_close(loss, loss_num / loss_den) + accuracy_num, accuracy_den = metrics["ratio_metrics"]["acc"] + torch.testing.assert_close(accuracy, accuracy_num / accuracy_den) + + def test_dflash_partial_tail_has_one_finite_target(self): + vocab_size = 7 + logits = torch.randn(1, 1, 5, vocab_size, dtype=torch.double) + input_ids = torch.tensor([[1, 2, 3, 4]]) + loss_mask = torch.ones_like(input_ids, dtype=torch.double) + model = _make_model( + logits, + anchors=torch.tensor([[2]]), + keep_mask=torch.tensor([[True]]), + ) + + loss, _accuracy, metrics = model( + input_ids=input_ids, + hidden_states=torch.zeros(1, 4, 4, dtype=torch.double), + loss_mask=loss_mask, + ) + + expected = F.cross_entropy(logits[0, 0, 1].unsqueeze(0), input_ids[:, 3]) + torch.testing.assert_close(loss, expected) + torch.testing.assert_close(metrics["loss_terms"][1], loss.new_tensor(1.0)) + def test_dpace_full_matches_naive_reference(self): alpha = 0.5 got = self._forward_loss(loss_type="dpace", dpace_alpha=alpha) @@ -406,12 +453,28 @@ def test_continuation_value_ablation_matches_naive_reference(self): def test_dpace_loss_reduces_by_batch_size(self): alpha = 0.5 - got = self._forward_loss(loss_type="dpace", dpace_alpha=alpha) + model = _make_model( + self.logits, + self.anchors, + self.keep_mask, + loss_type="dpace", + dpace_alpha=alpha, + ) + got, _accuracy, metrics = model( + input_ids=self.input_ids, + hidden_states=self.hidden_states, + loss_mask=self.loss_mask, + ) weight = _naive_dpace_weight(self.q, self.binary_mask, alpha, "dpace") weighted_sum = (self.neg_log_q * weight * self.binary_mask).sum() token_count_loss = weighted_sum / ((weight * self.binary_mask).sum() + 1e-6) batch_loss = weighted_sum / float(self.input_ids.shape[0]) torch.testing.assert_close(got, batch_loss, rtol=0, atol=1e-10) + torch.testing.assert_close(metrics["loss_terms"][0], weighted_sum) + torch.testing.assert_close( + metrics["loss_terms"][1], + got.new_tensor(float(self.input_ids.shape[0])), + ) self.assertFalse(torch.allclose(got, token_count_loss)) def test_alpha_changes_dpace_loss(self): @@ -774,6 +837,59 @@ def test_dspark_sampler_keeps_sparse_high_index_anchor(self): self.assertEqual(anchors[0, 0].item(), 4) self.assertEqual(keep[0].tolist(), [True, False]) + def test_shared_sampler_uses_adjacent_targets_and_partial_tails(self): + sampler = OnlineDFlashModel._sample_anchor_positions + model = _anchor_sampler_subject() + loss_mask = torch.tensor([[1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0]]) + + anchors, keep = sampler( + model, + seq_len=loss_mask.shape[1], + loss_mask=loss_mask, + device=loss_mask.device, + ) + + self.assertEqual(anchors[keep].tolist(), [0, 5]) + + def test_shared_sampler_is_batch_padding_invariant(self): + sampler = OnlineDFlashModel._sample_anchor_positions + model = _anchor_sampler_subject() + short_mask = torch.tensor([[0.0, 0.0, 1.0, 1.0]]) + short_anchors, short_keep = sampler( + model, + seq_len=4, + loss_mask=short_mask, + device=short_mask.device, + ) + padded_batch = torch.tensor( + [ + [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + ] + ) + batch_anchors, batch_keep = sampler( + model, + seq_len=7, + loss_mask=padded_batch, + device=padded_batch.device, + ) + + self.assertEqual(short_anchors[short_keep].tolist(), [2]) + self.assertEqual(batch_anchors[0][batch_keep[0]].tolist(), [2]) + self.assertFalse(batch_keep[2].any()) + + def test_shared_sampler_rejects_a_batch_without_adjacent_targets(self): + model = _anchor_sampler_subject() + loss_mask = torch.tensor([[1.0, 0.0, 1.0]]) + with self.assertRaisesRegex(ValueError, "two consecutive"): + OnlineDFlashModel._sample_anchor_positions( + model, + seq_len=loss_mask.shape[1], + loss_mask=loss_mask, + device=loss_mask.device, + ) + if __name__ == "__main__": unittest.main() From 9fc651469915cae15b535f301f0a4060397476a0 Mon Sep 17 00:00:00 2001 From: maocheng Date: Fri, 31 Jul 2026 21:04:13 -0700 Subject: [PATCH 03/88] Add configurable learning-rate schedules --- examples/configs/README.md | 1 + specforge/config/schema.py | 1 + specforge/lr_scheduler.py | 30 +++++++++++- specforge/optimizer.py | 25 ++++++++-- specforge/training/assembly.py | 1 + tests/test_config/test_schema.py | 9 +++- tests/test_optimizer/test_lr_scheduler.py | 59 +++++++++++++++++++++++ 7 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 tests/test_optimizer/test_lr_scheduler.py diff --git a/examples/configs/README.md b/examples/configs/README.md index 451e9ac6c..35efb5208 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -218,6 +218,7 @@ Common fields: | `training.accumulation_steps` | `1` | Positive microbatches per optimizer update. | | `training.fsdp_sharding` | `SHARD_GRAD_OP` | Trainer FSDP mode: `SHARD_GRAD_OP`, `FULL_SHARD`, or `NO_SHARD`. | | `training.learning_rate` | `1e-4` | Positive peak learning rate. | +| `training.lr_scheduler` | `cosine` | Learning-rate schedule after warmup: `cosine` or `constant`. | | `training.warmup_ratio` | `0.015` | Fraction in `[0, 1]` used for scheduler warmup. | | `training.max_grad_norm` | `0.5` | Positive gradient-clipping norm. | | `training.optimizer_cpu_offload` | `false` | Keep the optimizer's FP32 master parameters and Adam state on CPU. | diff --git a/specforge/config/schema.py b/specforge/config/schema.py index cabaa975d..4dacd85ae 100644 --- a/specforge/config/schema.py +++ b/specforge/config/schema.py @@ -488,6 +488,7 @@ class TrainingConfig(StrictConfigModel): accumulation_steps: int = Field(default=1, gt=0) fsdp_sharding: Literal["SHARD_GRAD_OP", "FULL_SHARD", "NO_SHARD"] = "SHARD_GRAD_OP" learning_rate: float = Field(default=1e-4, gt=0.0) + lr_scheduler: Literal["cosine", "constant"] = "cosine" warmup_ratio: float = Field(default=0.015, ge=0.0, le=1.0) max_grad_norm: float = Field(default=0.5, gt=0.0) #: Keep FP32 Adam masters and moments on CPU while the trainable draft diff --git a/specforge/lr_scheduler.py b/specforge/lr_scheduler.py index caf6b4cec..c375842bd 100644 --- a/specforge/lr_scheduler.py +++ b/specforge/lr_scheduler.py @@ -119,4 +119,32 @@ def __init__( super().__init__(optimizer, warmup_steps, base_scheduler, last_epoch=last_epoch) -__all__ = ["CosineAnnealingWarmupLR"] +class _FlatLR(_LRScheduler): + """Keep every parameter group at its configured base learning rate.""" + + def get_lr(self): + return self.base_lrs + + +class ConstantWarmupLR(_WarmupScheduler): + """Linear warmup followed by a constant learning rate.""" + + def __init__( + self, + optimizer, + total_steps: int, + warmup_steps: int = 0, + last_epoch: int = -1, + ): + if total_steps <= 0: + raise ValueError(f"total_steps must be positive, got {total_steps}") + if not 0 <= warmup_steps < total_steps: + raise ValueError( + "warmup_steps must be in [0, total_steps), got " + f"{warmup_steps} for total_steps={total_steps}" + ) + base_scheduler = _FlatLR(optimizer, last_epoch=last_epoch) + super().__init__(optimizer, warmup_steps, base_scheduler, last_epoch=last_epoch) + + +__all__ = ["ConstantWarmupLR", "CosineAnnealingWarmupLR"] diff --git a/specforge/optimizer.py b/specforge/optimizer.py index 3d5e1aabe..10c3fb137 100644 --- a/specforge/optimizer.py +++ b/specforge/optimizer.py @@ -3,7 +3,7 @@ import torch import torch.distributed as dist -from specforge.lr_scheduler import CosineAnnealingWarmupLR +from specforge.lr_scheduler import ConstantWarmupLR, CosineAnnealingWarmupLR from specforge.utils import print_on_rank0 logger = logging.getLogger(__name__) @@ -11,7 +11,7 @@ class BF16Optimizer: """AdamW over fp32 master copies of the bf16 trainable params, with grad - clipping and cosine warmup scheduling.""" + clipping and configurable warmup scheduling.""" def __init__( self, @@ -21,6 +21,7 @@ def __init__( max_grad_norm=0.5, total_steps=800_000, warmup_ratio=0.015, + lr_scheduler="cosine", offload_master=False, ): # defaults copied from EAGLE traineagle3 ds_config.json @@ -44,7 +45,17 @@ def __init__( self.last_grad_norm = None self._grad_norm_process_group = None self._reduce_grad_norm_across_ranks = True - self.scheduler = CosineAnnealingWarmupLR( + scheduler_types = { + "constant": ConstantWarmupLR, + "cosine": CosineAnnealingWarmupLR, + } + if lr_scheduler not in scheduler_types: + raise ValueError( + f"unsupported lr_scheduler={lr_scheduler!r}; " + f"expected one of {sorted(scheduler_types)}" + ) + self.lr_scheduler_type = lr_scheduler + self.scheduler = scheduler_types[lr_scheduler]( self.optimizer, total_steps=total_steps, warmup_steps=int(warmup_ratio * total_steps), @@ -160,6 +171,13 @@ def load_state_dict(self, state_dict): """Restore optimizer/scheduler state and, when present, the rank-local fp32 master params; without them the masters are re-cloned from the bf16 weights and the resume is not numerically faithful.""" + saved_scheduler_type = state_dict.get("lr_scheduler_type", "cosine") + if saved_scheduler_type != self.lr_scheduler_type: + raise ValueError( + "checkpoint optimizer used lr_scheduler=" + f"{saved_scheduler_type!r} but this run has " + f"lr_scheduler={self.lr_scheduler_type!r}" + ) saved_max_grad_norm = state_dict.get("max_grad_norm") if saved_max_grad_norm is not None and float(saved_max_grad_norm) != float( self.max_grad_norm @@ -204,6 +222,7 @@ def state_dict(self): return { "optimizer_state_dict": self.optimizer.state_dict(), "scheduler_state_dict": self.scheduler.state_dict(), + "lr_scheduler_type": self.lr_scheduler_type, "max_grad_norm": self.max_grad_norm, # rank-local fp32 masters; without them a resume re-quantizes from bf16 "fp32_params": [t.detach().cpu() for t in self.fp32_params], diff --git a/specforge/training/assembly.py b/specforge/training/assembly.py index 5fa2e3091..0e39cc480 100644 --- a/specforge/training/assembly.py +++ b/specforge/training/assembly.py @@ -268,6 +268,7 @@ def __call__(self, draft_module): lr=t.learning_rate, max_grad_norm=t.max_grad_norm, warmup_ratio=t.warmup_ratio, + lr_scheduler=t.lr_scheduler, total_steps=self.total_steps, offload_master=t.optimizer_cpu_offload, ) diff --git a/tests/test_config/test_schema.py b/tests/test_config/test_schema.py index 2825e5b91..03edbbd6a 100644 --- a/tests/test_config/test_schema.py +++ b/tests/test_config/test_schema.py @@ -626,9 +626,16 @@ def test_offline_dp_and_usp_topologies_are_validated(self): def test_overrides_coerce_and_revalidate(self): cfg = Config.model_validate(MINIMAL) out = apply_overrides( - cfg, ["training.learning_rate=1e-3", "training.max_steps=7", "run_id=r2"] + cfg, + [ + "training.learning_rate=1e-3", + "training.lr_scheduler=constant", + "training.max_steps=7", + "run_id=r2", + ], ) self.assertEqual(out.training.learning_rate, 1e-3) + self.assertEqual(out.training.lr_scheduler, "constant") self.assertEqual(out.training.max_steps, 7) self.assertEqual(out.run_id, "r2") # original untouched diff --git a/tests/test_optimizer/test_lr_scheduler.py b/tests/test_optimizer/test_lr_scheduler.py new file mode 100644 index 000000000..7f30dd146 --- /dev/null +++ b/tests/test_optimizer/test_lr_scheduler.py @@ -0,0 +1,59 @@ +import unittest + +import torch + +from specforge.optimizer import BF16Optimizer + + +def _optimizer(*, scheduler="cosine", total_steps=4, warmup_ratio=0.0): + model = torch.nn.Linear(2, 2, bias=False) + optimizer = BF16Optimizer( + model, + lr=1e-3, + max_grad_norm=1.0, + total_steps=total_steps, + warmup_ratio=warmup_ratio, + lr_scheduler=scheduler, + ) + return model, optimizer + + +class TestLearningRateScheduler(unittest.TestCase): + def test_constant_scheduler_keeps_base_lr_without_warmup(self): + model, optimizer = _optimizer(scheduler="constant") + observed = [optimizer.get_learning_rate()] + for _ in range(4): + model.weight.grad = torch.ones_like(model.weight) + optimizer.step() + observed.append(optimizer.get_learning_rate()) + self.assertEqual(observed, [1e-3] * 5) + + def test_constant_scheduler_supports_linear_warmup(self): + _model, optimizer = _optimizer( + scheduler="constant", total_steps=4, warmup_ratio=0.5 + ) + self.assertAlmostEqual(optimizer.get_learning_rate(), 5e-4) + optimizer.scheduler.step() + self.assertAlmostEqual(optimizer.get_learning_rate(), 1e-3) + optimizer.scheduler.step() + self.assertAlmostEqual(optimizer.get_learning_rate(), 1e-3) + + def test_unknown_scheduler_is_rejected(self): + with self.assertRaisesRegex(ValueError, "unsupported lr_scheduler"): + _optimizer(scheduler="linear") + + def test_resume_rejects_scheduler_change(self): + _model, cosine = _optimizer(scheduler="cosine") + _other_model, constant = _optimizer(scheduler="constant") + with self.assertRaisesRegex(ValueError, "checkpoint optimizer used"): + constant.load_state_dict(cosine.state_dict()) + + def test_legacy_checkpoint_defaults_to_cosine(self): + _model, cosine = _optimizer(scheduler="cosine") + state = cosine.state_dict() + state.pop("lr_scheduler_type") + self.assertEqual(state.get("lr_scheduler_type", "cosine"), "cosine") + + +if __name__ == "__main__": + unittest.main() From 3ef00949f5f0e8c4de2a96d52a4b9e9c7d4a8aef Mon Sep 17 00:00:00 2001 From: maocheng Date: Fri, 31 Jul 2026 21:06:59 -0700 Subject: [PATCH 04/88] Decouple online prompt ordering from run seed --- examples/configs/README.md | 1 + specforge/config/schema.py | 3 +++ specforge/training/disaggregated.py | 12 +++++++++--- tests/test_runtime/test_schedule.py | 30 +++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/examples/configs/README.md b/examples/configs/README.md index 35efb5208..c6caec046 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -236,6 +236,7 @@ Common fields: | `training.compact_teacher_chunk_size` | `null` | Positive vocabulary chunk size; requires `compact_teacher: true`. | | `training.role` | `all` | Use `all` for local offline training; disaggregated entrypoints select `auto`, `producer`, or `consumer`. | | `training.seed` | `42` | Run and per-rank RNG seed. | +| `training.prompt_seed` | `null` | Optional online prompt-shuffle seed. `null` preserves the historical behavior of using `training.seed`. | Strategy-specific fields should be written only when tuning that objective: diff --git a/specforge/config/schema.py b/specforge/config/schema.py index 4dacd85ae..f70f80521 100644 --- a/specforge/config/schema.py +++ b/specforge/config/schema.py @@ -547,6 +547,9 @@ class TrainingConfig(StrictConfigModel): #: and different roles. role: Literal["auto", "all", "producer", "consumer"] = "all" seed: int = 42 + #: Deterministic online prompt ordering. ``None`` preserves the historical + #: behavior of using the run RNG seed for both model and prompt sampling. + prompt_seed: Optional[int] = None @model_validator(mode="after") def _validate_training_shape(self): diff --git a/specforge/training/disaggregated.py b/specforge/training/disaggregated.py index 7b9bbf058..e68e4139f 100644 --- a/specforge/training/disaggregated.py +++ b/specforge/training/disaggregated.py @@ -201,6 +201,12 @@ def _consumer_database_path(cfg: Config) -> Optional[str]: return os.path.join(state_dir, "consumer.sqlite") +def _online_prompt_seed(cfg: Config) -> int: + """Resolve prompt ordering independently while preserving old configs.""" + configured = getattr(cfg.training, "prompt_seed", None) + return cfg.training.seed if configured is None else configured + + def _online_schedule_payload(cfg: Config, *, num_prompts: int) -> dict: """Describe the exact finite online schedule prepared by the producer.""" from specforge.training.schedule import resolve_online_total_steps @@ -219,7 +225,7 @@ def _online_schedule_payload(cfg: Config, *, num_prompts: int) -> dict: "total_steps": total_steps, "num_prompts": num_prompts, "prompt_epochs": cfg.training.num_epochs, - "prompt_seed": cfg.training.seed, + "prompt_seed": _online_prompt_seed(cfg), "dp_size": dp_size, "batch_size": cfg.training.batch_size, "accumulation_steps": cfg.training.accumulation_steps, @@ -246,7 +252,7 @@ def _read_online_total_steps(cfg: Config, channel_path: str) -> int: expected = { "version": 1, "prompt_epochs": cfg.training.num_epochs, - "prompt_seed": cfg.training.seed, + "prompt_seed": _online_prompt_seed(cfg), "dp_size": trainer.nnodes * trainer.nproc_per_node, "batch_size": cfg.training.batch_size, "accumulation_steps": cfg.training.accumulation_steps, @@ -623,7 +629,7 @@ def _build_online( target_repr=target_repr, aux_hidden_state_layer_ids=layers, prompt_epochs=cfg.training.num_epochs, - prompt_seed=cfg.training.seed, + prompt_seed=_online_prompt_seed(cfg), lease=cfg.runtime.producer_lease, in_flight_high_watermark=in_flight_high_watermark, in_flight_low_watermark=in_flight_low_watermark, diff --git a/tests/test_runtime/test_schedule.py b/tests/test_runtime/test_schedule.py index 4a28c3adb..2d25c04ca 100644 --- a/tests/test_runtime/test_schedule.py +++ b/tests/test_runtime/test_schedule.py @@ -94,6 +94,7 @@ def test_online_schedule_sidecar_round_trips_the_producer_horizon(self): training=SimpleNamespace( num_epochs=3, seed=17, + prompt_seed=None, batch_size=2, accumulation_steps=4, ), @@ -117,6 +118,35 @@ def test_online_schedule_sidecar_round_trips_the_producer_horizon(self): with self.assertRaisesRegex(ValueError, "does not match"): _read_online_total_steps(cfg, channel_path) + def test_online_prompt_seed_is_independent_from_model_seed(self): + cfg = SimpleNamespace( + training=SimpleNamespace( + num_epochs=3, + seed=17, + prompt_seed=5, + batch_size=2, + accumulation_steps=4, + ), + deployment=SimpleNamespace( + trainer=SimpleNamespace(nnodes=2, nproc_per_node=2) + ), + ) + payload = _online_schedule_payload(cfg, num_prompts=100) + self.assertEqual(payload["prompt_seed"], 5) + + with tempfile.TemporaryDirectory() as directory: + channel_path = f"{directory}/refs.jsonl" + _write_control( + channel_path + _ONLINE_SCHEDULE_SUFFIX, + json.dumps(payload), + ) + cfg.training.seed = 18 + self.assertEqual(_read_online_total_steps(cfg, channel_path), 9) + + cfg.training.prompt_seed = 6 + with self.assertRaisesRegex(ValueError, "does not match"): + _read_online_total_steps(cfg, channel_path) + def test_fixed_plan_rejects_partial_accumulation_before_training(self): with self.assertRaisesRegex( ValueError, "ends with incomplete gradient accumulation" From 99431d9dcaad0ac64801d4655b82109308979a3a Mon Sep 17 00:00:00 2001 From: maocheng Date: Fri, 31 Jul 2026 21:15:15 -0700 Subject: [PATCH 05/88] Add Kimi K3 DSpark capture support --- docs/basic_usage/disaggregated_training.md | 5 +- .../sglang/kimi-k3-f8493a4/spec-capture.patch | 703 ++++++++++++++++++ scripts/apply_sglang_spec_capture_patch.sh | 57 +- specforge/algorithms/dspark/providers.py | 4 +- specforge/inference/sglang_patch_inventory.md | 33 +- .../offline_capture/sglang_backend/capture.py | 4 +- tests/test_algorithms/test_builtin_parity.py | 7 + .../test_offline_capture_layout.py | 29 +- .../test_disaggregated_model_loading.py | 31 + 9 files changed, 839 insertions(+), 34 deletions(-) create mode 100644 patches/sglang/kimi-k3-f8493a4/spec-capture.patch diff --git a/docs/basic_usage/disaggregated_training.md b/docs/basic_usage/disaggregated_training.md index b4d5f9cb3..a3a20b54f 100644 --- a/docs/basic_usage/disaggregated_training.md +++ b/docs/basic_usage/disaggregated_training.md @@ -315,8 +315,9 @@ node-local deployment values. The online producer sends prompts to the URLs in `deployment.disaggregated.server_urls`. Start a patched SGLang server separately with the model, capture method, and auxiliary layer ids matching the draft -config. DFlash, Domino, and DSpark use the DFlash capture contract; EAGLE3 and -P-EAGLE use the EAGLE3 capture contract. Capture rejects chunked prefill and +config. DFlash and Domino use the DFlash capture contract, DSpark uses its +dedicated K3 capture contract, and EAGLE3 and P-EAGLE use the EAGLE3 capture +contract. Capture rejects chunked prefill and gives every request attempt a unique radix-cache namespace so cached prefixes cannot truncate the captured sequence. Online capture is text-only: VLM training, including Qwen2.5-VL, is not supported. Online evaluation is also not diff --git a/patches/sglang/kimi-k3-f8493a4/spec-capture.patch b/patches/sglang/kimi-k3-f8493a4/spec-capture.patch new file mode 100644 index 000000000..93c5f499e --- /dev/null +++ b/patches/sglang/kimi-k3-f8493a4/spec-capture.patch @@ -0,0 +1,703 @@ +diff --git a/python/sglang/srt/layers/attn_residual.py b/python/sglang/srt/layers/attn_residual.py +index 266d2f2..2a75af7 100644 +--- a/python/sglang/srt/layers/attn_residual.py ++++ b/python/sglang/srt/layers/attn_residual.py +@@ -115,10 +115,28 @@ def _score_kernel( + BLOCK_H: tl.constexpr, + ): + """One CTA per (token, row): scan H, output one scalar score.""" +- pid_t = tl.program_id(0) ++ # bank stride is 8 * 7168; 64K token offsets exceed signed int32. ++ pid_t = tl.program_id(0).to(tl.int64) + j = tl.program_id(1) + if j > NVB: + return ++ ++ # The raw K3 residual stream can remain finite near the bf16 limit. A ++ # direct fp32 sum(v * v) then overflows even though RMSNorm(v) is well ++ # defined, making this DSpark-only capture path emit NaN scores. Scale ++ # each row before the norm/projection reduction to keep intermediates ++ # representable without changing the normalized score. ++ max_abs = 0.0 ++ for h0 in tl.static_range(0, H, BLOCK_H): ++ offs_h = h0 + tl.arange(0, BLOCK_H) ++ if j < NVB: ++ v = tl.load(bank_ptr + pid_t * stride_bm + j * stride_bb + offs_h).to( ++ tl.float32 ++ ) ++ else: ++ v = tl.load(prefix_ptr + pid_t * stride_pm + offs_h).to(tl.float32) ++ max_abs = tl.maximum(max_abs, tl.max(tl.abs(v), axis=0)) ++ scale = tl.maximum(max_abs, 1.0) + sumsq = 0.0 + dotv = 0.0 + for h0 in tl.static_range(0, H, BLOCK_H): +@@ -130,9 +148,10 @@ def _score_kernel( + else: + v = tl.load(prefix_ptr + pid_t * stride_pm + offs_h).to(tl.float32) + cw = tl.load(cw_ptr + offs_h) +- sumsq += tl.sum(v * v) +- dotv += tl.sum(v * cw) +- rrms = 1.0 / tl.sqrt(sumsq / H + eps) ++ v_scaled = v / scale ++ sumsq += tl.sum(v_scaled * v_scaled) ++ dotv += tl.sum(v_scaled * cw) ++ rrms = 1.0 / tl.sqrt(sumsq / H + eps / scale / scale) + tl.store(scores_ptr + pid_t * stride_sm + j, dotv * rrms) + + +@@ -159,7 +178,8 @@ def _combine_kernel( + Softmax is redundantly computed by each H-chunk CTA (≤16 elements, trivial). + This gives full H-parallelism: 7 CTAs for H=7168/1024. + """ +- pid_t = tl.program_id(0) ++ # Keep bank/scores/output pointer arithmetic in the same 64-bit domain. ++ pid_t = tl.program_id(0).to(tl.int64) + pid_h = tl.program_id(1) + h0 = pid_h * BLOCK_H + +diff --git a/python/sglang/srt/layers/logits_processor.py b/python/sglang/srt/layers/logits_processor.py +index cf55b97..51a1872 100644 +--- a/python/sglang/srt/layers/logits_processor.py ++++ b/python/sglang/srt/layers/logits_processor.py +@@ -159,6 +159,9 @@ class LogitsProcessorOutput: + # Used by speculative decoding (EAGLE) + # The last hidden layers + hidden_states: Optional[torch.Tensor] = None ++ # Spec-training capture: under FULL+aux, `hidden_states` is the aux ++ # concatenation, so the post-norm last hidden is exposed here separately. ++ last_hidden_states: Optional[torch.Tensor] = None + + ## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler + # he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature. +@@ -443,6 +446,16 @@ class LogitsProcessor(nn.Module): + sample_indices, + logits_metadata, + ) ++ # Spec-training capture: keep the post-norm last hidden (the training ++ # target) alongside the aux concatenation in hidden_states_to_store. ++ last_hidden_states_to_store = ( ++ hidden_states ++ if ( ++ logits_metadata.capture_hidden_mode.is_full() ++ and aux_hidden_states is not None ++ ) ++ else None ++ ) + del hidden_states + + if not logits_metadata.extend_return_logprob: +@@ -456,6 +469,7 @@ class LogitsProcessor(nn.Module): + return LogitsProcessorOutput( + next_token_logits=sampled_logits, + hidden_states=hidden_states_to_store, ++ last_hidden_states=last_hidden_states_to_store, + # FIXME: These fields are not logits-related but are passed through here as a + # workaround since ForwardBatch is local to forward_batch_generation(). + # They should be moved to GenerationBatchResult to keep this class clean. +diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py b/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py +index 1bf6aa7..e7a3030 100644 +--- a/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py ++++ b/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py +@@ -402,6 +402,9 @@ def fused_marlin_moe( + and intermediate_cache3.is_contiguous() + and output.is_contiguous() + and intermediate_cache3.shape[-1] % 8 == 0 ++ # The JIT helper maps M to CUDA grid.y, whose architectural limit ++ # is 65,535. Long-context prefills use the generic reduction. ++ and intermediate_cache3.shape[0] <= 65_535 + ): + from sglang.kernels.ops.moe.moe_topk_sum import moe_topk_sum + +diff --git a/python/sglang/srt/managers/detokenizer_manager.py b/python/sglang/srt/managers/detokenizer_manager.py +index 7b3ca52..84e0ae6 100644 +--- a/python/sglang/srt/managers/detokenizer_manager.py ++++ b/python/sglang/srt/managers/detokenizer_manager.py +@@ -472,6 +472,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): + output_token_sampling_mask=recv_obj.output_token_sampling_mask, + output_token_sampling_logprobs=recv_obj.output_token_sampling_logprobs, + output_hidden_states=recv_obj.output_hidden_states, ++ spec_capture=recv_obj.spec_capture, + routed_experts=routed_experts, + indexer_topk=indexer_topk, + customized_info=recv_obj.customized_info, +diff --git a/python/sglang/srt/managers/io_struct.py b/python/sglang/srt/managers/io_struct.py +index fa359c5..d9881c0 100644 +--- a/python/sglang/srt/managers/io_struct.py ++++ b/python/sglang/srt/managers/io_struct.py +@@ -303,6 +303,10 @@ class GenerateReqInput: + # For Unlimited-OCR + images_config: Optional[dict] = None + ++ # Spec-training capture sink instructions (see spec_capture_sink.py). ++ # Batch-level: List[Optional[dict]]; per-request after __getitem__. ++ spec_capture: Optional[Union[List[Optional[Dict]], Dict]] = None ++ + # Pre-computed delimiter indices for multi-item scoring. + # Batch-level: List[List[int]] (one per request). After __getitem__: List[int]. + multi_item_delimiter_indices: Optional[Union[List[List[int]], List[int]]] = None +@@ -780,6 +784,11 @@ class GenerateReqInput: + if self.multi_item_delimiter_indices is not None + else None + ), ++ spec_capture=( ++ self.spec_capture[i] ++ if isinstance(self.spec_capture, list) ++ else self.spec_capture ++ ), + ) + cache[i] = sub + return sub +@@ -874,6 +883,9 @@ class TokenizedGenerateReqInput(BaseReq, kw_only=True): + # Internal IPC only. + encoder_urls: Optional[List[str]] = None + ++ # Spec-training capture sink instructions (see GenerateReqInput.spec_capture) ++ spec_capture: Optional[Dict] = None ++ + # Pre-computed delimiter indices for multi-item scoring + multi_item_delimiter_indices: Optional[List[int]] = None + +@@ -1267,6 +1279,9 @@ class BatchTokenIDOutput(BaseBatchReq, kw_only=True): + # Number of times each request was retracted. + retraction_counts: Optional[List[int]] = None + ++ # Spec-training capture: one result dict per request (see spec_capture_sink). ++ spec_capture: Optional[List[Any]] = None ++ + # The trainer step id. Used to know which step's weights are used for sampling. + token_steps: Optional[List[List[int]]] = None + +@@ -1349,6 +1364,9 @@ class BatchStrOutput(BaseBatchReq, kw_only=True): + # Number of times each request was retracted. + retraction_counts: Optional[List[int]] = None + ++ # Spec-training capture: one result dict per request (see spec_capture_sink). ++ spec_capture: Optional[List[Any]] = None ++ + # The trainer step id. Used to know which step's weights are used for sampling. + token_steps: Optional[List[List[int]]] = None + +diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py +index af2eca5..1b50341 100755 +--- a/python/sglang/srt/managers/schedule_batch.py ++++ b/python/sglang/srt/managers/schedule_batch.py +@@ -759,6 +759,7 @@ class Req(ReqDllmMixin): + Union[APIServerReqTimeStats, DPControllerReqTimeStats] + ] = None, + return_pooled_hidden_states: bool = False, ++ spec_capture: Optional[Dict[str, Any]] = None, + multi_item_delimiter_indices: Optional[List[int]] = None, + session_id: Optional[str] = None, + ): +@@ -821,7 +822,13 @@ class Req(ReqDllmMixin): + } + self.sampling_params = sampling_params + self.custom_logit_processor = custom_logit_processor +- self.return_hidden_states = return_hidden_states ++ # Spec-training capture: piggyback the return_hidden_states path (fires ++ # CaptureHiddenMode.FULL); the sink consumes the slices, not the response. ++ self.spec_capture = spec_capture ++ self.return_hidden_states = return_hidden_states or spec_capture is not None ++ self.spec_capture_aux: List[torch.Tensor] = [] ++ self.spec_capture_last_hidden: List[torch.Tensor] = [] ++ self.spec_capture_result = None # per-request sink result -> output field + + # extra key for classifying the request (e.g. cache_salt) + if lora_id is not None: +diff --git a/python/sglang/srt/managers/scheduler.py b/python/sglang/srt/managers/scheduler.py +index ef024da..6ab1dce 100644 +--- a/python/sglang/srt/managers/scheduler.py ++++ b/python/sglang/srt/managers/scheduler.py +@@ -585,6 +585,19 @@ class Scheduler( + + self.init_batch_result_processor() + ++ if server_args.enable_spec_capture: ++ # Capture needs single-pass prefill; chunking would drop all but the ++ # final chunk's hidden rows. ++ if server_args.chunked_prefill_size != -1: ++ raise ValueError( ++ "--enable-spec-capture requires --chunked-prefill-size -1 " ++ "(single-pass prefill) so captured hidden states cover the " ++ "whole sequence" ++ ) ++ from sglang.srt import spec_capture_sink ++ ++ spec_capture_sink.maybe_init_sink(server_args) ++ + self.is_initializing = False + + def init_zbal_on_npu(self): +@@ -2196,6 +2209,7 @@ class Scheduler( + dllm_config=self.dllm_config, + time_stats=recv_req.time_stats, + multi_item_delimiter_indices=recv_req.multi_item_delimiter_indices, ++ spec_capture=recv_req.spec_capture, + ) + req.tokenizer = self.tokenizer + +diff --git a/python/sglang/srt/managers/scheduler_components/batch_result_processor.py b/python/sglang/srt/managers/scheduler_components/batch_result_processor.py +index 248a929..b2d8f42 100644 +--- a/python/sglang/srt/managers/scheduler_components/batch_result_processor.py ++++ b/python/sglang/srt/managers/scheduler_components/batch_result_processor.py +@@ -263,6 +263,18 @@ class SchedulerBatchResultProcessor: + self.add_sampling_mask_return_values(i, req, logits_output) + + if ( ++ req.spec_capture is not None ++ and logits_output.hidden_states is not None ++ ): ++ # Spec-training capture: keep tensor slices, sink on finish. ++ hidden_state_offset = self._append_spec_capture_states( ++ req=req, ++ logits_output=logits_output, ++ hidden_state_offset=hidden_state_offset, ++ ) ++ if req.finished(): ++ self._sink_spec_capture(req) ++ elif ( + req.return_hidden_states + and logits_output.hidden_states is not None + ): +@@ -470,6 +482,62 @@ class SchedulerBatchResultProcessor: + f"Placeholder zeros would be appended to output_ids." + ) + ++ def _append_spec_capture_states( ++ self, ++ *, ++ req: Req, ++ logits_output: LogitsProcessorOutput, ++ hidden_state_offset: int, ++ ) -> int: ++ """Accumulate captured rows as CPU tensors for the Mooncake sink. ++ ++ Same offset arithmetic as ``_append_prefill_hidden_states`` but keeps ++ tensor slices (aux concat in ``hidden_states``, post-norm last in ++ ``last_hidden_states``) rather than the JSON-able response payload. ++ """ ++ start = hidden_state_offset ++ end = start + len(req.origin_input_ids) ++ req.spec_capture_aux.append( ++ logits_output.hidden_states[start:end].cpu().clone() ++ ) ++ if logits_output.last_hidden_states is not None: ++ req.spec_capture_last_hidden.append( ++ logits_output.last_hidden_states[start:end].cpu().clone() ++ ) ++ return end ++ ++ def _sink_spec_capture(self, req: Req) -> None: ++ """Write a finished capture request's tensors to the Mooncake sink. ++ ++ Runs on the attention-TP rank that streams output; the per-request result ++ (or an ``{"error": ...}`` marker) is set on ``req.spec_capture_result``, ++ returned to the client via the dedicated ``spec_capture`` output field ++ (a per-request channel, unlike per-token ``customized_info``). ++ """ ++ from sglang.srt import spec_capture_sink ++ ++ sink = spec_capture_sink.get_sink() ++ if sink is None or self.output_streamer.ps.attn_tp_rank != 0: ++ return ++ aux = torch.cat(req.spec_capture_aux, dim=0) if req.spec_capture_aux else None ++ last_hidden = ( ++ torch.cat(req.spec_capture_last_hidden, dim=0) ++ if req.spec_capture_last_hidden ++ else None ++ ) ++ try: ++ req.spec_capture_result = sink.put_sample( ++ req.spec_capture, aux=aux, last_hidden=last_hidden ++ ) ++ except Exception as e: ++ logger.error("spec-capture sink failed for %s: %s", req.rid, e) ++ req.spec_capture_result = { ++ "sample_id": req.spec_capture.get("sample_id"), ++ "error": str(e), ++ } ++ req.spec_capture_aux = [] ++ req.spec_capture_last_hidden = [] ++ + def _append_prefill_hidden_states( + self, + *, +diff --git a/python/sglang/srt/managers/scheduler_components/output_streamer.py b/python/sglang/srt/managers/scheduler_components/output_streamer.py +index 278ccf4..7a9d8d5 100644 +--- a/python/sglang/srt/managers/scheduler_components/output_streamer.py ++++ b/python/sglang/srt/managers/scheduler_components/output_streamer.py +@@ -289,6 +289,7 @@ class _GenerationStreamAccumulator: + spec_cap_lens_histogram: list = field(default_factory=list) + retraction_counts: list = field(default_factory=list) + output_hidden_states: Optional[list] = None ++ spec_capture: list = field(default_factory=list) + routed_experts: Optional[list] = None + indexer_topk: Optional[list] = None + customized_info: dict = field(default_factory=dict) +@@ -525,6 +526,8 @@ class _GenerationStreamAccumulator: + self.output_hidden_states.append(hs) + else: + self.output_hidden_states.append(None) ++ # Per-request spec-capture result (aligned with rids), like the field above. ++ self.spec_capture.append(getattr(req, "spec_capture_result", None)) + if self.return_routed_experts: + self.routed_experts.append( + req.routed_experts if req.return_routed_experts else None +@@ -600,6 +603,7 @@ class _GenerationStreamAccumulator: + output_token_sampling_mask=self.output_token_sampling_mask, + output_token_sampling_logprobs=self.output_token_sampling_logprobs, + output_hidden_states=self.output_hidden_states, ++ spec_capture=self.spec_capture or None, + routed_experts=self.routed_experts, + indexer_topk=self.indexer_topk, + customized_info=( +diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py +index 2228008..15cb30a 100644 +--- a/python/sglang/srt/managers/tokenizer_manager.py ++++ b/python/sglang/srt/managers/tokenizer_manager.py +@@ -1231,6 +1231,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): + multi_item_delimiter_indices=obj.multi_item_delimiter_indices, + mm_data_mooncake=obj.mm_data_mooncake, + encoder_urls=obj.encoder_urls, ++ spec_capture=obj.spec_capture, + ) + elif isinstance(obj, EmbeddingReqInput): + # Resolve unresolved embed overrides now that input_ids are available +@@ -2018,6 +2019,10 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): + hidden_states = recv_obj.output_hidden_states[i] + if hidden_states is not None: + meta_info["hidden_states"] = hidden_states ++ if getattr(recv_obj, "spec_capture", None): ++ sc = recv_obj.spec_capture[i] ++ if sc is not None: ++ meta_info["spec_capture"] = sc + if getattr(recv_obj, "routed_experts", None): + val = recv_obj.routed_experts[i] + if val is not None: +diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py +index 3d668b2..7d1f889 100644 +--- a/python/sglang/srt/model_executor/model_runner.py ++++ b/python/sglang/srt/model_executor/model_runner.py +@@ -487,6 +487,26 @@ class ModelRunner: + is_draft_worker=self.is_draft_worker, + ) + ) ++ if self.server_args.enable_spec_capture and not self.is_draft_worker: ++ # Spec-training capture runs without a speculative draft worker, so ++ # populate the same aux-state configuration that online decoding ++ # would normally derive from the draft model. ++ capture_method = self.server_args.spec_capture_method ++ if capture_method in ("dflash", "dspark"): ++ self.spec_aux_config.dflash_use_aux_hidden_state = True ++ self.spec_aux_config.dflash_target_layer_ids = ( ++ self.server_args.spec_capture_aux_layer_ids ++ ) ++ elif capture_method == "eagle3": ++ self.spec_aux_config.eagle_use_aux_hidden_state = True ++ self.spec_aux_config.eagle_aux_hidden_state_layer_ids = ( ++ self.server_args.spec_capture_aux_layer_ids ++ ) ++ else: ++ raise ValueError( ++ "--spec-capture-method must be one of: eagle3, dflash, dspark; " ++ f"got {capture_method!r}" ++ ) + + def init_weight_exporter(self): + self.weight_exporter = WeightExporter( +@@ -863,7 +883,13 @@ class ModelRunner: + eagle_aux_hidden_state_layer_ids=self.spec_aux_config.eagle_aux_hidden_state_layer_ids, + dflash_use_aux_hidden_state=self.spec_aux_config.dflash_use_aux_hidden_state, + dflash_target_layer_ids=self.spec_aux_config.dflash_target_layer_ids, +- is_dspark=self.spec_algorithm.is_dspark(), ++ is_dspark=( ++ self.spec_algorithm.is_dspark() ++ or ( ++ self.server_args.enable_spec_capture ++ and self.server_args.spec_capture_method == "dspark" ++ ) ++ ), + ) + backends = build_attention_backends(model_runner=self) + self.attn_backend = backends.attn_backend +diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py +index 62a0177..3f40061 100644 +--- a/python/sglang/srt/server_args.py ++++ b/python/sglang/srt/server_args.py +@@ -3333,6 +3333,26 @@ class ServerArgs: + enable_return_hidden_states: A[ + bool, "Enable returning hidden states with responses.", NS("exec.features") + ] = False ++ enable_spec_capture: A[ ++ bool, ++ "Enable server-side speculative-training capture: per-request aux/last " ++ "hidden states are written to a Mooncake store (SpecForge DataFlow " ++ "layout) instead of the response payload. Enables aux-hidden-state " ++ "capture on the target model without a speculative draft worker.", ++ ] = False ++ spec_capture_aux_layer_ids: A[ ++ Optional[List[int]], ++ "Target layer ids whose hidden states are captured (concatenated) for " ++ "spec-capture requests. Defaults to the model's EAGLE3 default layers " ++ "(low/mid/high) when unset.", ++ ] = None ++ spec_capture_method: A[ ++ str, ++ "Capture method for --enable-spec-capture: 'eagle3', 'dflash', or " ++ "'dspark'. Must " ++ "match the draft strategy being trained; they wire capture onto " ++ "different target-model submodules.", ++ ] = "eagle3" + enable_return_routed_experts: A[ + bool, + "Enable returning routed experts of each layer with responses.", +diff --git a/python/sglang/srt/spec_capture_sink.py b/python/sglang/srt/spec_capture_sink.py +new file mode 100644 +index 0000000..038a084 +--- /dev/null ++++ b/python/sglang/srt/spec_capture_sink.py +@@ -0,0 +1,243 @@ ++# Copyright 2024 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 ++"""Server-side spec-training capture sink (SpecForge DataFlow transport). ++ ++Under ``--enable-spec-capture``, a request's ``spec_capture`` dict tells this ++sink to write the prefill's captured tensors straight into a Mooncake store ++(one hard-pinned object per tensor at ``{store_id}/{sample_id}/g{gen}/{name}``, ++raw bytes — shape/dtype travel on the returned spec). Feature tensors never ++touch the response path; ``meta_info["spec_capture"]`` returns only keys + ++shapes/dtypes. Strategy naming is the client's (the ``features`` mapping); the ++server knows only generic artifacts. Self-contained: the scheduler hooks are ++one-liners, every capture decision lives here. ++ ++Request schema:: ++ ++ {"store_id", "sample_id", "gen", "replace", # key namespace / retry policy ++ "features": {"aux": , "last_hidden": }, # artifact -> feature ++ "passthrough": [{"name", "data", "shape", "dtype"}]} # client tensors verbatim ++ ++Response (``meta_info["spec_capture"]``): ``{"sample_id", "store_id", "gen", ++"aux_layer_ids", "features": {name: {"shape", "dtype"}}}``. ++ ++Mooncake connection uses the standard ``MOONCAKE_*`` env vars (see ++``MooncakeFeatureStore``). ++""" ++ ++from __future__ import annotations ++ ++import logging ++import os ++import threading ++from typing import Any, Dict, List, Optional ++ ++import torch ++ ++logger = logging.getLogger(__name__) ++ ++# torch dtype -> the FeatureSpec dtype string SpecForge's zero-copy get() maps ++# back to a torch dtype. Keep in sync with MooncakeFeatureStore._TORCH_DTYPES. ++_DTYPE_STR = { ++ torch.float32: "float32", ++ torch.float64: "float64", ++ torch.float16: "float16", ++ torch.bfloat16: "bfloat16", ++ torch.int64: "int64", ++ torch.int32: "int32", ++ torch.int16: "int16", ++ torch.int8: "int8", ++ torch.uint8: "uint8", ++ torch.bool: "bool", ++} ++_STR_DTYPE = {v: k for k, v in _DTYPE_STR.items()} ++ ++_ARTIFACT_AUX = "aux" ++_ARTIFACT_LAST_HIDDEN = "last_hidden" ++ ++ ++class SpecCaptureSink: ++ """Writes captured per-request tensors into Mooncake in SpecForge layout.""" ++ ++ def __init__(self, aux_layer_ids: Optional[List[int]] = None) -> None: ++ self.aux_layer_ids = list(aux_layer_ids) if aux_layer_ids else None ++ self._store = None ++ self._put_config = None ++ self._lock = threading.Lock() ++ # Retried HTTP requests reuse deterministic keys. Striped locks keep ++ # replacement atomic per key without retaining one lock per sample. ++ self._write_locks = [threading.Lock() for _ in range(256)] ++ ++ # -- connection --------------------------------------------------------- ++ def _connect(self): ++ if self._store is not None: ++ return self._store ++ with self._lock: ++ if self._store is not None: ++ return self._store ++ from mooncake.store import MooncakeDistributedStore, ReplicateConfig ++ ++ store = MooncakeDistributedStore() ++ rc = store.setup( ++ local_hostname=os.environ.get("MOONCAKE_LOCAL_HOSTNAME", "localhost"), ++ metadata_server=os.environ.get( ++ "MOONCAKE_METADATA_SERVER", "http://localhost:8080/metadata" ++ ), ++ global_segment_size=int( ++ os.environ.get("MOONCAKE_GLOBAL_SEGMENT_SIZE", 1 << 30) ++ ), ++ local_buffer_size=int( ++ os.environ.get("MOONCAKE_LOCAL_BUFFER_SIZE", 1 << 30) ++ ), ++ protocol=os.environ.get("MOONCAKE_PROTOCOL", "tcp"), ++ rdma_devices=os.environ.get("MOONCAKE_RDMA_DEVICES", ""), ++ master_server_addr=os.environ.get( ++ "MOONCAKE_MASTER_SERVER_ADDR", "localhost:50051" ++ ), ++ ) ++ if rc is not None and int(rc) != 0: ++ raise RuntimeError(f"spec-capture mooncake setup failed (status {rc})") ++ # Hard-pin every object: SpecForge (not Mooncake's LRU) is the ++ # lifetime authority — a committed feature must never be evicted ++ # before the trainer consumes it. ++ cfg = ReplicateConfig() ++ cfg.replica_num = 1 ++ cfg.with_hard_pin = True ++ self._put_config = cfg ++ self._store = store ++ logger.info("spec-capture mooncake sink connected") ++ return store ++ ++ # -- key/put primitives (pinned to MooncakeFeatureStore's layout) -------- ++ @staticmethod ++ def _tkey(store_id: str, sample_id: str, gen: int, name: str) -> str: ++ return f"{store_id}/{sample_id}/g{gen}/{name}" ++ ++ def _put_tensor( ++ self, key: str, t: torch.Tensor, *, replace: bool = False ++ ) -> None: ++ store = self._connect() ++ t = t.detach().to("cpu").contiguous() ++ nbytes = t.element_size() * t.numel() ++ lock = self._write_locks[hash(key) % len(self._write_locks)] ++ with lock: ++ if replace: ++ # Do not probe with is_exist(): Mooncake existence checks can ++ # acquire a read lease that prevents the following removal. ++ self._remove_quiet(key) ++ try: ++ store.register_buffer(t.data_ptr(), nbytes) ++ except Exception: ++ pass # some builds auto-register ++ try: ++ rc = store.put_from(key, t.data_ptr(), nbytes, self._put_config) ++ finally: ++ try: ++ store.unregister_buffer(t.data_ptr()) ++ except Exception: ++ pass ++ if rc is not None and int(rc) < 0: ++ raise RuntimeError(f"spec-capture put_from failed (status {rc}) for {key}") ++ ++ def _remove_quiet(self, key: str) -> None: ++ try: ++ self._connect().remove(key) ++ except Exception: ++ pass ++ ++ # -- the one entry point -------------------------------------------------- ++ def put_sample( ++ self, ++ spec: Dict[str, Any], ++ *, ++ aux: Optional[torch.Tensor], ++ last_hidden: Optional[torch.Tensor], ++ ) -> Dict[str, Any]: ++ """Write one sample's artifacts; return the meta_info result dict. ++ ++ ``aux``/``last_hidden`` are the per-request (L, W) captured tensors, ++ stored with a leading batch dim of 1. On any failure the keys already ++ written are best-effort removed (no partial sample is consumable). ++ """ ++ store_id = str(spec["store_id"]) ++ sample_id = str(spec["sample_id"]) ++ gen = int(spec.get("gen", 1)) ++ replace = bool(spec.get("replace", False)) ++ features: Dict[str, str] = dict(spec.get("features") or {}) ++ ++ written: List[str] = [] ++ result_feats: Dict[str, Dict[str, Any]] = {} ++ ++ def _write(name: str, t: torch.Tensor) -> None: ++ key = self._tkey(store_id, sample_id, gen, name) ++ self._put_tensor(key, t, replace=replace) ++ written.append(key) ++ result_feats[name] = { ++ "shape": list(t.shape), ++ "dtype": _DTYPE_STR.get(t.dtype, str(t.dtype).replace("torch.", "")), ++ } ++ ++ try: ++ aux_name = features.get(_ARTIFACT_AUX) ++ if aux_name is not None: ++ if aux is None: ++ raise RuntimeError( ++ "spec_capture requested 'aux' but no aux hidden states were " ++ "captured — launch the server with --enable-spec-capture " ++ "(and optionally --spec-capture-aux-layer-ids)" ++ ) ++ _write(aux_name, aux.unsqueeze(0)) ++ lh_name = features.get(_ARTIFACT_LAST_HIDDEN) ++ if lh_name is not None: ++ if last_hidden is None: ++ raise RuntimeError( ++ "spec_capture requested 'last_hidden' but the logits " ++ "processor did not return it (is aux capture enabled?)" ++ ) ++ _write(lh_name, last_hidden.unsqueeze(0)) ++ for item in spec.get("passthrough") or []: ++ dtype = _STR_DTYPE.get(str(item.get("dtype", "int64"))) ++ if dtype is None: ++ raise RuntimeError( ++ f"spec_capture passthrough {item.get('name')!r}: " ++ f"unsupported dtype {item.get('dtype')!r}" ++ ) ++ t = torch.tensor(item["data"], dtype=dtype).reshape( ++ [int(d) for d in item["shape"]] ++ ) ++ _write(str(item["name"]), t) ++ except Exception: ++ for key in written: ++ self._remove_quiet(key) ++ raise ++ ++ return { ++ "sample_id": sample_id, ++ "store_id": store_id, ++ "gen": gen, ++ "aux_layer_ids": self.aux_layer_ids, ++ "features": result_feats, ++ } ++ ++ ++_SINK: Optional[SpecCaptureSink] = None ++ ++ ++def maybe_init_sink(server_args) -> None: ++ """Called from Scheduler init on the writer rank when spec capture is on. ++ ++ Connection to Mooncake is lazy (first put), so a capture-enabled server ++ without a reachable Mooncake master still boots and serves normal traffic. ++ """ ++ global _SINK ++ if getattr(server_args, "enable_spec_capture", False) and _SINK is None: ++ _SINK = SpecCaptureSink( ++ aux_layer_ids=getattr(server_args, "spec_capture_aux_layer_ids", None) ++ ) ++ ++ ++def get_sink() -> Optional[SpecCaptureSink]: ++ return _SINK diff --git a/scripts/apply_sglang_spec_capture_patch.sh b/scripts/apply_sglang_spec_capture_patch.sh index cb59afced..f7cdf1222 100755 --- a/scripts/apply_sglang_spec_capture_patch.sh +++ b/scripts/apply_sglang_spec_capture_patch.sh @@ -12,31 +12,70 @@ # when a reverse dry-run proves it matches the current patch byte-for-byte; # anything else fails loudly rather than testing against unknown server code. # -# Usage: scripts/apply_sglang_spec_capture_patch.sh [--reverse] +# Usage: scripts/apply_sglang_spec_capture_patch.sh +# [--target v0.5.14|kimi-k3-f8493a4] [--reverse] set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" -PATCH="$HERE/patches/sglang/v0.5.14/spec-capture.patch" +TARGET="v0.5.14" +REVERSE=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --target) + if [[ $# -lt 2 ]]; then + echo "ERROR: --target requires a value" >&2 + exit 2 + fi + TARGET="$2" + shift 2 + ;; + --reverse) + REVERSE=1 + shift + ;; + *) + echo "ERROR: unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +case "$TARGET" in + v0.5.14) + EXPECTED_VERSION_PREFIX="0.5.14" + ;; + kimi-k3-f8493a4) + # Kimi K3's SGLang fork currently reports a base-package version that + # does not uniquely identify this source revision, so patch --check is + # the authoritative compatibility gate below. + EXPECTED_VERSION_PREFIX="" + ;; + *) + echo "ERROR: unsupported SGLang patch target: $TARGET" >&2 + exit 2 + ;; +esac +PATCH="$HERE/patches/sglang/$TARGET/spec-capture.patch" SGL_PARENT="$(python -c 'import sglang, os; print(os.path.dirname(os.path.dirname(sglang.__file__)))')" SGL_VERSION="$(python -c 'import sglang; print(sglang.__version__)')" APPLIED_COPY="$SGL_PARENT/sglang/.spec_capture_patch.applied" SINK="$SGL_PARENT/sglang/srt/spec_capture_sink.py" -if [[ "$SGL_VERSION" != 0.5.14* ]]; then - echo "WARNING: installed sglang is $SGL_VERSION; the patch targets v0.5.14" >&2 +if [[ -n "$EXPECTED_VERSION_PREFIX" && "$SGL_VERSION" != "$EXPECTED_VERSION_PREFIX"* ]]; then + echo "WARNING: installed sglang is $SGL_VERSION; the patch targets $TARGET" >&2 fi -if [[ "${1:-}" == "--reverse" ]]; then +if [[ "$REVERSE" == 1 ]]; then patch --reverse -p2 --batch -N -d "$SGL_PARENT" < "$PATCH" rm -f "$APPLIED_COPY" - echo "spec-capture patch --reverse at $SGL_PARENT/sglang (sglang $SGL_VERSION)" + echo "spec-capture patch $TARGET --reverse at $SGL_PARENT/sglang (sglang $SGL_VERSION)" exit 0 fi if [[ -f "$APPLIED_COPY" ]]; then if cmp -s "$APPLIED_COPY" "$PATCH"; then - echo "spec-capture patch already applied at $SGL_PARENT/sglang" + echo "spec-capture patch $TARGET already applied at $SGL_PARENT/sglang" exit 0 fi echo "spec-capture patch changed; reversing the recorded version first" @@ -52,7 +91,7 @@ elif [[ -f "$SINK" ]]; then fi if matches; then cp "$PATCH" "$APPLIED_COPY" - echo "spec-capture patch already applied at $SGL_PARENT/sglang (adopted)" + echo "spec-capture patch $TARGET already applied at $SGL_PARENT/sglang (adopted)" exit 0 fi echo "ERROR: $SGL_PARENT/sglang carries an unknown spec-capture patch state" >&2 @@ -62,4 +101,4 @@ fi patch -p2 --batch -N -d "$SGL_PARENT" < "$PATCH" cp "$PATCH" "$APPLIED_COPY" -echo "spec-capture patch applied at $SGL_PARENT/sglang (sglang $SGL_VERSION)" +echo "spec-capture patch $TARGET applied at $SGL_PARENT/sglang (sglang $SGL_VERSION)" diff --git a/specforge/algorithms/dspark/providers.py b/specforge/algorithms/dspark/providers.py index 153575d5d..b115f8929 100644 --- a/specforge/algorithms/dspark/providers.py +++ b/specforge/algorithms/dspark/providers.py @@ -169,7 +169,7 @@ def algorithm_providers() -> AlgorithmProviders: modality="text", normalizer_id=DSPARK_NORMALIZER_ID, capture_layout=OfflineCaptureLayout( - capture_method="dflash", + capture_method="dspark", aux_feature="hidden_states", last_hidden_feature="target_last_hidden_states", passthrough=( @@ -185,7 +185,7 @@ def algorithm_providers() -> AlgorithmProviders: server_streaming=( ServerStreamingProvider( modality="text", - capture_method="dflash", + capture_method="dspark", target_representation="hidden_state", layout=ServerCaptureLayout( aux_feature="hidden_states", diff --git a/specforge/inference/sglang_patch_inventory.md b/specforge/inference/sglang_patch_inventory.md index 5ecd2e08f..83029b2c5 100644 --- a/specforge/inference/sglang_patch_inventory.md +++ b/specforge/inference/sglang_patch_inventory.md @@ -1,13 +1,19 @@ # SGLang patch inventory and supported version -SpecForge pins `sglang==0.5.14`. The online patch is also kept compatible with -SGLang's public `inkling-support` layout. There are two deliberately separate -SGLang integration surfaces. +SpecForge pins `sglang==0.5.14` by default. The online patch is also kept +compatible with SGLang's public `inkling-support` layout, and a separately +versioned patch supports the Kimi K3 SGLang fork at revision `f8493a4`. There +are two deliberately separate SGLang integration surfaces. ## Online: external spec-capture server -Online training uses -[`patches/sglang/v0.5.14/spec-capture.patch`](../../patches/sglang/v0.5.14/spec-capture.patch). +Online training uses one of these source-specific patches: + +| Target | Patch | Capture methods | +|---|---|---| +| SGLang v0.5.14 / `inkling-support` | [`patches/sglang/v0.5.14/spec-capture.patch`](../../patches/sglang/v0.5.14/spec-capture.patch) | EAGLE3, DFlash | +| Kimi K3 SGLang `f8493a4` | [`patches/sglang/kimi-k3-f8493a4/spec-capture.patch`](../../patches/sglang/kimi-k3-f8493a4/spec-capture.patch) | EAGLE3, DFlash, DSpark | + The patch adds `--enable-spec-capture` and a server-side sink that: 1. captures requested auxiliary and final hidden states during prefill; @@ -28,8 +34,15 @@ training sample executes a full prefill even when radix cache support is present. Capture launch configs therefore leave radix cache enabled, including for hybrid targets that require the unified radix tree. -Apply the patch with `scripts/apply_sglang_spec_capture_patch.sh`. The -server-capture unit and GPU gates must pass before updating the SGLang pin. +Apply the default patch with `scripts/apply_sglang_spec_capture_patch.sh`, or +the K3 patch with +`scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-f8493a4`. +The K3 patch routes `--spec-capture-method dspark` to the model's dedicated +`set_dspark_layers_to_capture` hook. It also keeps 64K capture correct by using +64-bit Triton pointer arithmetic, scale-stable residual scoring, and a generic +Marlin reduction fallback when the token dimension exceeds CUDA grid.y's +65,535 limit. The server-capture unit and GPU gates must pass before updating +either supported source revision. ## Offline: dedicated local capture @@ -40,13 +53,13 @@ version-pinned APIs required for offline EAGLE3 preprocessing: | Dependency | Upgrade risk | |---|---| | `CaptureHiddenMode.FULL` and logits-processor replacement | hidden-state output fields or pruning behavior may change | -| `set_eagle3_layers_to_capture` / `set_dflash_layers_to_capture` | strategy-specific layer-selection APIs may move | +| `set_eagle3_layers_to_capture` / `set_dflash_layers_to_capture` / `set_dspark_layers_to_capture` | strategy-specific layer-selection APIs may move | | `ScheduleBatch`, `ForwardBatch`, and `ModelRunner` construction | constructor and memory-pool setup may change | | splitting captured states by request input length | token packing conventions may change | | DP-attention/model-parallel initialization patches | distributed group signatures may change | -This package computes no logits and supports text EAGLE3 and DFlash-family -state capture needed by the preprocessing script. It does not provide +This package computes no logits and supports text EAGLE3, DFlash, Domino, and +K3 DSpark state capture needed by the preprocessing script. It does not provide HF/custom backends, VLM capture, online rollout, or a general target-engine factory. diff --git a/specforge/offline_capture/sglang_backend/capture.py b/specforge/offline_capture/sglang_backend/capture.py index 5721ee576..306bab3fe 100644 --- a/specforge/offline_capture/sglang_backend/capture.py +++ b/specforge/offline_capture/sglang_backend/capture.py @@ -97,10 +97,12 @@ def set_capture_layers( setter_name = { "eagle3": "set_eagle3_layers_to_capture", "dflash": "set_dflash_layers_to_capture", + "dspark": "set_dspark_layers_to_capture", }.get(capture_method) if setter_name is None: raise ValueError( - "offline SGLang capture method must be 'eagle3' or 'dflash', " + "offline SGLang capture method must be 'eagle3', 'dflash', or " + "'dspark', " f"got {capture_method!r}" ) setter = getattr(self.model_runner.model, setter_name, None) diff --git a/tests/test_algorithms/test_builtin_parity.py b/tests/test_algorithms/test_builtin_parity.py index a4d62d356..e7559faad 100644 --- a/tests/test_algorithms/test_builtin_parity.py +++ b/tests/test_algorithms/test_builtin_parity.py @@ -144,6 +144,13 @@ def test_peagle_reuses_the_eagle_server_contract_explicitly(self): self.assertEqual("hidden_state", peagle_stream.target_representation) self.assertTrue(peagle.step.uses_external_target_head) + def test_dspark_uses_the_dedicated_server_capture_method(self): + stream = self.registry.resolve("dspark").providers.server_streaming_for( + "text" + ) + + self.assertEqual("dspark", stream.capture_method) + def test_step_factories_preserve_concrete_strategy_types(self): expected = { "eagle3": "Eagle3TrainStrategy", diff --git a/tests/test_algorithms/test_offline_capture_layout.py b/tests/test_algorithms/test_offline_capture_layout.py index 3acc1087d..82f7fced8 100644 --- a/tests/test_algorithms/test_offline_capture_layout.py +++ b/tests/test_algorithms/test_offline_capture_layout.py @@ -40,6 +40,12 @@ def test_builtin_offline_layouts_materialize_exact_storage_schemas(self): "target_last_hidden_states": "last_hidden_states", }, } + expected_capture_methods = { + "eagle3": "eagle3", + "dflash": "dflash", + "domino": "dflash", + "dspark": "dspark", + } sources = { "input_ids": torch.tensor([1, 2, 3]), "loss_mask": torch.tensor([1, 1, 0]), @@ -54,7 +60,7 @@ def test_builtin_offline_layouts_materialize_exact_storage_schemas(self): record = provider.capture_layout.materialize(sources) self.assertEqual( - "eagle3" if strategy == "eagle3" else "dflash", + expected_capture_methods[strategy], provider.capture_layout.capture_method, ) @@ -134,16 +140,19 @@ def test_local_capture_forwards_the_algorithm_capture_method(self): backend = mock.Mock() capture = OfflineSGLangCapture(backend) - capture.set_capture_layers( - [1, 9, 17, 25, 33], - capture_method="dflash", - ) + for capture_method in ("dflash", "dspark"): + with self.subTest(capture_method=capture_method): + backend.reset_mock() + capture.set_capture_layers( + [1, 9, 17, 25, 33], + capture_method=capture_method, + ) - self.assertEqual("dflash", capture.capture_method) - backend.set_capture_layers.assert_called_once_with( - [1, 9, 17, 25, 33], - capture_method="dflash", - ) + self.assertEqual(capture_method, capture.capture_method) + backend.set_capture_layers.assert_called_once_with( + [1, 9, 17, 25, 33], + capture_method=capture_method, + ) if __name__ == "__main__": diff --git a/tests/test_runtime/test_disaggregated_model_loading.py b/tests/test_runtime/test_disaggregated_model_loading.py index 8b98c893f..2593fcb02 100644 --- a/tests/test_runtime/test_disaggregated_model_loading.py +++ b/tests/test_runtime/test_disaggregated_model_loading.py @@ -135,6 +135,37 @@ def test_domino_uses_the_dflash_server_capture_method(self): self.assertEqual(contract.method, "dflash") self.assertEqual(contract.aux_layer_ids, (3, 7)) + def test_dspark_uses_its_dedicated_server_capture_method(self): + resolved = resolve_run( + _config(strategy="dspark", draft_model_config="dspark-draft") + ) + draft_payload = { + "architectures": ["DSparkDraftModel"], + "vocab_size": 128, + "num_target_layers": 2, + "dflash_config": { + "projector_type": "dspark", + "target_layer_ids": [3, 7], + }, + } + with ( + mock.patch( + "transformers.AutoConfig.from_pretrained", + return_value=SimpleNamespace(hidden_size=64, vocab_size=128), + ), + mock.patch( + "specforge.training.model_loading.draft_config_dict", + return_value=draft_payload, + ), + ): + contract = resolve_server_capture_contract( + resolved.config, + algorithm=resolved.algorithm, + ) + + self.assertEqual(contract.method, "dspark") + self.assertEqual(contract.aux_layer_ids, (3, 7)) + if __name__ == "__main__": unittest.main(verbosity=2) From 967fd9ecbe863aa52bae343db5972b6a590a4e20 Mon Sep 17 00:00:00 2001 From: maocheng Date: Fri, 31 Jul 2026 21:26:23 -0700 Subject: [PATCH 06/88] Add Kimi K3 V1C disaggregated recipe --- configs/kimi-k3-dspark-v1c.json | 53 ++++++++ .../kimi-k3-dspark-v1c-disaggregated.md | 121 ++++++++++++++++++ examples/configs/README.md | 6 + .../kimi-k3-dspark-v1c-disaggregated.yaml | 91 +++++++++++++ tests/test_config/test_launch_topology.py | 23 +++- .../test_unified_feature_reachability.py | 2 +- .../test_runtime/test_package_architecture.py | 1 + 7 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 configs/kimi-k3-dspark-v1c.json create mode 100644 docs/recipes/kimi-k3-dspark-v1c-disaggregated.md create mode 100644 examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml diff --git a/configs/kimi-k3-dspark-v1c.json b/configs/kimi-k3-dspark-v1c.json new file mode 100644 index 000000000..d8278d87f --- /dev/null +++ b/configs/kimi-k3-dspark-v1c.json @@ -0,0 +1,53 @@ +{ + "architectures": ["DSparkDraftModel"], + "attention_bias": false, + "attention_dropout": 0.0, + "auto_map": {"AutoModel": "dspark.DSparkDraftModel"}, + "block_size": 7, + "bos_token_id": 163584, + "dflash_config": { + "attention_mode": "gqa", + "confidence_head_alpha": 1.0, + "confidence_head_with_markov": true, + "enable_confidence_head": true, + "markov_head_type": "vanilla", + "markov_rank": 256, + "mask_token_id": 163824, + "projector_type": "dspark", + "target_layer_ids": [7, 23, 51, 67, 83] + }, + "dtype": "bfloat16", + "eos_token_id": 163586, + "head_dim": 64, + "hidden_act": "silu", + "hidden_size": 7168, + "initializer_range": 0.02, + "intermediate_size": 14336, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 65536, + "max_window_layers": 5, + "model_type": "qwen3", + "num_attention_heads": 64, + "num_hidden_layers": 5, + "num_key_value_heads": 16, + "num_target_layers": 93, + "pad_token_id": 163839, + "rms_norm_eps": 0.00001, + "rope_parameters": { + "factor": 16.0, + "original_max_position_embeddings": 4096, + "rope_theta": 10000.0, + "rope_type": "yarn" + }, + "sliding_window": null, + "tie_word_embeddings": false, + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 163840 +} diff --git a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md new file mode 100644 index 000000000..aa939b64f --- /dev/null +++ b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md @@ -0,0 +1,121 @@ +# Kimi K3 V1C DSpark disaggregated reproduction + +This recipe migrates the prior four-node colocated Kimi K3 V1C continual run +to one TP8 capture node and one four-rank trainer node. It preserves the draft +architecture, weights-only warm start, regenerated agentic prompt order, +effective global batch, constant learning rate, and DSpark loss weights. + +## Required source revisions + +- SpecForge with configurable LR scheduling, an independent online prompt + seed, and dedicated DSpark capture support. +- Kimi K3 SGLang revision `f8493a43a6a30d2a1cad6b0034e2c1b362d920d5`. +- The K3 SGLang tree patched with: + + ```bash + scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-f8493a4 + ``` + +The patch makes `--spec-capture-method dspark` call K3's +`set_dspark_layers_to_capture` hook. The generic DFlash capture method is not +equivalent for K3. The same versioned patch carries the three required 64K +correctness guards: 64-bit Triton token offsets, scale-stable residual scoring, +and the Marlin grid.y fallback above 65,535 tokens. + +## Artifacts + +The checked-in recipe uses the paths already provisioned on the K3 RunPod +pool. Other deployments should override them without editing the recipe: + +- target revision `cdd2e49a2c1cf8d4713b513955e415ed75405a72`; +- the weights-only V1C `epoch_0_step_0` draft checkpoint; +- the 462-row regenerated dataset whose SHA-256 is + `6d50e6bb9ee59095eed91bfba035081efef9fea43bece9ad5dd01c6648a8ef24`. + +Do not put Hugging Face or W&B credentials in YAML. Supply `HF_TOKEN` and +`WANDB_API_KEY` through protected node-local files or the process environment. + +## Capture node + +Start Mooncake with at least a 1 TiB global segment, then start the patched K3 +server. Replace `CAPTURE_IP` with the routable address used by both nodes. + +```bash +export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MOONCAKE_GLOBAL_SEGMENT_SIZE=1099511627776 +export MOONCAKE_LOCAL_BUFFER_SIZE=1073741824 +mooncake_master \ + --enable_http_metadata_server=true \ + --http_metadata_server_host=0.0.0.0 \ + --rpc_port=35551 \ + --http_metadata_server_port=35880 \ + --metrics_port=35903 +``` + +In a second process: + +```bash +export MOONCAKE_MASTER_SERVER_ADDR="$CAPTURE_IP:35551" +export MOONCAKE_METADATA_SERVER="http://$CAPTURE_IP:35880/metadata" +export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MOONCAKE_PROTOCOL=tcp +CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m sglang.launch_server \ + --host 0.0.0.0 \ + --port 30000 \ + --model-path /workspace/models/Kimi-K3-cdd2e49a \ + --trust-remote-code \ + --skip-tokenizer-init \ + --tp-size 8 \ + --mem-fraction-static 0.76 \ + --context-length 66048 \ + --max-running-requests 1 \ + --max-total-tokens 66048 \ + --attention-backend trtllm_mla \ + --moe-runner-backend marlin \ + --mamba-radix-cache-strategy extra_buffer \ + --max-mamba-cache-size 5 \ + --chunked-prefill-size -1 \ + --enable-spec-capture \ + --spec-capture-method dspark \ + --spec-capture-aux-layer-ids 7 23 51 67 83 +``` + +## Trainer node + +Resolve `capture-node` through DNS or override the three endpoint fields with +the capture node's IP. The default CLI role starts one CPU producer and a +four-rank FSDP consumer on the same trainer host. + +```bash +export MOONCAKE_LOCAL_HOSTNAME="$TRAINER_IP" +export WANDB_API_KEY="$(< /protected/path/wandb-api-key)" +export WANDB_ENTITY=your-entity +CUDA_VISIBLE_DEVICES=0,1,2,3 specforge train \ + -c examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml \ + --role both \ + "deployment.disaggregated.server_urls=[\"http://$CAPTURE_IP:30000\"]" \ + "deployment.disaggregated.mooncake_metadata_server=http://$CAPTURE_IP:35880/metadata" \ + "deployment.disaggregated.mooncake_master_server_addr=$CAPTURE_IP:35551" +``` + +For a one-update smoke run, additionally override the pre-tokenized four-row +fixture and shrink the optimizer quantum: + +```bash +specforge train \ + -c examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml \ + --role both \ + data.train_data_path= \ + data.prompts_path=/workspace/k3_dspark/k3_specforge/cache/kimi-k3-agentic-regen-9a6ea2c7-v1c-full139264/longest-smoke-pretokenized-4rows-65536.jsonl \ + training.num_epochs=1 \ + training.max_steps=1 \ + training.accumulation_steps=1 \ + tracking.report_to=none \ + runtime.in_flight_high_watermark=4 \ + runtime.in_flight_low_watermark=2 +``` + +Validate in order: config plan, patch dry-run, server health, one captured +sample's tensor shapes/dtypes, one finite optimizer update and checkpoint, then +the full run. A smoke pass does not establish numerical parity; compare the +full run's loss/accuracy/tau trajectory and final checkpoint hashes separately. diff --git a/examples/configs/README.md b/examples/configs/README.md index c6caec046..2ca3d48c2 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -50,6 +50,12 @@ two patched SGLang capture servers, and the trainer GPU allocation; the same Disaggregated recipes without `managed_local` keep Mooncake and SGLang external for scheduler- or service-managed deployments. +The `kimi-k3-dspark-v1c-disaggregated.yaml` recipe is the external-service +two-node migration of the 64K Kimi K3 V1C continual run. Its dedicated +[runbook](../../docs/recipes/kimi-k3-dspark-v1c-disaggregated.md) pins the K3 +SGLang revision and patch target, preserves the old effective global batch and +prompt order, and documents the TP8 capture plus four-rank trainer topology. + Before running a recipe, update model/data paths and create any referenced offline feature or vocabulary-mapping artifacts. Managed-local recipes intentionally record their GPU allocation and loopback services. External diff --git a/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml b/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml new file mode 100644 index 000000000..39762e205 --- /dev/null +++ b/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml @@ -0,0 +1,91 @@ +model: + target_model_path: /workspace/models/Kimi-K3-cdd2e49a + draft_model_config: configs/kimi-k3-dspark-v1c.json + # Weights-only initialization: optimizer, scheduler, counters, and RNG start + # fresh for the regenerated agentic data. + draft_checkpoint_path: /workspace/k3_dspark/k3_specforge-replay/outputs/kimi-k3-dspark-v1c-cdd2e49a-agentic-65536-a512-b7-constantlr-20260729/epoch_0_step_0 + target_backend: sglang + trust_remote_code: true + embedding_key: language_model.model.embed_tokens.weight + lm_head_key: language_model.lm_head.weight + mask_token_id: 163824 + torch_dtype: bfloat16 + sglang_attention_backend: trtllm_mla + sglang_mem_fraction_static: 0.76 + sglang_context_length: 66048 + sglang_max_running_requests: 1 + sglang_max_total_tokens: 66048 + sglang_moe_runner_backend: marlin + sglang_mamba_radix_cache_strategy: extra_buffer + sglang_max_mamba_cache_size: 5 + +data: + train_data_path: /workspace/k3_dspark/k3_specforge/cache/kimi-k3-agentic-regen-9a6ea2c7-v1c-full139264/kimi-k3-agentic-regen-v1c-full139264.jsonl + max_length: 65536 + chat_template: kimi-k3-thinking + cache_dir: /workspace/k3_dspark/cache + build_dataset_num_proc: 64 + dataloader_num_workers: 0 + +training: + strategy: dspark + num_epochs: 10 + # Four consumer ranks x one sequence x 32 microbatches preserves the old + # effective global batch of 128 sequences. + batch_size: 1 + accumulation_steps: 32 + learning_rate: 0.000050959167111070076 + lr_scheduler: constant + warmup_ratio: 0 + max_grad_norm: 1 + attention_backend: flex_attention + num_anchors: 512 + loss_decay_gamma: 4.0 + objective_chunk_blocks: 128 + dspark_ce_loss_alpha: 0.1 + dspark_l1_loss_alpha: 0.9 + dspark_confidence_head_alpha: 1.0 + save_interval: 8 + log_interval: 10 + dist_timeout: 30 + seed: 42 + prompt_seed: 1 + +tracking: + report_to: wandb + wandb_project: specforge-dspark + wandb_name: kimi-k3-dspark-v1c-specforge-disaggregated + wandb_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/wandb + +runtime: + producer_lease: 1 + producer_concurrency: 1 + # One 64K K3 sample is roughly 5.25 GiB of BF16 DSpark features. A complete + # optimizer quantum is 128 samples, so the server segment must exceed 672 GiB. + in_flight_high_watermark: 128 + in_flight_low_watermark: 96 + resident_high_watermark_bytes: 858993459200 + resident_low_watermark_bytes: 697932185600 + feature_store_max_resident_bytes: 966367641600 + +run_id: kimi-k3-dspark-v1c-specforge-disaggregated +output_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/output + +deployment: + mode: disaggregated + trainer: + nnodes: 1 + nproc_per_node: 4 + disaggregated: + control_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/control + consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/consumer-state + backend: mooncake + store_id: kimi-k3-dspark-v1c-specforge-disaggregated + server_urls: + - http://capture-node:30000 + mooncake_metadata_server: http://capture-node:35880/metadata + mooncake_master_server_addr: capture-node:35551 + mooncake_protocol: tcp + client_buffer_size: 1073741824 + idle_timeout_s: 7200 + peer_wait_timeout_s: 7200 diff --git a/tests/test_config/test_launch_topology.py b/tests/test_config/test_launch_topology.py index b3554f88e..13b3d9b43 100644 --- a/tests/test_config/test_launch_topology.py +++ b/tests/test_config/test_launch_topology.py @@ -20,6 +20,7 @@ "gpt-oss-20b-eagle3-online.yaml": 8, "lfm2.5-1.2b-instruct-dflash-online.yaml": 8, "inkling-dspark-disaggregated.yaml": 1, + "kimi-k3-dspark-v1c-disaggregated.yaml": 4, "ling-flash-2.0-eagle3-offline.yaml": 8, "ling-flash-2.0-eagle3-online.yaml": 8, "llama3.1-8b-eagle3-offline.yaml": 1, @@ -312,7 +313,7 @@ def _recipes() -> dict[str, Path]: class ExampleLaunchTopologyTest(unittest.TestCase): def test_every_recipe_has_the_explicit_golden_topology(self): recipes = _recipes() - self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 63) + self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 64) self.assertEqual(set(recipes), set(EXPECTED_NPROC_PER_NODE)) for filename, nproc_per_node in EXPECTED_NPROC_PER_NODE.items(): @@ -416,6 +417,26 @@ def test_migrated_dspark_recipes_match_source_training_contract(self): self.assertEqual(qwen4b.training.loss_decay_gamma, 4.0) self.assertEqual(qwen4b.training.objective_chunk_blocks, 128) + kimi = Config.from_file( + str(EXAMPLE_CONFIG_DIR / "kimi-k3-dspark-v1c-disaggregated.yaml") + ) + kimi_topology = kimi.deployment.trainer + self.assertEqual( + kimi_topology.nnodes + * kimi_topology.nproc_per_node + * kimi.training.batch_size + * kimi.training.accumulation_steps, + 128, + ) + self.assertEqual(kimi.training.lr_scheduler, "constant") + self.assertEqual(kimi.training.prompt_seed, 1) + self.assertAlmostEqual( + kimi.training.learning_rate, + 0.000050959167111070076, + ) + self.assertEqual(kimi.data.max_length, 65536) + self.assertEqual(kimi.training.num_anchors, 512) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_config/test_unified_feature_reachability.py b/tests/test_config/test_unified_feature_reachability.py index f9abc327a..db9dbb48c 100644 --- a/tests/test_config/test_unified_feature_reachability.py +++ b/tests/test_config/test_unified_feature_reachability.py @@ -146,7 +146,7 @@ def test_all_example_configs_validate_through_the_typed_entry(self): for path in EXAMPLE_CONFIG_DIR.glob("*.yaml") if not path.name.startswith(".") ) - self.assertEqual(len(paths), 63) + self.assertEqual(len(paths), 64) resolved_runs = { path.name: resolve_run(Config.from_file(str(path))) for path in paths diff --git a/tests/test_runtime/test_package_architecture.py b/tests/test_runtime/test_package_architecture.py index 82e4e3b71..e421927aa 100644 --- a/tests/test_runtime/test_package_architecture.py +++ b/tests/test_runtime/test_package_architecture.py @@ -713,6 +713,7 @@ def test_dspark_configs_are_qwen3_gqa_only(self): { "glm-5.2-dspark.json", "inkling-dspark.json", + "kimi-k3-dspark-v1c.json", "qwen3-4b-dspark.json", "qwen3-8b-dspark.json", "qwen3.6-27b-dspark.json", From 9ed2462e9f6f30719ee7e197b56e82f08462c205 Mon Sep 17 00:00:00 2001 From: maocheng23 Date: Fri, 31 Jul 2026 22:38:25 -0700 Subject: [PATCH 07/88] Support Inkling managed capture --- examples/configs/README.md | 1 + patches/sglang/v0.5.14/spec-capture.patch | 28 +++++++++++++++++-- specforge/config/schema.py | 3 ++ specforge/inference/sglang_patch_inventory.md | 12 ++++++-- specforge/launch_plan.py | 1 - tests/test_runtime/test_launch_plan.py | 15 +++++++++- 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/examples/configs/README.md b/examples/configs/README.md index 451e9ac6c..e31c93364 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -159,6 +159,7 @@ should make their training strategy and topology explicit. | `model.tokenizer_pad_token_id` | `null` | Explicit non-negative tokenizer pad ID. Use it for released tokenizers that omit padding metadata. | | `model.sglang_attention_backend` | `flashinfer` | SGLang attention implementation for an in-process or managed capture server. | | `model.sglang_mem_fraction_static` | `0.4` | SGLang static-memory fraction in `(0, 1]`; inherited by managed capture servers unless they override it. | +| `model.sglang_disable_radix_cache` | `true` | Preserve the historical managed-capture behavior. Set `false` for hybrid targets such as Inkling that require the radix tree. Unique per-attempt cache namespaces still force complete capture prefills. | | `model.sglang_context_length` | `null` | Positive explicit context limit. Managed capture requires at least `data.max_length + 7`; omitting it derives that value. | | `model.sglang_enable_nccl_nvls` | `false` | Pass the matching SGLang NCCL NVLS optimization flag. | | `model.sglang_enable_symm_mem` | `false` | Pass the matching SGLang symmetric-memory flag. | diff --git a/patches/sglang/v0.5.14/spec-capture.patch b/patches/sglang/v0.5.14/spec-capture.patch index 33a674213..434c4c34d 100644 --- a/patches/sglang/v0.5.14/spec-capture.patch +++ b/patches/sglang/v0.5.14/spec-capture.patch @@ -12,7 +12,7 @@ index a99d25267..f5d0b35e7 100644 ## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler # he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature. -@@ -361,6 +364,16 @@ class LogitsProcessor(nn.Module): +@@ -361,6 +364,30 @@ class LogitsProcessor(nn.Module): sample_indices, logits_metadata, ) @@ -26,6 +26,20 @@ index a99d25267..f5d0b35e7 100644 + ) + else None + ) ++ # muP targets pass LM-head-scaled hidden states into LogitsProcessor, ++ # while SpecForge folds the same multiplier into its frozen target ++ # head. Restore the pre-head-scale representation before capture so ++ # the multiplier is applied exactly once when training recomputes logits. ++ logits_mup_width_multiplier = getattr( ++ self.config, "logits_mup_width_multiplier", None ++ ) ++ if ( ++ last_hidden_states_to_store is not None ++ and logits_mup_width_multiplier ++ ): ++ last_hidden_states_to_store = ( ++ last_hidden_states_to_store * float(logits_mup_width_multiplier) ++ ) del hidden_states if not logits_metadata.extend_return_logprob: @@ -281,7 +295,7 @@ diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/sr index 1cff5c983..a5935fa3c 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py -@@ -518,3 +518,19 @@ class ModelRunner(ModelRunnerKVCacheMixin): +@@ -518,3 +518,29 @@ class ModelRunner(ModelRunnerKVCacheMixin): + if server_args.enable_spec_capture and not self.is_draft_worker: + # Aux capture without a draft worker, routed to the strategy's own + # capture method (they wire different submodules — e.g. VL models @@ -289,6 +303,11 @@ index 1cff5c983..a5935fa3c 100644 + if getattr(server_args, "spec_capture_method", "eagle3") == "dflash": + self.dflash_use_aux_hidden_state = True + self.dflash_target_layer_ids = server_args.spec_capture_aux_layer_ids ++ if hasattr(self, "spec_aux_config"): ++ self.spec_aux_config.dflash_use_aux_hidden_state = True ++ self.spec_aux_config.dflash_target_layer_ids = ( ++ server_args.spec_capture_aux_layer_ids ++ ) + self.dflash_family_use_aux_hidden_state = True + self.dflash_family_target_layer_ids = ( + server_args.spec_capture_aux_layer_ids @@ -298,6 +317,11 @@ index 1cff5c983..a5935fa3c 100644 + self.eagle_aux_hidden_state_layer_ids = ( + server_args.spec_capture_aux_layer_ids + ) ++ if hasattr(self, "spec_aux_config"): ++ self.spec_aux_config.eagle_use_aux_hidden_state = True ++ self.spec_aux_config.eagle_aux_hidden_state_layer_ids = ( ++ server_args.spec_capture_aux_layer_ids ++ ) # Apply the rank zero filter to logger if server_args.show_time_cost: enable_show_time_cost() diff --git a/specforge/config/schema.py b/specforge/config/schema.py index cabaa975d..767e0e394 100644 --- a/specforge/config/schema.py +++ b/specforge/config/schema.py @@ -80,6 +80,9 @@ class ModelConfig(StrictConfigModel): #: SGLang target-engine tuning. Ignored by hf/custom backends. sglang_attention_backend: str = "flashinfer" sglang_mem_fraction_static: float = Field(default=0.4, gt=0.0, le=1.0) + #: Keep the historical managed-local behavior by default. Hybrid targets + #: such as Inkling require the radix tree and can opt back in explicitly. + sglang_disable_radix_cache: bool = True sglang_context_length: Optional[int] = Field(default=None, gt=0) sglang_enable_nccl_nvls: bool = False sglang_enable_symm_mem: bool = False diff --git a/specforge/inference/sglang_patch_inventory.md b/specforge/inference/sglang_patch_inventory.md index 5ecd2e08f..95cbe916e 100644 --- a/specforge/inference/sglang_patch_inventory.md +++ b/specforge/inference/sglang_patch_inventory.md @@ -23,10 +23,16 @@ inputs) to training feature names. No trainer or producer process imports SGLang model-runner internals or loads a target model. The same patch is dry-run validated against the v0.5.14 tag and the public -`inkling-support` branch. Capture requests carry a unique `extra_key`, so every +Inkling integration. Capture requests carry a unique `extra_key`, so every training sample executes a full prefill even when radix cache support is -present. Capture launch configs therefore leave radix cache enabled, including -for hybrid targets that require the unified radix tree. +present. Managed-local launch preserves the historical disabled-cache default; +hybrid targets that require the unified radix tree set +`model.sglang_disable_radix_cache: false`. + +For targets that declare `logits_mup_width_multiplier`, the SGLang model passes +an LM-head-scaled hidden state into the logits processor. The capture patch +restores the pre-head-scale post-norm representation because SpecForge folds +the same multiplier into the frozen target head used during training. Apply the patch with `scripts/apply_sglang_spec_capture_patch.sh`. The server-capture unit and GPU gates must pass before updating the SGLang pin. diff --git a/specforge/launch_plan.py b/specforge/launch_plan.py index 293a91b01..8fbacff78 100644 --- a/specforge/launch_plan.py +++ b/specforge/launch_plan.py @@ -451,7 +451,6 @@ def _managed_local_services( str(server.tp_size), "--chunked-prefill-size", "-1", - "--disable-radix-cache", "--enable-spec-capture", "--spec-capture-method", contract.method, diff --git a/tests/test_runtime/test_launch_plan.py b/tests/test_runtime/test_launch_plan.py index 226410221..77a36b35d 100644 --- a/tests/test_runtime/test_launch_plan.py +++ b/tests/test_runtime/test_launch_plan.py @@ -539,7 +539,7 @@ def test_managed_local_rejects_external_and_nonlocal_modes(self): with self.assertRaisesRegex(ValidationError, message): Config.model_validate(raw) - def test_managed_local_accepts_minimum_context_and_disables_radix_cache(self): + def test_managed_local_accepts_minimum_context_and_configures_radix_cache(self): with tempfile.TemporaryDirectory() as root: cfg = _managed_config(os.path.join(root, "attempt")) raw = cfg.model_dump() @@ -556,6 +556,19 @@ def test_managed_local_accepts_minimum_context_and_disables_radix_cache(self): self.assertEqual(argv[argv.index("--context-length") + 1], "135") self.assertIn("--disable-radix-cache", argv) + with tempfile.TemporaryDirectory() as root: + cfg = _managed_config(os.path.join(root, "attempt")) + raw = cfg.model_dump() + raw["model"]["sglang_disable_radix_cache"] = False + validated = Config.model_validate(raw) + with mock.patch( + "specforge.training.capture_contract.resolve_server_capture_contract", + return_value=CAPTURE_CONTRACT, + ): + plan = build_launch_plan(validated, config_path="run.yaml", env={}) + + self.assertNotIn("--disable-radix-cache", plan.services[1].command.argv) + def test_managed_local_plan_owns_mooncake_and_multiple_capture_servers(self): servers = [ { From 623c07ecbb31e619aca85e570a3ee2d14c93a862 Mon Sep 17 00:00:00 2001 From: maocheng23 Date: Fri, 31 Jul 2026 23:11:58 -0700 Subject: [PATCH 08/88] Add Inkling two-node recipe --- .../configs/inkling-dspark-disaggregated.yaml | 14 ++++ examples/disagg/README.md | 21 ++++++ .../disagg/run_inkling_dspark_disagg_2node.sh | 43 ++++++++++++ .../run_qwen3_8b_dflash_disagg_2node.sh | 70 ++++++++++++++----- patches/sglang/v0.5.14/spec-capture.patch | 15 ++-- specforge/inference/sglang_patch_inventory.md | 4 +- .../test_runtime/test_package_architecture.py | 1 + tests/test_scripts/test_disagg_launchers.py | 51 ++++++++++++++ 8 files changed, 192 insertions(+), 27 deletions(-) create mode 100755 examples/disagg/run_inkling_dspark_disagg_2node.sh diff --git a/examples/configs/inkling-dspark-disaggregated.yaml b/examples/configs/inkling-dspark-disaggregated.yaml index afadeac74..46e18e8dd 100644 --- a/examples/configs/inkling-dspark-disaggregated.yaml +++ b/examples/configs/inkling-dspark-disaggregated.yaml @@ -2,9 +2,22 @@ model: target_model_path: thinkingmachines/Inkling draft_model_config: configs/inkling-dspark.json target_backend: sglang + trust_remote_code: true tokenizer_pad_token_id: 200006 embedding_key: model.llm.embed.weight lm_head_key: model.llm.unembed.weight + # Inkling's hybrid cache path requires the unified radix tree. These values + # match the SGLang #31847 configuration validated by the two-node launcher. + sglang_attention_backend: fa4 + sglang_mem_fraction_static: 0.85 + sglang_disable_radix_cache: false + sglang_context_length: 4103 + sglang_moe_runner_backend: flashinfer_trtllm_routed + sglang_page_size: 128 + sglang_quantization: modelopt_fp4 + sglang_mamba_radix_cache_strategy: extra_buffer + sglang_max_mamba_cache_size: 64 + sglang_swa_full_tokens_ratio: 0.2 data: train_data_path: ./cache/dataset/inkling_dspark_train.jsonl max_length: 4096 @@ -20,6 +33,7 @@ training: learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1.0 + attention_backend: eager num_anchors: 512 loss_decay_gamma: 4.0 objective_chunk_blocks: 128 diff --git a/examples/disagg/README.md b/examples/disagg/README.md index 6be6e0841..2f7900527 100644 --- a/examples/disagg/README.md +++ b/examples/disagg/README.md @@ -90,6 +90,27 @@ The consumer's SQLite/WAL and rank inboxes default to the trainer-node-local `DISAGG_CONSUMER_STATE_DIR` or `LOCAL_SCRATCH` when `/tmp` is unsuitable. Node-local consumer state currently supports one trainer node only. +The Inkling DSpark variant uses the same two-node lifecycle with the target +settings validated against SGLang +[#31847](https://github.com/sgl-project/sglang/pull/31847): + +```bash +export DISAGG_STORE_ID=inkling-two-node-attempt-001 +export DISAGG_RUN_ROOT=/shared/specforge/$DISAGG_STORE_ID + +rcli exec --per-node \ + 'bash examples/disagg/run_inkling_dspark_disagg_2node.sh' +``` + +Rank 0 uses four GPUs for TP4 ModelOpt-FP4 capture; rank 1 defaults to four +FSDP trainer ranks. Override `TARGET_MODEL_PATH`, `SERVER_GPUS`, +`TRAINER_GPUS`, or `TRAINER_NPROC` for another allocation. The launcher keeps +the unified radix tree enabled and does not pass `--disable-radix-cache`. +Until #31847 is available in a supported SGLang release, install that PR's +checkout into both nodes' environment. The wrapper applies SpecForge's +checked-in capture patch before starting the server; the patch is dry-run +validated against both v0.5.14 and #31847 commit `b7252cc`. + ## External and managed-local services By default, online capture requires an already-running Mooncake deployment and diff --git a/examples/disagg/run_inkling_dspark_disagg_2node.sh b/examples/disagg/run_inkling_dspark_disagg_2node.sh new file mode 100755 index 000000000..f34ad8703 --- /dev/null +++ b/examples/disagg/run_inkling_dspark_disagg_2node.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Two-node Inkling DSpark recipe: +# rank 0: Mooncake + SGLang #31847 TP4 capture + CPU producer +# rank 1: four-rank FSDP consumer/trainer +# +# Launch this command on both nodes. The cluster launcher supplies +# RCLI_NODE_RANK, RCLI_NUM_NODES, and RCLI_HEAD_IP; both nodes must share the +# fresh DISAGG_RUN_ROOT. Install SGLang #31847 in the active environment; the +# shared launcher applies the checked-in SpecForge capture patch before start. +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +export CONFIG="${CONFIG:-$ROOT_DIR/examples/configs/inkling-dspark-disaggregated.yaml}" +export RUN_LABEL="${RUN_LABEL:-inkling-dspark-2node}" +export TARGET_MODEL_PATH="${TARGET_MODEL_PATH:-thinkingmachines/Inkling}" + +export SERVER_GPUS="${SERVER_GPUS:-0,1,2,3}" +export SERVER_TP="${SERVER_TP:-4}" +export SERVER_MEM_FRACTION="${SERVER_MEM_FRACTION:-0.85}" +export CAPTURE_LAYER_IDS="${CAPTURE_LAYER_IDS:-5 17 35 47 59}" + +export TRAINER_GPUS="${TRAINER_GPUS:-0,1,2,3}" +export TRAINER_NPROC="${TRAINER_NPROC:-4}" +TRAINER_ACCUMULATION_STEPS="${TRAINER_ACCUMULATION_STEPS:-128}" + +export APPLY_SGLANG_CAPTURE_PATCH="${APPLY_SGLANG_CAPTURE_PATCH:-1}" +export SGLANG_ENABLE_UNIFIED_RADIX_TREE="${SGLANG_ENABLE_UNIFIED_RADIX_TREE:-1}" +export SGLANG_OPT_USE_INKLING_CUSTOM_AR="${SGLANG_OPT_USE_INKLING_CUSTOM_AR:-1}" + +DEFAULT_SERVER_EXTRA_ARGS="--dtype bfloat16 --attention-backend fa4" +DEFAULT_SERVER_EXTRA_ARGS+=" --context-length 4103 --quantization modelopt_fp4" +DEFAULT_SERVER_EXTRA_ARGS+=" --moe-runner-backend flashinfer_trtllm_routed" +DEFAULT_SERVER_EXTRA_ARGS+=" --page-size 128" +DEFAULT_SERVER_EXTRA_ARGS+=" --mamba-radix-cache-strategy extra_buffer" +DEFAULT_SERVER_EXTRA_ARGS+=" --max-mamba-cache-size 64" +DEFAULT_SERVER_EXTRA_ARGS+=" --swa-full-tokens-ratio 0.2" +export SERVER_EXTRA_ARGS="${SERVER_EXTRA_ARGS:-$DEFAULT_SERVER_EXTRA_ARGS}" + +exec "$SCRIPT_DIR/run_qwen3_8b_dflash_disagg_2node.sh" \ + "training.accumulation_steps=$TRAINER_ACCUMULATION_STEPS" \ + "$@" diff --git a/examples/disagg/run_qwen3_8b_dflash_disagg_2node.sh b/examples/disagg/run_qwen3_8b_dflash_disagg_2node.sh index 60531035c..b8dbeea71 100755 --- a/examples/disagg/run_qwen3_8b_dflash_disagg_2node.sh +++ b/examples/disagg/run_qwen3_8b_dflash_disagg_2node.sh @@ -18,6 +18,7 @@ RUN_ID="${DISAGG_STORE_ID:-}" RUN_ROOT="${DISAGG_RUN_ROOT:-}" CONSUMER_STATE_DIR="${DISAGG_CONSUMER_STATE_DIR:-${LOCAL_SCRATCH:-/tmp}/specforge/$RUN_ID/consumer-state}" CONFIG="${CONFIG:-$ROOT_DIR/examples/configs/qwen3-8b-dflash-disaggregated.yaml}" +RUN_LABEL="${RUN_LABEL:-qwen3-8b-dflash-2node}" SERVER_GPUS="${SERVER_GPUS:-0}" SERVER_TP="${SERVER_TP:-1}" @@ -27,6 +28,15 @@ CAPTURE_LAYER_IDS="${CAPTURE_LAYER_IDS:-1 9 17 25 33}" TRAINER_GPUS="${TRAINER_GPUS:-0,1,2,3}" TRAINER_NPROC="${TRAINER_NPROC:-4}" TARGET_MODEL_PATH="${TARGET_MODEL_PATH:-Qwen/Qwen3-8B}" +# Whitespace-separated SGLang CLI tokens for model-specific server settings. +# Each flag and value must be one shell token; embedded whitespace is +# unsupported. +SERVER_EXTRA_ARGS="${SERVER_EXTRA_ARGS:-}" +APPLY_SGLANG_CAPTURE_PATCH="${APPLY_SGLANG_CAPTURE_PATCH:-1}" +SERVER_EXTRA_ARGV=() +if [[ -n "$SERVER_EXTRA_ARGS" ]]; then + read -r -a SERVER_EXTRA_ARGV <<< "$SERVER_EXTRA_ARGS" +fi MOONCAKE_RPC_PORT="${MOONCAKE_RPC_PORT:-35551}" MOONCAKE_HTTP_PORT="${MOONCAKE_HTTP_PORT:-35880}" @@ -36,7 +46,7 @@ START_TIMEOUT_S="${START_TIMEOUT_S:-1800}" PEER_TIMEOUT_S="${PEER_TIMEOUT_S:-1800}" log() { - printf '[qwen3-8b-dflash-2node][rank=%s] %s\n' "${NODE_RANK:-?}" "$*" + printf '[%s][rank=%s] %s\n' "$RUN_LABEL" "${NODE_RANK:-?}" "$*" } fail() { @@ -120,6 +130,9 @@ validate_identity() { [[ "$SERVER_TP" =~ ^[1-9][0-9]*$ ]] || fail "SERVER_TP must be positive" [[ "$TRAINER_NPROC" =~ ^[1-9][0-9]*$ ]] || \ fail "TRAINER_NPROC must be positive" + [[ "$APPLY_SGLANG_CAPTURE_PATCH" == "0" || \ + "$APPLY_SGLANG_CAPTURE_PATCH" == "1" ]] || \ + fail "APPLY_SGLANG_CAPTURE_PATCH must be 0 or 1" [[ "$(count_devices "$SERVER_GPUS")" == "$SERVER_TP" ]] || \ fail "SERVER_GPUS must contain exactly SERVER_TP=$SERVER_TP devices" [[ "$(count_devices "$TRAINER_GPUS")" == "$TRAINER_NPROC" ]] || \ @@ -161,17 +174,28 @@ run_inference_node() { local producer_result=1 if [[ "${DRY_RUN:-0}" == "1" ]]; then + local -a dry_run_server_command=( + python -m sglang.launch_server --host 0.0.0.0 + --model-path "$TARGET_MODEL_PATH" + --trust-remote-code + --skip-tokenizer-init + --tp-size "$SERVER_TP" + --mem-fraction-static "$SERVER_MEM_FRACTION" + --chunked-prefill-size -1 + --enable-spec-capture --spec-capture-method dflash + --spec-capture-aux-layer-ids $CAPTURE_LAYER_IDS + --port "$SERVER_PORT" + ) + if [[ -n "$SERVER_EXTRA_ARGS" ]]; then + dry_run_server_command+=("${SERVER_EXTRA_ARGV[@]}") + fi print_command mooncake_master --enable_http_metadata_server=true \ --http_metadata_server_host=0.0.0.0 \ --rpc_port="$MOONCAKE_RPC_PORT" \ --http_metadata_server_port="$MOONCAKE_HTTP_PORT" \ --metrics_port="$MOONCAKE_METRICS_PORT" print_command env "CUDA_VISIBLE_DEVICES=$SERVER_GPUS" \ - python -m sglang.launch_server --host 0.0.0.0 \ - --model-path "$TARGET_MODEL_PATH" --tp-size "$SERVER_TP" \ - --enable-spec-capture --spec-capture-method dflash \ - --spec-capture-aux-layer-ids $CAPTURE_LAYER_IDS \ - --port "$SERVER_PORT" + "${dry_run_server_command[@]}" print_command env CUDA_VISIBLE_DEVICES= specforge train -c "$CONFIG" \ --role producer "${COMMON_OVERRIDES[@]}" "$@" result=0 @@ -195,7 +219,9 @@ run_inference_node() { command -v mooncake_master >/dev/null || fail "mooncake_master is not on PATH" command -v curl >/dev/null || fail "curl is not on PATH" - "$ROOT_DIR/scripts/apply_sglang_spec_capture_patch.sh" + if [[ "$APPLY_SGLANG_CAPTURE_PATCH" == "1" ]]; then + "$ROOT_DIR/scripts/apply_sglang_spec_capture_patch.sh" + fi export MOONCAKE_LOCAL_HOSTNAME="${INFERENCE_NODE_IP:-$HEAD_IP}" export MOONCAKE_GLOBAL_SEGMENT_SIZE="${MOONCAKE_GLOBAL_SEGMENT_SIZE:-$((32 << 30))}" export MOONCAKE_LOCAL_BUFFER_SIZE="${MOONCAKE_LOCAL_BUFFER_SIZE:-$((1 << 30))}" @@ -227,19 +253,25 @@ run_inference_node() { done read -r -a capture_layers <<< "$CAPTURE_LAYER_IDS" + local -a server_command=( + python -m sglang.launch_server + --host 0.0.0.0 + --model-path "$TARGET_MODEL_PATH" + --trust-remote-code + --skip-tokenizer-init + --tp-size "$SERVER_TP" + --mem-fraction-static "$SERVER_MEM_FRACTION" + --chunked-prefill-size -1 + --enable-spec-capture + --spec-capture-method dflash + --spec-capture-aux-layer-ids "${capture_layers[@]}" + --port "$SERVER_PORT" + ) + if [[ -n "$SERVER_EXTRA_ARGS" ]]; then + server_command+=("${SERVER_EXTRA_ARGV[@]}") + fi setsid env CUDA_VISIBLE_DEVICES="$SERVER_GPUS" \ - python -m sglang.launch_server \ - --host 0.0.0.0 \ - --model-path "$TARGET_MODEL_PATH" \ - --trust-remote-code \ - --skip-tokenizer-init \ - --tp-size "$SERVER_TP" \ - --mem-fraction-static "$SERVER_MEM_FRACTION" \ - --chunked-prefill-size -1 \ - --enable-spec-capture \ - --spec-capture-method dflash \ - --spec-capture-aux-layer-ids "${capture_layers[@]}" \ - --port "$SERVER_PORT" \ + "${server_command[@]}" \ > "$RUN_ROOT/sglang-server.log" 2>&1 & server_pid="$!" diff --git a/patches/sglang/v0.5.14/spec-capture.patch b/patches/sglang/v0.5.14/spec-capture.patch index 434c4c34d..a1081ba3d 100644 --- a/patches/sglang/v0.5.14/spec-capture.patch +++ b/patches/sglang/v0.5.14/spec-capture.patch @@ -67,14 +67,15 @@ diff --git a/python/sglang/srt/managers/io_struct.py b/python/sglang/srt/manager index 951f35495..2359c31b7 100644 --- a/python/sglang/srt/managers/io_struct.py +++ b/python/sglang/srt/managers/io_struct.py -@@ -283,3 +283,7 @@ class GenerateReqInput(BaseReq): +@@ -189,4 +189,8 @@ class GenerateReqInput(BaseReq): + # Whether to return hidden states + return_hidden_states: Union[List[bool], bool] = False + # Spec-training capture sink instructions (see spec_capture_sink.py). + # Batch-level: List[Optional[dict]]; per-request after __getitem__. + spec_capture: Optional[Union[List[Optional[Dict]], Dict]] = None + - # Pre-computed delimiter indices for multi-item scoring. - # Batch-level: List[List[int]] (one per request). After __getitem__: List[int]. - multi_item_delimiter_indices: Optional[Union[List[List[int]], List[int]]] = None + # Whether to return captured routed experts + return_routed_experts: bool = False @@ -743,6 +747,11 @@ class GenerateReqInput(BaseReq): if self.multi_item_delimiter_indices is not None else None @@ -295,7 +296,8 @@ diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/sr index 1cff5c983..a5935fa3c 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py -@@ -518,3 +518,29 @@ class ModelRunner(ModelRunnerKVCacheMixin): +@@ -518,3 +518,30 @@ class ModelRunner(ModelRunnerKVCacheMixin): +- # Apply the rank zero filter to logger + if server_args.enable_spec_capture and not self.is_draft_worker: + # Aux capture without a draft worker, routed to the strategy's own + # capture method (they wire different submodules — e.g. VL models @@ -322,7 +324,8 @@ index 1cff5c983..a5935fa3c 100644 + self.spec_aux_config.eagle_aux_hidden_state_layer_ids = ( + server_args.spec_capture_aux_layer_ids + ) - # Apply the rank zero filter to logger ++ ++ # Apply the rank zero filter to logger if server_args.show_time_cost: enable_show_time_cost() diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py diff --git a/specforge/inference/sglang_patch_inventory.md b/specforge/inference/sglang_patch_inventory.md index 95cbe916e..4bcea0603 100644 --- a/specforge/inference/sglang_patch_inventory.md +++ b/specforge/inference/sglang_patch_inventory.md @@ -22,8 +22,8 @@ providers map generic server artifacts (`aux`, `last_hidden`, passthrough inputs) to training feature names. No trainer or producer process imports SGLang model-runner internals or loads a target model. -The same patch is dry-run validated against the v0.5.14 tag and the public -Inkling integration. Capture requests carry a unique `extra_key`, so every +The same patch is dry-run validated against the v0.5.14 tag and SGLang #31847 +commit `b7252cc`. Capture requests carry a unique `extra_key`, so every training sample executes a full prefill even when radix cache support is present. Managed-local launch preserves the historical disabled-cache default; hybrid targets that require the unified radix tree set diff --git a/tests/test_runtime/test_package_architecture.py b/tests/test_runtime/test_package_architecture.py index 82e4e3b71..8559dca69 100644 --- a/tests/test_runtime/test_package_architecture.py +++ b/tests/test_runtime/test_package_architecture.py @@ -751,6 +751,7 @@ def test_examples_and_scripts_do_not_bypass_the_cli(self): Path("examples/disagg/run_offline.sh"), Path("examples/disagg/run_offline_2node.sh"), Path("examples/disagg/run_qwen3_8b_dflash_disagg_2node.sh"), + Path("examples/disagg/run_inkling_dspark_disagg_2node.sh"), } bypasses = [] train_command = re.compile(r"\btrain\s+(?:--config|-c)\b") diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index fa06f9be1..7b4751696 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -13,6 +13,9 @@ OFFLINE = ROOT / "examples" / "disagg" / "run_offline.sh" OFFLINE_TWO_NODE = ROOT / "examples" / "disagg" / "run_offline_2node.sh" TWO_NODE = ROOT / "examples" / "disagg" / "run_qwen3_8b_dflash_disagg_2node.sh" +INKLING_TWO_NODE = ( + ROOT / "examples" / "disagg" / "run_inkling_dspark_disagg_2node.sh" +) class DisaggregatedWrapperTest(unittest.TestCase): @@ -179,6 +182,54 @@ def test_two_node_wrapper_keeps_training_on_the_unified_cli(self): self.assertNotIn("torchrun", "".join(outputs.values())) self.assertFalse(shared_root.exists()) + def test_inkling_two_node_wrapper_pins_the_validated_server_contract(self): + self.assertTrue(os.access(INKLING_TWO_NODE, os.X_OK)) + syntax = subprocess.run( + ["bash", "-n", str(INKLING_TWO_NODE)], + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(syntax.returncode, 0, syntax.stderr) + + env = self._env() + env.update( + { + "NODE_RANK": "0", + "NUM_NODES": "2", + "HEAD_IP": "10.0.0.1", + "DISAGG_STORE_ID": "inkling-two-node-test", + "DISAGG_RUN_ROOT": str(self.root / "inkling-shared-attempt"), + "DRY_RUN": "1", + } + ) + result = subprocess.run( + [str(INKLING_TWO_NODE), "training.max_steps=1"], + cwd=ROOT, + env=env, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(result.returncode, 0, result.stderr) + output = result.stdout + for expected in ( + "thinkingmachines/Inkling", + "--tp-size 4", + "--spec-capture-aux-layer-ids 5 17 35 47 59", + "--attention-backend fa4", + "--quantization modelopt_fp4", + "--mamba-radix-cache-strategy extra_buffer", + "training.accumulation_steps=128", + ): + with self.subTest(expected=expected): + self.assertIn(expected, output) + self.assertNotIn("--disable-radix-cache", output) + + source = INKLING_TWO_NODE.read_text(encoding="utf-8") + self.assertIn("SGLANG_ENABLE_UNIFIED_RADIX_TREE", source) + self.assertIn("SGLANG_OPT_USE_INKLING_CUSTOM_AR", source) + def test_offline_two_node_wrapper_dispatches_roles_to_the_unified_cli(self): self.assertTrue(os.access(OFFLINE_TWO_NODE, os.X_OK)) syntax = subprocess.run( From 2021359a5aa15aa1474ca4369995e464ea636562 Mon Sep 17 00:00:00 2001 From: maocheng Date: Fri, 31 Jul 2026 22:56:33 -0700 Subject: [PATCH 09/88] docs: harden Kimi K3 disaggregated launch settings --- .../kimi-k3-dspark-v1c-disaggregated.md | 36 ++++++++++++++++--- .../kimi-k3-dspark-v1c-disaggregated.yaml | 5 ++- scripts/apply_sglang_spec_capture_patch.sh | 11 ++++-- specforge/inference/sglang_patch_inventory.md | 9 ++--- tests/test_algorithms/test_builtin_parity.py | 4 +-- 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md index aa939b64f..13fa63e97 100644 --- a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md +++ b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md @@ -9,14 +9,16 @@ effective global batch, constant learning rate, and DSpark loss weights. - SpecForge with configurable LR scheduling, an independent online prompt seed, and dedicated DSpark capture support. -- Kimi K3 SGLang revision `f8493a43a6a30d2a1cad6b0034e2c1b362d920d5`. +- Kimi K3 SGLang revision `9acd9cba39f522da71c6d9b9695f2ceb41d36b18` + (the current public `kimi-k3` branch tip validated by this recipe). - The K3 SGLang tree patched with: ```bash - scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-f8493a4 + scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-9acd9cb ``` -The patch makes `--spec-capture-method dspark` call K3's +The patch was originally authored against `f8493a4`, remains byte-identical, +and applies cleanly to `9acd9cb`. It makes `--spec-capture-method dspark` call K3's `set_dspark_layers_to_capture` hook. The generic DFlash capture method is not equivalent for K3. The same versioned patch carries the three required 64K correctness guards: 64-bit Triton token offsets, scale-stable residual scoring, @@ -42,6 +44,8 @@ server. Replace `CAPTURE_IP` with the routable address used by both nodes. ```bash export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" +export MC_TRANSFER_TIMEOUT=300 export MOONCAKE_GLOBAL_SEGMENT_SIZE=1099511627776 export MOONCAKE_LOCAL_BUFFER_SIZE=1073741824 mooncake_master \ @@ -49,7 +53,8 @@ mooncake_master \ --http_metadata_server_host=0.0.0.0 \ --rpc_port=35551 \ --http_metadata_server_port=35880 \ - --metrics_port=35903 + --metrics_port=35903 \ + --default_kv_lease_ttl=5m ``` In a second process: @@ -58,7 +63,11 @@ In a second process: export MOONCAKE_MASTER_SERVER_ADDR="$CAPTURE_IP:35551" export MOONCAKE_METADATA_SERVER="http://$CAPTURE_IP:35880/metadata" export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" +export MC_TRANSFER_TIMEOUT=300 export MOONCAKE_PROTOCOL=tcp +export MOONCAKE_GLOBAL_SEGMENT_SIZE=1099511627776 +export MOONCAKE_LOCAL_BUFFER_SIZE=1073741824 CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m sglang.launch_server \ --host 0.0.0.0 \ --port 30000 \ @@ -88,8 +97,11 @@ four-rank FSDP consumer on the same trainer host. ```bash export MOONCAKE_LOCAL_HOSTNAME="$TRAINER_IP" +export MC_TCP_BIND_ADDRESS="$TRAINER_IP" +export MC_TRANSFER_TIMEOUT=300 export WANDB_API_KEY="$(< /protected/path/wandb-api-key)" export WANDB_ENTITY=your-entity +unset RANK LOCAL_RANK WORLD_SIZE MASTER_ADDR MASTER_PORT NODE_RANK CUDA_VISIBLE_DEVICES=0,1,2,3 specforge train \ -c examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml \ --role both \ @@ -98,6 +110,16 @@ CUDA_VISIBLE_DEVICES=0,1,2,3 specforge train \ "deployment.disaggregated.mooncake_master_server_addr=$CAPTURE_IP:35551" ``` +`MC_TCP_BIND_ADDRESS` is required on multi-interface or containerized hosts. +Without it Mooncake may publish a Docker bridge address even when +`MOONCAKE_LOCAL_HOSTNAME` names the routable inter-node address, causing remote +`get_into` operations to fail. The five-minute master lease and matching +transfer timeout cover a 5.25 GiB 64K feature object over a shared TCP link; +Mooncake's short default lease can expire while that object is still in flight. +The rendezvous variables are cleared because cluster base images sometimes +inject a partial multi-node environment; SpecForge intentionally rejects that +instead of guessing which world the four trainer ranks should join. + For a one-update smoke run, additionally override the pre-tokenized four-row fixture and shrink the optimizer quantum: @@ -112,9 +134,13 @@ specforge train \ training.accumulation_steps=1 \ tracking.report_to=none \ runtime.in_flight_high_watermark=4 \ - runtime.in_flight_low_watermark=2 + runtime.in_flight_low_watermark=4 ``` +The smoke low watermark must stay at least as large as the four-rank global +optimizer-step quantum. A lower value is rejected before capture starts so a +producer cannot pause while consumers are waiting for an incomplete step. + Validate in order: config plan, patch dry-run, server health, one captured sample's tensor shapes/dtypes, one finite optimizer update and checkpoint, then the full run. A smoke pass does not establish numerical parity; compare the diff --git a/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml b/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml index 39762e205..18527b084 100644 --- a/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml @@ -63,7 +63,10 @@ runtime: # One 64K K3 sample is roughly 5.25 GiB of BF16 DSpark features. A complete # optimizer quantum is 128 samples, so the server segment must exceed 672 GiB. in_flight_high_watermark: 128 - in_flight_low_watermark: 96 + # Consumers dispatch complete optimizer windows. Keep the resume threshold + # at least one global quantum to avoid a producer/consumer backpressure + # deadlock while a window is still incomplete. + in_flight_low_watermark: 128 resident_high_watermark_bytes: 858993459200 resident_low_watermark_bytes: 697932185600 feature_store_max_resident_bytes: 966367641600 diff --git a/scripts/apply_sglang_spec_capture_patch.sh b/scripts/apply_sglang_spec_capture_patch.sh index f7cdf1222..f18e66e2b 100755 --- a/scripts/apply_sglang_spec_capture_patch.sh +++ b/scripts/apply_sglang_spec_capture_patch.sh @@ -13,11 +13,12 @@ # anything else fails loudly rather than testing against unknown server code. # # Usage: scripts/apply_sglang_spec_capture_patch.sh -# [--target v0.5.14|kimi-k3-f8493a4] [--reverse] +# [--target v0.5.14|kimi-k3-9acd9cb|kimi-k3-f8493a4] [--reverse] set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" TARGET="v0.5.14" +PATCH_TARGET="" REVERSE=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -43,19 +44,23 @@ done case "$TARGET" in v0.5.14) EXPECTED_VERSION_PREFIX="0.5.14" + PATCH_TARGET="$TARGET" ;; - kimi-k3-f8493a4) + kimi-k3-9acd9cb|kimi-k3-f8493a4) # Kimi K3's SGLang fork currently reports a base-package version that # does not uniquely identify this source revision, so patch --check is # the authoritative compatibility gate below. EXPECTED_VERSION_PREFIX="" + # The patch remains byte-identical and applies cleanly to both the + # original f8493a4 integration point and current K3 tip 9acd9cb. + PATCH_TARGET="kimi-k3-f8493a4" ;; *) echo "ERROR: unsupported SGLang patch target: $TARGET" >&2 exit 2 ;; esac -PATCH="$HERE/patches/sglang/$TARGET/spec-capture.patch" +PATCH="$HERE/patches/sglang/$PATCH_TARGET/spec-capture.patch" SGL_PARENT="$(python -c 'import sglang, os; print(os.path.dirname(os.path.dirname(sglang.__file__)))')" SGL_VERSION="$(python -c 'import sglang; print(sglang.__version__)')" diff --git a/specforge/inference/sglang_patch_inventory.md b/specforge/inference/sglang_patch_inventory.md index 83029b2c5..5fe8de953 100644 --- a/specforge/inference/sglang_patch_inventory.md +++ b/specforge/inference/sglang_patch_inventory.md @@ -2,8 +2,9 @@ SpecForge pins `sglang==0.5.14` by default. The online patch is also kept compatible with SGLang's public `inkling-support` layout, and a separately -versioned patch supports the Kimi K3 SGLang fork at revision `f8493a4`. There -are two deliberately separate SGLang integration surfaces. +versioned patch supports the Kimi K3 SGLang fork at the current validated +`kimi-k3` branch tip `9acd9cb` (and its original `f8493a4` integration point). +There are two deliberately separate SGLang integration surfaces. ## Online: external spec-capture server @@ -12,7 +13,7 @@ Online training uses one of these source-specific patches: | Target | Patch | Capture methods | |---|---|---| | SGLang v0.5.14 / `inkling-support` | [`patches/sglang/v0.5.14/spec-capture.patch`](../../patches/sglang/v0.5.14/spec-capture.patch) | EAGLE3, DFlash | -| Kimi K3 SGLang `f8493a4` | [`patches/sglang/kimi-k3-f8493a4/spec-capture.patch`](../../patches/sglang/kimi-k3-f8493a4/spec-capture.patch) | EAGLE3, DFlash, DSpark | +| Kimi K3 SGLang `9acd9cb` (`f8493a4` compatible) | [`patches/sglang/kimi-k3-f8493a4/spec-capture.patch`](../../patches/sglang/kimi-k3-f8493a4/spec-capture.patch) | EAGLE3, DFlash, DSpark | The patch adds `--enable-spec-capture` and a server-side sink that: @@ -36,7 +37,7 @@ for hybrid targets that require the unified radix tree. Apply the default patch with `scripts/apply_sglang_spec_capture_patch.sh`, or the K3 patch with -`scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-f8493a4`. +`scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-9acd9cb`. The K3 patch routes `--spec-capture-method dspark` to the model's dedicated `set_dspark_layers_to_capture` hook. It also keeps 64K capture correct by using 64-bit Triton pointer arithmetic, scale-stable residual scoring, and a generic diff --git a/tests/test_algorithms/test_builtin_parity.py b/tests/test_algorithms/test_builtin_parity.py index e7559faad..50f366034 100644 --- a/tests/test_algorithms/test_builtin_parity.py +++ b/tests/test_algorithms/test_builtin_parity.py @@ -145,9 +145,7 @@ def test_peagle_reuses_the_eagle_server_contract_explicitly(self): self.assertTrue(peagle.step.uses_external_target_head) def test_dspark_uses_the_dedicated_server_capture_method(self): - stream = self.registry.resolve("dspark").providers.server_streaming_for( - "text" - ) + stream = self.registry.resolve("dspark").providers.server_streaming_for("text") self.assertEqual("dspark", stream.capture_method) From b7d21db466d5985f2722006097a3effb93b84b83 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 01:14:31 -0700 Subject: [PATCH 10/88] feat(dspark): add Kimi-K3 MLA and KDA draft backbones --- configs/kimi-k3-dspark-4kda-1mla.json | 67 ++ configs/kimi-k3-dspark-5mla.json | 58 ++ .../kimi-k3-dspark-mla-kda-disaggregated.md | 136 ++++ examples/README.md | 2 + ...a-1mla-openperfectblend-disaggregated.yaml | 85 +++ ...k-5mla-openperfectblend-disaggregated.yaml | 85 +++ pyproject.toml | 1 + scripts/prepare_kimi_k3_openperfectblend.sh | 62 ++ specforge/algorithms/common/providers.py | 15 + specforge/algorithms/dspark/providers.py | 10 +- specforge/modeling/draft/__init__.py | 3 + specforge/modeling/draft/kimi_k3_dspark.py | 652 ++++++++++++++++++ specforge/training/model_loading.py | 5 +- .../test_algorithms/test_builtin_providers.py | 1 + .../test_example_draft_config_wiring.py | 23 +- tests/test_config/test_launch_topology.py | 4 +- .../test_unified_feature_reachability.py | 2 +- .../test_kimi_k3_dspark_architectures.py | 199 ++++++ tests/test_runtime/test_model_loading.py | 20 + 19 files changed, 1420 insertions(+), 10 deletions(-) create mode 100644 configs/kimi-k3-dspark-4kda-1mla.json create mode 100644 configs/kimi-k3-dspark-5mla.json create mode 100644 docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md create mode 100644 examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml create mode 100644 examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml create mode 100755 scripts/prepare_kimi_k3_openperfectblend.sh create mode 100644 specforge/modeling/draft/kimi_k3_dspark.py create mode 100644 tests/test_modeling/test_kimi_k3_dspark_architectures.py diff --git a/configs/kimi-k3-dspark-4kda-1mla.json b/configs/kimi-k3-dspark-4kda-1mla.json new file mode 100644 index 000000000..cc3695452 --- /dev/null +++ b/configs/kimi-k3-dspark-4kda-1mla.json @@ -0,0 +1,67 @@ +{ + "architectures": ["KimiK3DSpark4KDA1MLADraftModel"], + "attention_bias": false, + "attention_dropout": 0.0, + "auto_map": { + "AutoModel": "kimi_k3_dspark.KimiK3DSpark4KDA1MLADraftModel" + }, + "block_size": 7, + "bos_token_id": 163584, + "dflash_config": { + "confidence_head_alpha": 1.0, + "confidence_head_with_markov": true, + "enable_confidence_head": true, + "markov_head_type": "vanilla", + "markov_rank": 256, + "mask_token_id": 163824, + "projector_type": "dspark", + "target_layer_ids": [11, 23, 47, 71, 83] + }, + "draft_layer_types": ["kda", "kda", "mla", "kda", "kda"], + "dtype": "bfloat16", + "eos_token_id": 163586, + "head_dim": 192, + "hidden_act": "silu", + "hidden_size": 7168, + "initializer_range": 0.02, + "intermediate_size": 14336, + "kv_lora_rank": 512, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "linear_attn_config": { + "backend": "fla", + "gate_lower_bound": -5.0, + "head_dim": 128, + "num_heads": 96, + "short_conv_kernel_size": 4, + "use_full_rank_gate": true + }, + "max_position_embeddings": 1048576, + "max_window_layers": 5, + "mla_use_nope": true, + "mla_use_output_gate": true, + "model_type": "qwen3", + "num_attention_heads": 96, + "num_hidden_layers": 5, + "num_key_value_heads": 1, + "num_target_layers": 93, + "pad_token_id": 163839, + "q_lora_rank": 1536, + "qk_nope_head_dim": 128, + "qk_rope_head_dim": 64, + "rms_norm_eps": 0.00001, + "rope_interleave": true, + "rope_scaling": null, + "rope_theta": 10000.0, + "sliding_window": null, + "tie_word_embeddings": false, + "use_cache": true, + "use_sliding_window": false, + "v_head_dim": 128, + "vocab_size": 163840 +} diff --git a/configs/kimi-k3-dspark-5mla.json b/configs/kimi-k3-dspark-5mla.json new file mode 100644 index 000000000..2b87ddc0b --- /dev/null +++ b/configs/kimi-k3-dspark-5mla.json @@ -0,0 +1,58 @@ +{ + "architectures": ["KimiK3DSpark5MLADraftModel"], + "attention_bias": false, + "attention_dropout": 0.0, + "auto_map": { + "AutoModel": "kimi_k3_dspark.KimiK3DSpark5MLADraftModel" + }, + "block_size": 7, + "bos_token_id": 163584, + "dflash_config": { + "confidence_head_alpha": 1.0, + "confidence_head_with_markov": true, + "enable_confidence_head": true, + "markov_head_type": "vanilla", + "markov_rank": 256, + "mask_token_id": 163824, + "projector_type": "dspark", + "target_layer_ids": [11, 23, 47, 71, 83] + }, + "dtype": "bfloat16", + "eos_token_id": 163586, + "head_dim": 192, + "hidden_act": "silu", + "hidden_size": 7168, + "initializer_range": 0.02, + "intermediate_size": 14336, + "kv_lora_rank": 512, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 1048576, + "max_window_layers": 5, + "mla_use_nope": true, + "mla_use_output_gate": true, + "model_type": "qwen3", + "num_attention_heads": 96, + "num_hidden_layers": 5, + "num_key_value_heads": 1, + "num_target_layers": 93, + "pad_token_id": 163839, + "q_lora_rank": 1536, + "qk_nope_head_dim": 128, + "qk_rope_head_dim": 64, + "rms_norm_eps": 0.00001, + "rope_interleave": true, + "rope_scaling": null, + "rope_theta": 10000.0, + "sliding_window": null, + "tie_word_embeddings": false, + "use_cache": true, + "use_sliding_window": false, + "v_head_dim": 128, + "vocab_size": 163840 +} diff --git a/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md b/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md new file mode 100644 index 000000000..c0d9b139e --- /dev/null +++ b/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md @@ -0,0 +1,136 @@ +# Kimi-K3 DSpark 5×MLA and 4×KDA+1×MLA + +These experimental draft backbones train against Kimi-K3 target features with +the disaggregated SpecForge data plane: + +- `KimiK3DSpark5MLADraftModel`: five K3-style MLA layers; +- `KimiK3DSpark4KDA1MLADraftModel`: `KDA → KDA → MLA → KDA → KDA`. + +Both retain the normal DSpark target projector, Markov head, confidence head, +loss, and seven-token proposal block. They use captured target layers +`[11, 23, 47, 71, 83]` (zero based). + +## Architecture contract + +MLA uses the K3 target dimensions: 96 heads, Q LoRA rank 1536, KV LoRA rank +512, 128 non-RoPE QK channels, 64 RoPE channels, 128 value channels, and the +K3 output gate. + +KDA uses 96 heads of width 128, a bias-free four-token causal depthwise +convolution, a full-rank output gate, and the bounded forget gate with lower +bound `-5`. GPU training uses `fla-core==0.5.1`. + +In the hybrid, the central MLA layer is the only layer that reads the captured +target context. Each KDA layer operates on one proposal block at a time and +resets its state between anchors. This preserves DFlash's anchor isolation; +linear recurrence cannot connect two independently sampled proposal blocks. + +## Pinned data + +The recipes use only this Kimi-K3 Open Perfect Blend regeneration: + +| Field | Value | +| --- | --- | +| HF dataset | `skx618/Kimi-K3-OpenPerfectBlend-Regen` | +| Revision | `439c2fdc9fd2ae92e194bde468d26867b36dd660` | +| File | `data.jsonl` | +| Rows | `698316` | +| SHA-256 | `5418f09d1af8ec2e08e8385799f1eeb3c062c669b28407870f7737007bc3eeb9` | + +Store the Hugging Face token outside the repository: + +```bash +install -d -m 700 /workspace/k3_dspark/secrets +install -m 600 /dev/stdin /workspace/k3_dspark/secrets/hf_token +scripts/prepare_kimi_k3_openperfectblend.sh +``` + +The downloader verifies both the row count and SHA-256. SpecForge dynamically +renders the conversation schema and right-truncates to 4096 tokens. + +## Reference training settings + +The full recipes preserve the K3 reference run's training settings: + +- 4096 token examples, batch size 8, accumulation 32; +- 10 epochs, learning rate `6e-4`, warmup ratio `0.04`; +- 512 anchors, block size 7, Markov rank 256; +- CE/L1/confidence weights `0.1/0.9/1.0`; +- log every 10 steps and save every 250 steps; +- online W&B project `specforge-dspark`. + +The supplied topology is one TP8 K3 capture server and a separate four-rank +trainer. Because the trainer world size is part of the effective global batch, +record any topology override in the W&B run config. + +## Smoke then full training + +Start Mooncake as described in +[the K3 V1C runbook](kimi-k3-dspark-v1c-disaggregated.md), then launch the +patched latest K3 SGLang server. The capture layer ids are part of this recipe's +contract and intentionally differ from the V1C reproduction: + +```bash +export CAPTURE_IP=10.65.0.2 +export TARGET_MODEL=/workspace/models/Kimi-K3 +export MOONCAKE_MASTER_SERVER_ADDR="$CAPTURE_IP:35551" +export MOONCAKE_METADATA_SERVER="http://$CAPTURE_IP:35880/metadata" +export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" +export MOONCAKE_PROTOCOL=tcp +export MOONCAKE_GLOBAL_SEGMENT_SIZE=1099511627776 +export MOONCAKE_LOCAL_BUFFER_SIZE=1073741824 +CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m sglang.launch_server \ + --host 0.0.0.0 \ + --port 30000 \ + --model-path "$TARGET_MODEL" \ + --trust-remote-code \ + --skip-tokenizer-init \ + --tp-size 8 \ + --mem-fraction-static 0.76 \ + --context-length 4608 \ + --max-running-requests 8 \ + --max-total-tokens 40960 \ + --prefill-attention-backend flashinfer \ + --decode-attention-backend trtllm_mla \ + --moe-runner-backend marlin \ + --enable-symm-mem \ + --mamba-radix-cache-strategy extra_buffer \ + --max-mamba-cache-size 40 \ + --chunked-prefill-size -1 \ + --enable-spec-capture \ + --spec-capture-method dspark \ + --spec-capture-aux-layer-ids 11 23 47 71 83 +``` + +Use capture IP/ports that match the selected YAML. + +Keep the W&B API key in a protected file and export it only in the trainer +shell; do not put it in YAML or a command transcript. + +Run a one-step smoke with the same architecture and objective before removing +the overrides: + +```bash +WANDB_MODE=online specforge train \ + --config examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml \ + --role both \ + training.max_steps=1 \ + training.batch_size=1 \ + training.accumulation_steps=1 \ + data.train_data_path=/workspace/k3_dspark/data/kimi-k3-openperfectblend-smoke.jsonl +``` + +After the smoke produces finite loss, gradients, a checkpoint, and online W&B +telemetry, launch the full run: + +```bash +WANDB_MODE=online specforge train \ + --config examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml \ + --role both +``` + +Use the corresponding +`kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` config for the +hybrid. The two full jobs need separate output/control directories and should +not share one four-rank trainer allocation concurrently. diff --git a/examples/README.md b/examples/README.md index bf614ee76..f0852f41a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,6 +31,8 @@ NPU, offline, and managed/external-service variants, is in | `examples/configs/qwen3-8b-domino-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | Domino | | `examples/configs/qwen3-8b-peagle-disaggregated.yaml` | Disaggregated SGLang server capture | P-EAGLE | | `examples/configs/qwen3-4b-dspark-disaggregated.yaml` | Disaggregated server capture | DSpark | +| `examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml` | Kimi-K3 TP8 server capture + four-rank trainer | DSpark 5×MLA | +| `examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` | Kimi-K3 TP8 server capture + four-rank trainer | DSpark 4×KDA+1×MLA | | `examples/configs/qwen3-4b-dspark-offline.yaml` | Precomputed features | DSpark | | `examples/configs/qwen3.6-27b-dflash-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | DFlash | | `examples/configs/qwen3.6-27b-dflash-1server-dp2-disaggregated.yaml` | Managed local one capture server + DP2 | DFlash | diff --git a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml new file mode 100644 index 000000000..851b886db --- /dev/null +++ b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml @@ -0,0 +1,85 @@ +model: + target_model_path: /workspace/models/Kimi-K3-cdd2e49a + draft_model_config: configs/kimi-k3-dspark-4kda-1mla.json + target_backend: sglang + trust_remote_code: true + embedding_key: language_model.model.embed_tokens.weight + lm_head_key: language_model.lm_head.weight + mask_token_id: 163824 + torch_dtype: bfloat16 + sglang_attention_backend: flashinfer + sglang_mem_fraction_static: 0.76 + sglang_context_length: 4608 + sglang_max_running_requests: 8 + sglang_max_total_tokens: 40960 + sglang_moe_runner_backend: marlin + sglang_enable_symm_mem: true + sglang_mamba_radix_cache_strategy: extra_buffer + sglang_max_mamba_cache_size: 40 + +data: + train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl + max_length: 4096 + chat_template: kimi-k3-thinking + cache_dir: /workspace/k3_dspark/cache + build_dataset_num_proc: 64 + dataloader_num_workers: 0 + +training: + strategy: dspark + num_epochs: 10 + batch_size: 8 + accumulation_steps: 32 + learning_rate: 0.0006 + warmup_ratio: 0.04 + max_grad_norm: 1 + attention_backend: flex_attention + num_anchors: 512 + loss_decay_gamma: 4.0 + objective_chunk_blocks: 128 + dspark_ce_loss_alpha: 0.1 + dspark_l1_loss_alpha: 0.9 + dspark_confidence_head_alpha: 1.0 + save_interval: 250 + log_interval: 10 + dist_timeout: 30 + seed: 42 + prompt_seed: 1 + +tracking: + report_to: wandb + wandb_project: specforge-dspark + wandb_name: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated + wandb_offline: false + wandb_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/wandb + +runtime: + producer_lease: 1 + producer_concurrency: 8 + in_flight_high_watermark: 64 + in_flight_low_watermark: 32 + resident_high_watermark_bytes: 34359738368 + resident_low_watermark_bytes: 17179869184 + feature_store_max_resident_bytes: 68719476736 + +run_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated +output_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/output + +deployment: + mode: disaggregated + trainer: + nnodes: 1 + nproc_per_node: 4 + disaggregated: + control_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/control + consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/consumer-state + backend: mooncake + store_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated + server_urls: + - http://10.65.0.2:30000 + mooncake_metadata_server: http://10.65.0.2:35880/metadata + mooncake_master_server_addr: 10.65.0.2:35551 + mooncake_protocol: tcp + client_buffer_size: 1073741824 + idle_timeout_s: 7200 + peer_wait_timeout_s: 7200 diff --git a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml new file mode 100644 index 000000000..771dd13de --- /dev/null +++ b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml @@ -0,0 +1,85 @@ +model: + target_model_path: /workspace/models/Kimi-K3-cdd2e49a + draft_model_config: configs/kimi-k3-dspark-5mla.json + target_backend: sglang + trust_remote_code: true + embedding_key: language_model.model.embed_tokens.weight + lm_head_key: language_model.lm_head.weight + mask_token_id: 163824 + torch_dtype: bfloat16 + sglang_attention_backend: flashinfer + sglang_mem_fraction_static: 0.76 + sglang_context_length: 4608 + sglang_max_running_requests: 8 + sglang_max_total_tokens: 40960 + sglang_moe_runner_backend: marlin + sglang_enable_symm_mem: true + sglang_mamba_radix_cache_strategy: extra_buffer + sglang_max_mamba_cache_size: 40 + +data: + train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl + max_length: 4096 + chat_template: kimi-k3-thinking + cache_dir: /workspace/k3_dspark/cache + build_dataset_num_proc: 64 + dataloader_num_workers: 0 + +training: + strategy: dspark + num_epochs: 10 + batch_size: 8 + accumulation_steps: 32 + learning_rate: 0.0006 + warmup_ratio: 0.04 + max_grad_norm: 1 + attention_backend: flex_attention + num_anchors: 512 + loss_decay_gamma: 4.0 + objective_chunk_blocks: 128 + dspark_ce_loss_alpha: 0.1 + dspark_l1_loss_alpha: 0.9 + dspark_confidence_head_alpha: 1.0 + save_interval: 250 + log_interval: 10 + dist_timeout: 30 + seed: 42 + prompt_seed: 1 + +tracking: + report_to: wandb + wandb_project: specforge-dspark + wandb_name: kimi-k3-dspark-5mla-openperfectblend-disaggregated + wandb_offline: false + wandb_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/wandb + +runtime: + producer_lease: 1 + producer_concurrency: 8 + in_flight_high_watermark: 64 + in_flight_low_watermark: 32 + resident_high_watermark_bytes: 34359738368 + resident_low_watermark_bytes: 17179869184 + feature_store_max_resident_bytes: 68719476736 + +run_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated +output_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/output + +deployment: + mode: disaggregated + trainer: + nnodes: 1 + nproc_per_node: 4 + disaggregated: + control_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/control + consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/consumer-state + backend: mooncake + store_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated + server_urls: + - http://10.65.0.2:30000 + mooncake_metadata_server: http://10.65.0.2:35880/metadata + mooncake_master_server_addr: 10.65.0.2:35551 + mooncake_protocol: tcp + client_buffer_size: 1073741824 + idle_timeout_s: 7200 + peer_wait_timeout_s: 7200 diff --git a/pyproject.toml b/pyproject.toml index 9d8097322..278cde7e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dev = [ ] fa = ["flash-attn", "ninja", "packaging"] liger = ["liger-kernel"] +kda = ["fla-core==0.5.1"] [tool.setuptools.dynamic] version = {file = "version.txt"} diff --git a/scripts/prepare_kimi_k3_openperfectblend.sh b/scripts/prepare_kimi_k3_openperfectblend.sh new file mode 100755 index 000000000..2079cfd6e --- /dev/null +++ b/scripts/prepare_kimi_k3_openperfectblend.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Download the exact Kimi-K3 Open Perfect Blend regeneration used by the K3 +# draft recipes. The token is read from a protected file and is never printed. + +REPO_ID=${REPO_ID:-skx618/Kimi-K3-OpenPerfectBlend-Regen} +REVISION=${REVISION:-439c2fdc9fd2ae92e194bde468d26867b36dd660} +FILENAME=${FILENAME:-data.jsonl} +EXPECTED_SHA256=${EXPECTED_SHA256:-5418f09d1af8ec2e08e8385799f1eeb3c062c669b28407870f7737007bc3eeb9} +EXPECTED_ROWS=${EXPECTED_ROWS:-698316} +OUTPUT=${OUTPUT:-/workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl} +SMOKE_OUTPUT=${SMOKE_OUTPUT:-/workspace/k3_dspark/data/kimi-k3-openperfectblend-smoke.jsonl} +HF_TOKEN_FILE=${HF_TOKEN_FILE:-/workspace/k3_dspark/secrets/hf_token} + +if [[ -z "${HF_TOKEN:-}" && -r "$HF_TOKEN_FILE" ]]; then + IFS= read -r HF_TOKEN < "$HF_TOKEN_FILE" || [[ -n "$HF_TOKEN" ]] +fi +if [[ -z "${HF_TOKEN:-}" ]]; then + printf 'ERROR: set HF_TOKEN or create mode-600 %s\n' "$HF_TOKEN_FILE" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$OUTPUT")" +downloaded=$( + HF_TOKEN="$HF_TOKEN" python3 - "$REPO_ID" "$REVISION" "$FILENAME" <<'PY' +import os +import sys + +from huggingface_hub import hf_hub_download + +print( + hf_hub_download( + repo_id=sys.argv[1], + repo_type="dataset", + revision=sys.argv[2], + filename=sys.argv[3], + token=os.environ["HF_TOKEN"], + ) +) +PY +) +install -m 0644 "$downloaded" "$OUTPUT" + +actual_sha256=$(sha256sum "$OUTPUT" | awk '{print $1}') +actual_rows=$(wc -l < "$OUTPUT" | tr -d ' ') +if [[ "$actual_sha256" != "$EXPECTED_SHA256" ]]; then + printf 'ERROR: dataset SHA-256 mismatch: %s\n' "$actual_sha256" >&2 + exit 1 +fi +if [[ "$actual_rows" != "$EXPECTED_ROWS" ]]; then + printf 'ERROR: dataset row count mismatch: %s\n' "$actual_rows" >&2 + exit 1 +fi +sed -n '1,4p' "$OUTPUT" > "$SMOKE_OUTPUT" +[[ "$(wc -l < "$SMOKE_OUTPUT" | tr -d ' ')" == 4 ]] || { + printf 'ERROR: failed to create four-row smoke fixture\n' >&2 + exit 1 +} +printf 'validated Kimi-K3 Open Perfect Blend: rows=%s sha256=%s path=%s\n' \ + "$actual_rows" "$actual_sha256" "$OUTPUT" +printf 'created four-row smoke fixture: %s\n' "$SMOKE_OUTPUT" diff --git a/specforge/algorithms/common/providers.py b/specforge/algorithms/common/providers.py index 370feeca8..18cbeb7b5 100644 --- a/specforge/algorithms/common/providers.py +++ b/specforge/algorithms/common/providers.py @@ -166,9 +166,24 @@ class DraftConfigProvider: target_defaults: TargetDerivedDraftDefaults | None = None expected_auto_map_model: str | None = None apply_overrides: Factory | None = None + compatible_architectures: FrozenSet[str] | None = None def __post_init__(self) -> None: _non_empty(self.architecture, field_name="architecture") + compatible = self.compatible_architectures + if compatible is None: + compatible = frozenset({self.architecture}) + else: + compatible = frozenset(compatible) + if not compatible: + raise ValueError("compatible_architectures must not be empty") + for item in compatible: + _non_empty(item, field_name="compatible_architectures item") + if self.architecture not in compatible: + raise ValueError( + "architecture must be included in compatible_architectures" + ) + object.__setattr__(self, "compatible_architectures", compatible) if self.expected_auto_map_model is not None: _non_empty( self.expected_auto_map_model, diff --git a/specforge/algorithms/dspark/providers.py b/specforge/algorithms/dspark/providers.py index b115f8929..e8ae66684 100644 --- a/specforge/algorithms/dspark/providers.py +++ b/specforge/algorithms/dspark/providers.py @@ -36,6 +36,13 @@ ALGORITHM_NAME = "dspark" DRAFT_ARCHITECTURE = "DSparkDraftModel" +COMPATIBLE_DRAFT_ARCHITECTURES = frozenset( + { + DRAFT_ARCHITECTURE, + "KimiK3DSpark5MLADraftModel", + "KimiK3DSpark4KDA1MLADraftModel", + } +) def build_step(wrapped_model, *, target_head=None, **_options): @@ -112,7 +119,7 @@ def algorithm_spec() -> AlgorithmSpec: return AlgorithmSpec( name=ALGORITHM_NAME, draft=DraftRequirement( - compatible_architectures={DRAFT_ARCHITECTURE}, + compatible_architectures=COMPATIBLE_DRAFT_ARCHITECTURES, default_architecture=DRAFT_ARCHITECTURE, ), feature_contracts=( @@ -155,6 +162,7 @@ def algorithm_providers() -> AlgorithmProviders: model=ModelProvider( draft_config=DraftConfigProvider( architecture=DRAFT_ARCHITECTURE, + compatible_architectures=COMPATIBLE_DRAFT_ARCHITECTURES, expected_auto_map_model="dspark.DSparkDraftModel", ), build_draft=build_draft, diff --git a/specforge/modeling/draft/__init__.py b/specforge/modeling/draft/__init__.py index 839869dd6..34cec6c4a 100644 --- a/specforge/modeling/draft/__init__.py +++ b/specforge/modeling/draft/__init__.py @@ -7,6 +7,7 @@ ) from .domino import DominoDraftModel from .dspark import DSparkDraftModel +from .kimi_k3_dspark import KimiK3DSpark4KDA1MLADraftModel, KimiK3DSpark5MLADraftModel from .llama3_eagle import LlamaForCausalLMEagle3 from .peagle import PEagleDraftModel from .registry import DRAFT_REGISTRY, available_drafts, register_draft, resolve_draft @@ -16,6 +17,8 @@ "DFlashDraftModel", "DominoDraftModel", "DSparkDraftModel", + "KimiK3DSpark4KDA1MLADraftModel", + "KimiK3DSpark5MLADraftModel", "LlamaForCausalLMEagle3", "PEagleDraftModel", "build_target_layer_ids", diff --git a/specforge/modeling/draft/kimi_k3_dspark.py b/specforge/modeling/draft/kimi_k3_dspark.py new file mode 100644 index 000000000..188095c03 --- /dev/null +++ b/specforge/modeling/draft/kimi_k3_dspark.py @@ -0,0 +1,652 @@ +"""Kimi-K3 MLA and KDA backbones for DSpark draft training. + +The target model alternates Kimi Delta Attention (KDA) with Multi-Latent +Attention (MLA). These draft variants keep DSpark's projector, Markov head, +confidence head, and DFlash objective while replacing only the five decoder +layers: + +* ``KimiK3DSpark5MLADraftModel`` uses five MLA layers. +* ``KimiK3DSpark4KDA1MLADraftModel`` uses KDA, KDA, MLA, KDA, KDA. + +The hybrid intentionally uses MLA as its only target-context injection point. +Each KDA layer resets at every proposal block, so anchors never share recurrent +state and the DFlash training mask cannot be bypassed by a linear-attention +scan. +""" + +from __future__ import annotations + +from typing import Callable, Optional + +import torch +import torch.nn.functional as F +from torch import nn +from torch.nn.attention.flex_attention import BlockMask, flex_attention +from transformers.cache_utils import Cache +from transformers.models.qwen3.modeling_qwen3 import ( + FlashAttentionKwargs, + GradientCheckpointingLayer, + Qwen3MLP, + Qwen3PreTrainedModel, + Qwen3RMSNorm, + Qwen3RotaryEmbedding, +) +from typing_extensions import Tuple, Unpack + +from .dflash import build_target_layer_ids, normalize_draft_head_checkpoint_keys +from .dspark import DSparkDraftModel +from .registry import register_draft + + +def _rotate_half(x: torch.Tensor, *, interleaved: bool) -> torch.Tensor: + if interleaved: + paired = x.float().reshape(*x.shape[:-1], -1, 2) + first, second = paired.unbind(dim=-1) + return torch.stack((-second, first), dim=-1).flatten(-2).to(x.dtype) + first, second = x.chunk(2, dim=-1) + return torch.cat((-second, first), dim=-1) + + +def _apply_rope( + x: torch.Tensor, + positions: torch.Tensor, + inv_freq: torch.Tensor, + *, + interleaved: bool, +) -> torch.Tensor: + freqs = torch.einsum("bs,d->bsd", positions.float(), inv_freq.float()) + angles = ( + torch.repeat_interleave(freqs, 2, dim=-1) + if interleaved + else torch.cat((freqs, freqs), dim=-1) + ) + cos = angles.cos().to(dtype=x.dtype).unsqueeze(1) + sin = angles.sin().to(dtype=x.dtype).unsqueeze(1) + return x * cos + _rotate_half(x, interleaved=interleaved) * sin + + +class KimiK3DraftMLAAttention(nn.Module): + """K3 MLA in compressed-latent (absorbed) form.""" + + def __init__(self, config, layer_idx: int): + super().__init__() + del layer_idx + self.hidden_size = int(config.hidden_size) + self.num_heads = int(config.num_attention_heads) + self.q_lora_rank = int(config.q_lora_rank) + self.kv_lora_rank = int(config.kv_lora_rank) + self.qk_nope_head_dim = int(config.qk_nope_head_dim) + self.qk_rope_head_dim = int(config.qk_rope_head_dim) + self.v_head_dim = int(config.v_head_dim) + self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim + self.scaling = self.qk_head_dim**-0.5 + self.rope_interleave = bool(getattr(config, "rope_interleave", True)) + self.use_output_gate = bool(getattr(config, "mla_use_output_gate", False)) + + if self.qk_rope_head_dim % 2: + raise ValueError("qk_rope_head_dim must be even") + + bias = bool(getattr(config, "attention_bias", False)) + eps = float(config.rms_norm_eps) + self.q_a_proj = nn.Linear(self.hidden_size, self.q_lora_rank, bias=bias) + self.q_a_layernorm = Qwen3RMSNorm(self.q_lora_rank, eps=eps) + self.q_b_proj = nn.Linear( + self.q_lora_rank, + self.num_heads * self.qk_head_dim, + bias=bias, + ) + self.kv_a_proj_with_mqa = nn.Linear( + self.hidden_size, + self.kv_lora_rank + self.qk_rope_head_dim, + bias=bias, + ) + self.kv_a_layernorm = Qwen3RMSNorm(self.kv_lora_rank, eps=eps) + self.kv_b_proj = nn.Linear( + self.kv_lora_rank, + self.num_heads * (self.qk_nope_head_dim + self.v_head_dim), + bias=bias, + ) + if self.use_output_gate: + self.g_proj = nn.Linear( + self.hidden_size, + self.num_heads * self.v_head_dim, + bias=False, + ) + self.o_proj = nn.Linear( + self.num_heads * self.v_head_dim, + self.hidden_size, + bias=bias, + ) + + rope_parameters = getattr(config, "rope_parameters", None) or {} + rope_theta = float( + rope_parameters.get( + "rope_theta", + getattr(config, "rope_theta", 10000.0), + ) + ) + inv_freq = 1.0 / ( + rope_theta + ** ( + torch.arange(0, self.qk_rope_head_dim, 2, dtype=torch.float32) + / self.qk_rope_head_dim + ) + ) + self.register_buffer("inv_freq", inv_freq, persistent=False) + + def _project_kv(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + latent = self.kv_a_proj_with_mqa(x) + kv_latent, k_rope = latent.split( + [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 + ) + return self.kv_a_layernorm(kv_latent), k_rope + + @torch.compiler.disable + def forward( + self, + hidden_states: torch.Tensor, + target_hidden: torch.Tensor, + position_ids: torch.LongTensor, + attention_mask: Optional[torch.Tensor], + past_key_values: Optional[Cache] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: + del kwargs + if past_key_values is not None: + raise NotImplementedError( + "Kimi-K3 draft MLA training does not use the HF cache path" + ) + + batch, query_len = hidden_states.shape[:2] + context_len = target_hidden.shape[1] + q_lora = self.q_a_layernorm(self.q_a_proj(hidden_states)) + q = self.q_b_proj(q_lora).view( + batch, query_len, self.num_heads, self.qk_head_dim + ) + q_nope, q_rope = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1) + + context_latent, context_rope = self._project_kv(target_hidden) + noise_latent, noise_rope = self._project_kv(hidden_states) + kv_latent = torch.cat((context_latent, noise_latent), dim=1) + k_rope = torch.cat((context_rope, noise_rope), dim=1) + + q_positions = position_ids[:, -query_len:] + q_rope = _apply_rope( + q_rope.transpose(1, 2), + q_positions, + self.inv_freq, + interleaved=self.rope_interleave, + ).transpose(1, 2) + k_rope = _apply_rope( + k_rope.unsqueeze(1), + position_ids[:, : context_len + query_len], + self.inv_freq, + interleaved=self.rope_interleave, + ) + + kv_b = self.kv_b_proj.weight.view( + self.num_heads, + self.qk_nope_head_dim + self.v_head_dim, + self.kv_lora_rank, + ) + w_kc, w_vc = kv_b.split([self.qk_nope_head_dim, self.v_head_dim], dim=1) + q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) + q_attn = torch.cat((q_absorbed, q_rope), dim=-1).transpose(1, 2) + k_attn = torch.cat((kv_latent.unsqueeze(1), k_rope), dim=-1) + v_attn = kv_latent.unsqueeze(1) + + if isinstance(attention_mask, BlockMask): + latent_out = flex_attention( + q_attn, + k_attn, + v_attn, + block_mask=attention_mask, + scale=self.scaling, + enable_gqa=True, + ) + else: + latent_out = F.scaled_dot_product_attention( + q_attn, + k_attn, + v_attn, + attn_mask=attention_mask, + dropout_p=0.0, + scale=self.scaling, + enable_gqa=True, + ) + + attn_out = torch.einsum("bhqk,hvk->bqhv", latent_out, w_vc) + attn_out = attn_out.reshape(batch, query_len, -1) + if self.use_output_gate: + attn_out = attn_out * torch.sigmoid(self.g_proj(hidden_states)) + return self.o_proj(attn_out), None + + +class KimiK3DraftMLADecoderLayer(GradientCheckpointingLayer): + def __init__(self, config, layer_idx: int): + super().__init__() + hidden_size = int(config.hidden_size) + eps = float(config.rms_norm_eps) + self.self_attn = KimiK3DraftMLAAttention(config, layer_idx) + self.mlp = Qwen3MLP(config) + self.input_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) + self.post_attention_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) + + def forward( + self, + target_hidden: Optional[torch.Tensor] = None, + hidden_states: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Cache] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[torch.FloatTensor]: + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + hidden_states = self.self_attn( + hidden_states=hidden_states, + target_hidden=target_hidden, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_value, + **kwargs, + )[0] + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + return (residual + self.mlp(hidden_states),) + + +class KimiK3ShortConvolution(nn.Module): + """Causal depthwise convolution with target-compatible parameter layout.""" + + def __init__(self, channels: int, kernel_size: int): + super().__init__() + self.kernel_size = int(kernel_size) + self.weight = nn.Parameter(torch.empty(channels, self.kernel_size)) + nn.init.normal_(self.weight, mean=0.0, std=0.02) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.transpose(1, 2) + x = F.pad(x, (self.kernel_size - 1, 0)) + x = F.conv1d( + x, + self.weight.unsqueeze(1), + bias=None, + groups=self.weight.shape[0], + ) + return F.silu(x.transpose(1, 2)) + + +class KimiK3GatedRMSNorm(nn.Module): + def __init__(self, hidden_size: int, eps: float): + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.eps = float(eps) + + def forward(self, x: torch.Tensor, gate: torch.Tensor) -> torch.Tensor: + variance = x.float().pow(2).mean(dim=-1, keepdim=True) + normalized = x * torch.rsqrt(variance + self.eps).to(x.dtype) + return normalized * self.weight.to(x.dtype) * torch.sigmoid(gate) + + +def _reference_kda( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + raw_gate: torch.Tensor, + beta: torch.Tensor, + A_log: torch.Tensor, + dt_bias: torch.Tensor, + lower_bound: Optional[float], +) -> torch.Tensor: + """Small differentiable recurrence used by CPU tests, not production.""" + + q = F.normalize(q.float(), dim=-1).to(q.dtype) + k = F.normalize(k.float(), dim=-1).to(k.dtype) + beta = torch.sigmoid(beta.float()).to(q.dtype) + gate_input = raw_gate.float() + dt_bias.view(1, 1, *raw_gate.shape[-2:]) + scale = A_log.float().exp().view(1, 1, -1, 1) + if lower_bound is None: + log_decay = -scale * F.softplus(gate_input) + else: + log_decay = float(lower_bound) * torch.sigmoid(scale * gate_input) + log_decay = log_decay.to(q.dtype) + + state = q.new_zeros( + q.shape[0], q.shape[2], q.shape[3], v.shape[3], dtype=torch.float32 + ) + outputs = [] + score_scale = q.shape[-1] ** -0.5 + for step in range(q.shape[1]): + decay = log_decay[:, step].float().exp().unsqueeze(-1) + state = state * decay + key = k[:, step].float() + value = v[:, step].float() + prediction = torch.einsum("bhd,bhdv->bhv", key, state) + delta = (value - prediction) * beta[:, step].float().unsqueeze(-1) + state = state + torch.einsum("bhd,bhv->bhdv", key, delta) + outputs.append( + torch.einsum("bhd,bhdv->bhv", q[:, step].float(), state) + .mul(score_scale) + .to(q.dtype) + ) + return torch.stack(outputs, dim=1) + + +def _fla_kda( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + raw_gate: torch.Tensor, + beta: torch.Tensor, + A_log: torch.Tensor, + dt_bias: torch.Tensor, + lower_bound: Optional[float], +) -> torch.Tensor: + try: + from fla.ops.kda import chunk_kda + except ImportError as exc: + raise ImportError( + "Kimi-K3 KDA training requires fla-core==0.5.1; install " + "SpecForge with the 'kda' extra" + ) from exc + + output, _ = chunk_kda( + q=q, + k=k, + v=v, + g=raw_gate, + beta=beta, + A_log=A_log, + dt_bias=dt_bias, + output_final_state=False, + use_qk_l2norm_in_kernel=True, + use_gate_in_kernel=True, + use_beta_sigmoid_in_kernel=True, + safe_gate=lower_bound is not None, + lower_bound=lower_bound, + ) + return output + + +class KimiK3DraftKDAAttention(nn.Module): + """K3 KDA applied independently to every DSpark proposal block.""" + + def __init__(self, config, layer_idx: int): + super().__init__() + del layer_idx + linear_config = dict(getattr(config, "linear_attn_config", None) or {}) + self.hidden_size = int(config.hidden_size) + self.head_dim = int(linear_config["head_dim"]) + self.num_heads = int(linear_config["num_heads"]) + self.block_size = int(config.block_size) + self.conv_size = int(linear_config["short_conv_kernel_size"]) + self.use_full_rank_gate = bool(linear_config.get("use_full_rank_gate", False)) + self.lower_bound = linear_config.get("gate_lower_bound") + self.backend = str(linear_config.get("backend", "fla")).lower() + if self.backend not in {"fla", "reference"}: + raise ValueError( + "linear_attn_config.backend must be 'fla' or 'reference', " + f"got {self.backend!r}" + ) + + projection_size = self.num_heads * self.head_dim + self.q_proj = nn.Linear(self.hidden_size, projection_size, bias=False) + self.k_proj = nn.Linear(self.hidden_size, projection_size, bias=False) + self.v_proj = nn.Linear(self.hidden_size, projection_size, bias=False) + self.q_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) + self.k_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) + self.v_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) + + self.A_log = nn.Parameter( + torch.log(torch.empty(self.num_heads, dtype=torch.float32).uniform_(1, 16)) + ) + self.f_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) + self.f_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) + self.dt_bias = nn.Parameter(torch.zeros(projection_size, dtype=torch.float32)) + self.b_proj = nn.Linear(self.hidden_size, self.num_heads, bias=False) + if self.use_full_rank_gate: + self.g_proj = nn.Linear(self.hidden_size, projection_size, bias=False) + else: + self.g_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) + self.g_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) + self.o_norm = KimiK3GatedRMSNorm(self.head_dim, eps=float(config.rms_norm_eps)) + self.o_proj = nn.Linear(projection_size, self.hidden_size, bias=False) + + def _blocks(self, x: torch.Tensor) -> tuple[torch.Tensor, int, int]: + batch, query_len, hidden = x.shape + if query_len % self.block_size: + raise ValueError( + "KDA draft query length must be divisible by block_size; " + f"got {query_len} and {self.block_size}" + ) + num_blocks = query_len // self.block_size + return ( + x.reshape(batch * num_blocks, self.block_size, hidden), + batch, + query_len, + ) + + @torch.compiler.disable + def forward( + self, + hidden_states: torch.Tensor, + target_hidden: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Cache] = None, + **kwargs, + ) -> tuple[torch.Tensor, None]: + del target_hidden, attention_mask, kwargs + if past_key_values is not None: + raise NotImplementedError( + "Kimi-K3 draft KDA training resets state at each proposal block" + ) + + blocks, batch, query_len = self._blocks(hidden_states) + q = self.q_conv1d(self.q_proj(blocks)) + k = self.k_conv1d(self.k_proj(blocks)) + v = self.v_conv1d(self.v_proj(blocks)) + shape = (*q.shape[:2], self.num_heads, self.head_dim) + q, k, v = (tensor.view(shape) for tensor in (q, k, v)) + raw_gate = self.f_b_proj(self.f_a_proj(blocks)).view(shape) + beta = self.b_proj(blocks).float() + + kernel: Callable[..., torch.Tensor] + kernel = _reference_kda if self.backend == "reference" else _fla_kda + output = kernel( + q, + k, + v, + raw_gate, + beta, + self.A_log, + self.dt_bias, + self.lower_bound, + ) + if self.use_full_rank_gate: + output_gate = self.g_proj(blocks).view(shape) + else: + output_gate = self.g_b_proj(self.g_a_proj(blocks)).view(shape) + output = self.o_norm(output, output_gate) + output = output.reshape(*blocks.shape[:2], -1) + output = self.o_proj(output).reshape(batch, query_len, self.hidden_size) + return output, None + + +class KimiK3DraftKDADecoderLayer(GradientCheckpointingLayer): + def __init__(self, config, layer_idx: int): + super().__init__() + hidden_size = int(config.hidden_size) + eps = float(config.rms_norm_eps) + self.self_attn = KimiK3DraftKDAAttention(config, layer_idx) + self.mlp = Qwen3MLP(config) + self.input_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) + self.post_attention_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) + + def forward( + self, + target_hidden: Optional[torch.Tensor] = None, + hidden_states: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Cache] = None, + **kwargs, + ) -> Tuple[torch.FloatTensor]: + del position_ids + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + hidden_states = self.self_attn( + hidden_states=hidden_states, + target_hidden=target_hidden, + attention_mask=attention_mask, + past_key_values=past_key_value, + **kwargs, + )[0] + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + return (residual + self.mlp(hidden_states),) + + +class _KimiK3DSparkDraftBase(DSparkDraftModel): + expected_projector_type = "dspark" + + def _initialize_kimi_backbone(self, config, layers: nn.ModuleList) -> None: + dflash_config = dict(getattr(config, "dflash_config", None) or {}) + projector_type = dflash_config.get("projector_type") + if projector_type is None: + dflash_config["projector_type"] = self.expected_projector_type + elif projector_type != self.expected_projector_type: + raise ValueError( + "Kimi-K3 DSpark drafts require dflash_config.projector_type='dspark'" + ) + config.dflash_config = dflash_config + + # Avoid constructing and immediately discarding the large GQA DSpark + # backbone. This is the shared DFlash initialization with custom layers. + Qwen3PreTrainedModel.__init__(self, config) + self.config = config + self.layers = layers + self.target_layer_ids = dflash_config.get( + "target_layer_ids", + build_target_layer_ids(config.num_target_layers, config.num_hidden_layers), + ) + self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen3RotaryEmbedding(config) + self.fc = nn.Linear( + len(self.target_layer_ids) * config.hidden_size, + config.hidden_size, + bias=False, + ) + self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.block_size = int(config.block_size) + self.mask_token_id = dflash_config.get("mask_token_id") + self.projector_type = dflash_config.get("projector_type") + self.pure_draft_prefix_len = dflash_config.get("pure_draft_prefix_len", 0) + self.shift_label = dflash_config.get("shift_label", False) + self._init_draft_head(config, dflash_config) + self.register_load_state_dict_pre_hook(normalize_draft_head_checkpoint_keys) + self.post_init() + + def forward( + self, + position_ids: torch.LongTensor, + attention_mask: Optional[torch.Tensor] = None, + noise_embedding: Optional[torch.Tensor] = None, + target_hidden: Optional[torch.Tensor] = None, + past_key_values: Optional[Cache] = None, + use_cache: bool = False, + **kwargs, + ) -> torch.Tensor: + if use_cache or past_key_values is not None: + raise NotImplementedError( + "Kimi-K3 DSpark training backbones do not use the HF cache path" + ) + hidden_states = noise_embedding + target_hidden = self.hidden_norm(self.fc(target_hidden)) + for layer in self.layers: + hidden_states = layer( + hidden_states=hidden_states, + target_hidden=target_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + **kwargs, + )[0] + return self.norm(hidden_states) + + +def _validate_mla_config(config) -> None: + required = ( + "q_lora_rank", + "kv_lora_rank", + "qk_nope_head_dim", + "qk_rope_head_dim", + "v_head_dim", + ) + missing = [name for name in required if getattr(config, name, None) is None] + if missing: + raise ValueError(f"Kimi-K3 draft MLA config is missing: {missing}") + if not bool(getattr(config, "mla_use_nope", False)): + raise ValueError("Kimi-K3 draft MLA requires mla_use_nope=true") + if not bool(getattr(config, "mla_use_output_gate", False)): + raise ValueError("Kimi-K3 draft MLA requires mla_use_output_gate=true") + + +@register_draft +class KimiK3DSpark5MLADraftModel(_KimiK3DSparkDraftBase): + """Five-layer K3 MLA DSpark draft.""" + + _no_split_modules = ["KimiK3DraftMLADecoderLayer"] + + def __init__(self, config) -> None: + _validate_mla_config(config) + if int(config.num_hidden_layers) != 5: + raise ValueError("KimiK3DSpark5MLADraftModel requires exactly 5 layers") + layers = nn.ModuleList( + KimiK3DraftMLADecoderLayer(config, layer_idx) + for layer_idx in range(config.num_hidden_layers) + ) + self._initialize_kimi_backbone(config, layers) + + +@register_draft +class KimiK3DSpark4KDA1MLADraftModel(_KimiK3DSparkDraftBase): + """K3 draft with KDA, KDA, MLA, KDA, KDA layers.""" + + _no_split_modules = [ + "KimiK3DraftKDADecoderLayer", + "KimiK3DraftMLADecoderLayer", + ] + + def __init__(self, config) -> None: + _validate_mla_config(config) + if int(config.num_hidden_layers) != 5: + raise ValueError("KimiK3DSpark4KDA1MLADraftModel requires exactly 5 layers") + layer_pattern = list( + getattr(config, "draft_layer_types", None) + or ["kda", "kda", "mla", "kda", "kda"] + ) + expected = ["kda", "kda", "mla", "kda", "kda"] + if layer_pattern != expected: + raise ValueError( + "Kimi-K3 4KDA+1MLA layer pattern must be " + f"{expected}, got {layer_pattern}" + ) + factories = { + "kda": KimiK3DraftKDADecoderLayer, + "mla": KimiK3DraftMLADecoderLayer, + } + layers = nn.ModuleList( + factories[layer_type](config, layer_idx) + for layer_idx, layer_type in enumerate(layer_pattern) + ) + self._initialize_kimi_backbone(config, layers) + + +__all__ = [ + "KimiK3DSpark5MLADraftModel", + "KimiK3DSpark4KDA1MLADraftModel", + "KimiK3DraftMLAAttention", + "KimiK3DraftKDAAttention", +] diff --git a/specforge/training/model_loading.py b/specforge/training/model_loading.py index 75fb32f82..fcc7294ee 100644 --- a/specforge/training/model_loading.py +++ b/specforge/training/model_loading.py @@ -288,11 +288,12 @@ def resolve_draft_config( draft_config = _generate_draft_config(cfg, provider) expected = provider.architecture + compatible = provider.compatible_architectures or frozenset({expected}) architectures = list(getattr(draft_config, "architectures", None) or []) - if architectures != [expected]: + if len(architectures) != 1 or architectures[0] not in compatible: raise ValueError( f"training.strategy={cfg.training.strategy!r} requires draft " - f"architecture {expected}, got {architectures!r}" + f"architecture in {sorted(compatible)!r}, got {architectures!r}" ) _apply_draft_overrides(cfg, draft_config, provider) return draft_config diff --git a/tests/test_algorithms/test_builtin_providers.py b/tests/test_algorithms/test_builtin_providers.py index 66bb669e3..79a0a7d94 100644 --- a/tests/test_algorithms/test_builtin_providers.py +++ b/tests/test_algorithms/test_builtin_providers.py @@ -101,6 +101,7 @@ def test_draft_resolution_stays_outside_algorithm_spec(self): self.assertEqual( { "architecture", + "compatible_architectures", "target_defaults", "expected_auto_map_model", "apply_overrides", diff --git a/tests/test_config/test_example_draft_config_wiring.py b/tests/test_config/test_example_draft_config_wiring.py index 74e610f9f..8b274f582 100644 --- a/tests/test_config/test_example_draft_config_wiring.py +++ b/tests/test_config/test_example_draft_config_wiring.py @@ -49,16 +49,29 @@ def test_local_draft_architecture_matches_recipe_strategy(self): draft_provider = algorithm.providers.model.draft_config self.assertTrue(draft_config.is_file(), draft_config) payload = json.loads(draft_config.read_text()) - self.assertEqual( - payload.get("architectures"), - [draft_provider.architecture], + architectures = payload.get("architectures") + self.assertIsInstance(architectures, list) + self.assertEqual(len(architectures), 1) + architecture = architectures[0] + self.assertIn( + architecture, + draft_provider.compatible_architectures, ) expected_auto_model = draft_provider.expected_auto_map_model - if expected_auto_model is not None: + actual_auto_model = payload.get("auto_map", {}).get("AutoModel") + if ( + expected_auto_model is not None + and architecture == draft_provider.architecture + ): self.assertEqual( - payload.get("auto_map", {}).get("AutoModel"), + actual_auto_model, expected_auto_model, ) + elif actual_auto_model is not None: + self.assertEqual( + actual_auto_model.rsplit(".", 1)[-1], + architecture, + ) def test_only_future_vlm_draft_configs_lack_a_unified_recipe(self): referenced = { diff --git a/tests/test_config/test_launch_topology.py b/tests/test_config/test_launch_topology.py index 13b3d9b43..ee1a06c26 100644 --- a/tests/test_config/test_launch_topology.py +++ b/tests/test_config/test_launch_topology.py @@ -21,6 +21,8 @@ "lfm2.5-1.2b-instruct-dflash-online.yaml": 8, "inkling-dspark-disaggregated.yaml": 1, "kimi-k3-dspark-v1c-disaggregated.yaml": 4, + "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml": 4, + "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml": 4, "ling-flash-2.0-eagle3-offline.yaml": 8, "ling-flash-2.0-eagle3-online.yaml": 8, "llama3.1-8b-eagle3-offline.yaml": 1, @@ -313,7 +315,7 @@ def _recipes() -> dict[str, Path]: class ExampleLaunchTopologyTest(unittest.TestCase): def test_every_recipe_has_the_explicit_golden_topology(self): recipes = _recipes() - self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 64) + self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 66) self.assertEqual(set(recipes), set(EXPECTED_NPROC_PER_NODE)) for filename, nproc_per_node in EXPECTED_NPROC_PER_NODE.items(): diff --git a/tests/test_config/test_unified_feature_reachability.py b/tests/test_config/test_unified_feature_reachability.py index db9dbb48c..b65b3c76f 100644 --- a/tests/test_config/test_unified_feature_reachability.py +++ b/tests/test_config/test_unified_feature_reachability.py @@ -146,7 +146,7 @@ def test_all_example_configs_validate_through_the_typed_entry(self): for path in EXAMPLE_CONFIG_DIR.glob("*.yaml") if not path.name.startswith(".") ) - self.assertEqual(len(paths), 64) + self.assertEqual(len(paths), 66) resolved_runs = { path.name: resolve_run(Config.from_file(str(path))) for path in paths diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py new file mode 100644 index 000000000..a9cd2a361 --- /dev/null +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -0,0 +1,199 @@ +import json +from pathlib import Path + +import pytest +import torch +from transformers.models.qwen3.modeling_qwen3 import Qwen3Config + +from specforge.config import Config +from specforge.modeling.draft.kimi_k3_dspark import ( + KimiK3DraftKDAAttention, + KimiK3DSpark4KDA1MLADraftModel, + KimiK3DSpark5MLADraftModel, +) + +ROOT = Path(__file__).resolve().parents[2] + + +def _tiny_config(architecture: str, *, layers: int = 5) -> Qwen3Config: + config = Qwen3Config( + architectures=[architecture], + hidden_size=32, + intermediate_size=64, + num_hidden_layers=layers, + num_attention_heads=4, + num_key_value_heads=1, + head_dim=8, + q_lora_rank=8, + kv_lora_rank=8, + qk_nope_head_dim=4, + qk_rope_head_dim=4, + v_head_dim=4, + mla_use_nope=True, + mla_use_output_gate=True, + rope_interleave=True, + max_position_embeddings=128, + vocab_size=64, + block_size=3, + num_target_layers=8, + dflash_config={ + "projector_type": "dspark", + "target_layer_ids": [0, 1, 2, 3, 4], + "mask_token_id": 63, + "markov_rank": 4, + "enable_confidence_head": True, + "confidence_head_with_markov": True, + "confidence_head_alpha": 1.0, + }, + draft_layer_types=["kda", "kda", "mla", "kda", "kda"], + linear_attn_config={ + "backend": "reference", + "gate_lower_bound": -5.0, + "head_dim": 4, + "num_heads": 4, + "short_conv_kernel_size": 4, + "use_full_rank_gate": True, + }, + layer_types=["full_attention"] * layers, + attention_bias=False, + ) + config._attn_implementation = "sdpa" + return config + + +@pytest.mark.parametrize( + ("filename", "architecture"), + [ + ("kimi-k3-dspark-5mla.json", "KimiK3DSpark5MLADraftModel"), + ( + "kimi-k3-dspark-4kda-1mla.json", + "KimiK3DSpark4KDA1MLADraftModel", + ), + ], +) +def test_production_configs_match_k3_target(filename, architecture): + config = json.loads((ROOT / "configs" / filename).read_text()) + assert config["architectures"] == [architecture] + assert config["block_size"] == 7 + assert config["num_hidden_layers"] == 5 + assert config["dflash_config"]["target_layer_ids"] == [11, 23, 47, 71, 83] + assert config["mla_use_output_gate"] is True + assert { + "num_attention_heads": config["num_attention_heads"], + "q_lora_rank": config["q_lora_rank"], + "kv_lora_rank": config["kv_lora_rank"], + "qk_nope_head_dim": config["qk_nope_head_dim"], + "qk_rope_head_dim": config["qk_rope_head_dim"], + "v_head_dim": config["v_head_dim"], + } == { + "num_attention_heads": 96, + "q_lora_rank": 1536, + "kv_lora_rank": 512, + "qk_nope_head_dim": 128, + "qk_rope_head_dim": 64, + "v_head_dim": 128, + } + + +@pytest.mark.parametrize( + "filename", + [ + "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", + "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", + ], +) +def test_training_recipes_preserve_reference_run_contract(filename): + config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) + assert config.data.max_length == 4096 + assert config.training.batch_size == 8 + assert config.training.accumulation_steps == 32 + assert config.training.num_epochs == 10 + assert config.training.learning_rate == pytest.approx(6e-4) + assert config.training.warmup_ratio == pytest.approx(0.04) + assert config.training.num_anchors == 512 + assert config.training.save_interval == 250 + assert config.training.log_interval == 10 + assert config.tracking.report_to == "wandb" + assert config.tracking.wandb_offline is False + assert "kimi-k3-openperfectblend-regen-439c2fdc" in (config.data.train_data_path) + + +def _forward_backward(model): + batch, context_len, query_len = 1, 5, 6 + config = model.config + target_hidden = torch.randn( + batch, + context_len, + len(config.dflash_config["target_layer_ids"]) * config.hidden_size, + ) + noise_embedding = torch.randn( + batch, query_len, config.hidden_size, requires_grad=True + ) + position_ids = torch.arange(context_len + query_len).expand(batch, -1) + output = model( + position_ids=position_ids, + target_hidden=target_hidden, + noise_embedding=noise_embedding, + ) + assert output.shape == (batch, query_len, config.hidden_size) + assert torch.isfinite(output).all() + output.square().mean().backward() + assert noise_embedding.grad is not None + assert torch.isfinite(noise_embedding.grad).all() + + +def test_tiny_5mla_forward_and_backward(): + model = KimiK3DSpark5MLADraftModel(_tiny_config("KimiK3DSpark5MLADraftModel")) + _forward_backward(model) + assert all(layer.self_attn.use_output_gate for layer in model.layers) + + +def test_tiny_4kda_1mla_forward_and_backward(): + model = KimiK3DSpark4KDA1MLADraftModel( + _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + ) + _forward_backward(model) + assert [type(layer.self_attn).__name__ for layer in model.layers] == [ + "KimiK3DraftKDAAttention", + "KimiK3DraftKDAAttention", + "KimiK3DraftMLAAttention", + "KimiK3DraftKDAAttention", + "KimiK3DraftKDAAttention", + ] + first_kda = model.layers[0].self_attn + assert "q_conv1d.weight" in first_kda.state_dict() + assert "q_conv1d.bias" not in first_kda.state_dict() + + +def test_kda_resets_state_between_proposal_blocks(): + config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + attention = KimiK3DraftKDAAttention(config, layer_idx=0) + first = torch.randn(1, config.block_size, config.hidden_size) + second = torch.randn_like(first) + baseline = attention(torch.cat((first, second), dim=1))[0] + changed = attention(torch.cat((first, second + 20.0), dim=1))[0] + torch.testing.assert_close( + baseline[:, : config.block_size], + changed[:, : config.block_size], + ) + + +def test_hybrid_rejects_noncanonical_layer_order(): + config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + config.draft_layer_types = ["mla", "kda", "kda", "kda", "kda"] + with pytest.raises(ValueError, match="layer pattern"): + KimiK3DSpark4KDA1MLADraftModel(config) + + +def test_5mla_rejects_noncanonical_layer_count(): + config = _tiny_config("KimiK3DSpark5MLADraftModel", layers=4) + with pytest.raises(ValueError, match="exactly 5 layers"): + KimiK3DSpark5MLADraftModel(config) + + +@pytest.mark.parametrize("field", ["mla_use_nope", "mla_use_output_gate"]) +def test_mla_requires_k3_attention_features(field): + config = _tiny_config("KimiK3DSpark5MLADraftModel") + setattr(config, field, False) + with pytest.raises(ValueError, match=field): + KimiK3DSpark5MLADraftModel(config) diff --git a/tests/test_runtime/test_model_loading.py b/tests/test_runtime/test_model_loading.py index 87f053957..3d16ed869 100644 --- a/tests/test_runtime/test_model_loading.py +++ b/tests/test_runtime/test_model_loading.py @@ -106,6 +106,26 @@ def _draft_payload(architecture: str, *, layers: int = 1, block_size=None): class DraftConfigResolutionTest(unittest.TestCase): + def test_dspark_accepts_registered_kimi_k3_backbones(self): + provider = _draft_config_provider("dspark") + for architecture in ( + "KimiK3DSpark5MLADraftModel", + "KimiK3DSpark4KDA1MLADraftModel", + ): + with ( + self.subTest(architecture=architecture), + tempfile.TemporaryDirectory() as directory, + ): + path = os.path.join(directory, "draft.json") + payload = _draft_payload(architecture, layers=5, block_size=7) + with open(path, "w", encoding="utf-8") as stream: + json.dump(payload, stream) + resolved = resolve_draft_config( + _run_config("dspark", draft_model_config=path), + provider=provider, + ) + self.assertEqual(resolved.architectures, [architecture]) + def test_config_resolution_does_not_initialize_cuda_model_dependencies(self): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "draft.json") From 15fd6c49d108dd12cd67c5e179dcec41e4190854 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 09:06:24 -0700 Subject: [PATCH 11/88] fix(data): register Kimi-K3 thinking template --- specforge/data/template.py | 19 +++++++++++++++++++ .../test_kimi_k3_dspark_architectures.py | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/specforge/data/template.py b/specforge/data/template.py index cfb409829..26d1e8218 100644 --- a/specforge/data/template.py +++ b/specforge/data/template.py @@ -249,6 +249,25 @@ def get_all_template_names(self) -> List[str]: ), ) +# Kimi K3 uses the checkpoint tokenizer's XTML renderer rather than a Jinja +# template. The rendered assistant turn opens the thinking segment before the +# stored reasoning content, so supervision starts after this exact scaffold and +# excludes the stop-trimmed end-of-message token. +TEMPLATE_REGISTRY.register( + name="kimi-k3-thinking", + template=ChatTemplate( + assistant_header=( + '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' + ), + user_header='<|open|>message role="user"<|sep|>', + system_prompt=None, + end_of_turn_token="<|end_of_msg|>", + parser_type="thinking", + enable_thinking=False, + ignore_token=["<|end_of_msg|>"], + ), +) + TEMPLATE_REGISTRY.register( name="deepseek-v3", template=ChatTemplate( diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index a9cd2a361..72529ed9c 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -6,6 +6,7 @@ from transformers.models.qwen3.modeling_qwen3 import Qwen3Config from specforge.config import Config +from specforge.data.template import TEMPLATE_REGISTRY from specforge.modeling.draft.kimi_k3_dspark import ( KimiK3DraftKDAAttention, KimiK3DSpark4KDA1MLADraftModel, @@ -115,9 +116,22 @@ def test_training_recipes_preserve_reference_run_contract(filename): assert config.training.log_interval == 10 assert config.tracking.report_to == "wandb" assert config.tracking.wandb_offline is False + assert config.data.chat_template in TEMPLATE_REGISTRY.get_all_template_names() assert "kimi-k3-openperfectblend-regen-439c2fdc" in (config.data.train_data_path) +def test_kimi_k3_template_matches_target_xtml_contract(): + template = TEMPLATE_REGISTRY.get("kimi-k3-thinking") + assert template.assistant_header == ( + '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' + ) + assert template.user_header == '<|open|>message role="user"<|sep|>' + assert template.end_of_turn_token == "<|end_of_msg|>" + assert template.parser_type == "thinking" + assert template.enable_thinking is False + assert template.ignore_token == ["<|end_of_msg|>"] + + def _forward_backward(model): batch, context_len, query_len = 1, 5, 6 config = model.config From 2ecd685cfa0dbbd0ca19e9e834c2abaa11d83fcc Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 09:12:49 -0700 Subject: [PATCH 12/88] fix(runtime): force terminal Mooncake cleanup --- .../runtime/data_plane/mooncake_store.py | 28 +++++++++++++--- tests/test_runtime/test_mooncake_store.py | 32 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/specforge/runtime/data_plane/mooncake_store.py b/specforge/runtime/data_plane/mooncake_store.py index 5ef4e75d3..62b28ae37 100644 --- a/specforge/runtime/data_plane/mooncake_store.py +++ b/specforge/runtime/data_plane/mooncake_store.py @@ -314,10 +314,23 @@ def _store_get_tensor(self, key: str, out: torch.Tensor) -> None: f"mooncake get_into short read for {key}: got {rc} of {nb} bytes" ) - def _store_remove(self, key: str) -> bool: - """Best-effort physical free. Returns True on confirmed removal.""" + def _store_remove(self, key: str, *, force: bool = False) -> bool: + """Best-effort physical free. Returns True on confirmed removal. + + Recent Mooncake bindings expose ``remove(key, force=True)`` so a + lifecycle authority can reclaim an object after all application-level + leases have closed without waiting for Mooncake's (potentially + minutes-long) KV lease TTL. Older bindings only accept ``key``; keep + those usable and let their normal bounded retry behavior apply. + """ try: - rc = self._store.remove(key) + if force: + try: + rc = self._store.remove(key, force=True) + except TypeError: + rc = self._store.remove(key) + else: + rc = self._store.remove(key) except Exception: # pragma: no cover - transient RPC failure return False return rc is None or int(rc) == 0 @@ -565,6 +578,7 @@ def _try_physical_free( sample_id: str, *, confirm_absent_on_failure: bool = True, + force: bool = False, ) -> bool: """Remove all tensor objects. False on a retryable RPC failure. @@ -581,7 +595,7 @@ def _try_physical_free( ok = True for name in self._sample_names.get(sample_id, []): key = self._tkey(sample_id, gen, name) - if self._store_remove(key): + if self._store_remove(key, force=force): continue if confirm_absent_on_failure and not self._store_exists(key): continue # already gone (freed remotely) counts as freed @@ -675,6 +689,12 @@ def drain_pending_removals( try: physically_removed = self._try_physical_free( sample_id, + # The application lease has already been released + # before a sample enters _release_pending. Use the + # lifecycle-authority path in current Mooncake so + # its default multi-minute KV lease does not turn a + # clean trainer shutdown into a false failure. + force=True, # Intermediate retries must not renew Mooncake's # read lease. The final probe only classifies an # already-absent key and has no following retry to diff --git a/tests/test_runtime/test_mooncake_store.py b/tests/test_runtime/test_mooncake_store.py index 3c1590b9f..a10cf3636 100644 --- a/tests/test_runtime/test_mooncake_store.py +++ b/tests/test_runtime/test_mooncake_store.py @@ -267,6 +267,38 @@ def release_remote_lease(interval): self.assertEqual(fs.health()["force_freed_total"], 1) self.assertFalse(_phys_resident(fake)) + def test_lifecycle_drain_forces_removal_after_application_lease_closes(self): + class ForceAwareFake(_FakeMooncakeStore): + def __init__(self): + super().__init__() + self.force_values = [] + + def remove(self, key, force=False): + self.remove_calls += 1 + self.force_values.append(force) + if not force: + return -706 + self._d.pop(key, None) + return 0 + + fake = ForceAwareFake() + fs = MooncakeFeatureStore(store=fake, store_id="run0") + ref = fs.put(_tensors(), sample_id="s0", metadata=_meta()) + _, handle = fs.get(ref) + + fs.release(handle) + self.assertEqual(fs.health()["release_pending"], 1) + self.assertTrue(_phys_resident(fake)) + + report = drain_feature_store_removals(fs) + + self.assertEqual(report["attempts"], 1) + self.assertEqual(fs.health()["release_pending"], 0) + self.assertFalse(_phys_resident(fake)) + num_features = len(ref.feature_keys) + self.assertEqual(fake.force_values[:num_features], [False] * num_features) + self.assertEqual(fake.force_values[num_features:], [True] * num_features) + def test_lifecycle_drain_does_not_renew_read_lease_between_retries(self): clock = _FakeClock() lease_ttl = 1.0 From af428cc890be2b98d6231c4ec5e55e70a9396d38 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 10:20:47 -0700 Subject: [PATCH 13/88] fix(runtime): validate online flow window before data build --- specforge/training/disaggregated.py | 69 ++++++++++++++++++++++------- tests/test_runtime/test_schedule.py | 41 +++++++++++++++++ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/specforge/training/disaggregated.py b/specforge/training/disaggregated.py index e68e4139f..7fa1faa6c 100644 --- a/specforge/training/disaggregated.py +++ b/specforge/training/disaggregated.py @@ -207,6 +207,56 @@ def _online_prompt_seed(cfg: Config) -> int: return cfg.training.seed if configured is None else configured +def _online_flow_window(cfg: Config) -> tuple[int, Optional[int]]: + """Resolve and validate producer ref watermarks before prompt preparation. + + The consumer dispatches one complete global optimizer window at a time. + Validating this contract only after tokenizing/materializing prompts can + waste tens of minutes and many GiB for a large online dataset. + """ + from specforge.runtime.control_plane.flow_control import FlowControlLimits + + high_override = os.environ.get("DISAGG_IN_FLIGHT_HIGH_WATERMARK") + high = int(high_override or cfg.runtime.in_flight_high_watermark) + low_override = os.environ.get("DISAGG_IN_FLIGHT_LOW_WATERMARK") + # Preserve the legacy one-watermark environment override: when only the + # old high value is supplied, resume at that same threshold. + low = ( + int(low_override) + if low_override is not None + else ( + None if high_override is not None else cfg.runtime.in_flight_low_watermark + ) + ) + limits = FlowControlLimits( + high_watermark_refs=high, + low_watermark_refs=low, + max_prompt_lease_per_worker=cfg.runtime.producer_lease, + ) + trainer = cfg.deployment.trainer + consumer_quantum = ( + trainer.nnodes + * trainer.nproc_per_node + * cfg.training.batch_size + * cfg.training.accumulation_steps + ) + if high < consumer_quantum: + raise ValueError( + "producer in-flight high watermark " + f"{high} is smaller than the consumer's global optimizer-step " + f"quantum {consumer_quantum}; set " + "DISAGG_IN_FLIGHT_HIGH_WATERMARK to at least that value" + ) + if limits.resolved_low_watermark_refs < consumer_quantum: + raise ValueError( + "producer in-flight low watermark " + f"{limits.resolved_low_watermark_refs} is smaller than the " + f"consumer's global optimizer-step quantum {consumer_quantum}; " + "set DISAGG_IN_FLIGHT_LOW_WATERMARK to at least that value" + ) + return high, low + + def _online_schedule_payload(cfg: Config, *, num_prompts: int) -> dict: """Describe the exact finite online schedule prepared by the producer.""" from specforge.training.schedule import resolve_online_total_steps @@ -545,6 +595,9 @@ def _build_online( from specforge.launch import build_disagg_online_producer from specforge.training.model_loading import resolve_draft_config + # This check is independent of dataset size. Keep it before tokenizer + # loading and prompt preparation so an invalid window fails cheaply. + in_flight_high_watermark, in_flight_low_watermark = _online_flow_window(cfg) input_adapter = streaming.create_input_adapter(cfg) input_tools = _load_input_tools( cfg, @@ -597,22 +650,6 @@ def _build_online( ] target_repr = streaming.target_representation peer_wait_timeout_s = _optional_timeout_s("DISAGG_PEER_WAIT_TIMEOUT") - high_watermark_override = os.environ.get("DISAGG_IN_FLIGHT_HIGH_WATERMARK") - in_flight_high_watermark = int( - high_watermark_override or cfg.runtime.in_flight_high_watermark - ) - low_watermark_override = os.environ.get("DISAGG_IN_FLIGHT_LOW_WATERMARK") - # Preserve the legacy one-watermark environment override: when only the - # old high value is supplied, resume at that same threshold. - in_flight_low_watermark = ( - int(low_watermark_override) - if low_watermark_override is not None - else ( - None - if high_watermark_override is not None - else cfg.runtime.in_flight_low_watermark - ) - ) _workers, drive = build_disagg_online_producer( algorithm=algorithm, modality=modality, diff --git a/tests/test_runtime/test_schedule.py b/tests/test_runtime/test_schedule.py index 2d25c04ca..cd0e826d5 100644 --- a/tests/test_runtime/test_schedule.py +++ b/tests/test_runtime/test_schedule.py @@ -1,10 +1,13 @@ import json +import os import tempfile import unittest from types import SimpleNamespace +from unittest import mock from specforge.training.disaggregated import ( _ONLINE_SCHEDULE_SUFFIX, + _online_flow_window, _online_schedule_payload, _read_online_total_steps, _write_control, @@ -17,6 +20,44 @@ class TestResolveTotalSteps(unittest.TestCase): + @staticmethod + def _online_flow_config(*, high=1152, low=1024): + return SimpleNamespace( + runtime=SimpleNamespace( + in_flight_high_watermark=high, + in_flight_low_watermark=low, + producer_lease=8, + ), + training=SimpleNamespace(batch_size=8, accumulation_steps=32), + deployment=SimpleNamespace( + trainer=SimpleNamespace(nnodes=1, nproc_per_node=4) + ), + ) + + def test_online_flow_window_accepts_one_global_optimizer_window(self): + self.assertEqual( + _online_flow_window(self._online_flow_config()), + (1152, 1024), + ) + + def test_online_flow_window_rejects_small_watermarks_before_data_build(self): + cfg = self._online_flow_config(high=64, low=32) + with self.assertRaisesRegex(ValueError, "high watermark 64.*quantum 1024"): + _online_flow_window(cfg) + + cfg = self._online_flow_config(high=1152, low=32) + with self.assertRaisesRegex(ValueError, "low watermark 32.*quantum 1024"): + _online_flow_window(cfg) + + def test_online_flow_window_preserves_high_only_environment_override(self): + cfg = self._online_flow_config(high=64, low=32) + environment = { + "DISAGG_IN_FLIGHT_HIGH_WATERMARK": "1024", + } + with mock.patch.dict(os.environ, environment, clear=False): + os.environ.pop("DISAGG_IN_FLIGHT_LOW_WATERMARK", None) + self.assertEqual(_online_flow_window(cfg), (1024, None)) + def test_finite_data_horizon_counts_optimizer_steps(self): self.assertEqual( resolve_total_steps( From e1a4bd10ed22a999618f82b38cba423b286180b6 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 10:32:04 -0700 Subject: [PATCH 14/88] fix(recipe): preserve Kimi K3 global batch window --- ...da-1mla-openperfectblend-disaggregated.yaml | 12 ++++++------ ...rk-5mla-openperfectblend-disaggregated.yaml | 12 ++++++------ .../test_kimi_k3_dspark_architectures.py | 18 +++++++++++++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml index 851b886db..8cdf7959a 100644 --- a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml @@ -29,7 +29,7 @@ training: strategy: dspark num_epochs: 10 batch_size: 8 - accumulation_steps: 32 + accumulation_steps: 16 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -56,11 +56,11 @@ tracking: runtime: producer_lease: 1 producer_concurrency: 8 - in_flight_high_watermark: 64 - in_flight_low_watermark: 32 - resident_high_watermark_bytes: 34359738368 - resident_low_watermark_bytes: 17179869184 - feature_store_max_resident_bytes: 68719476736 + in_flight_high_watermark: 576 + in_flight_low_watermark: 512 + resident_high_watermark_bytes: 206158430208 + resident_low_watermark_bytes: 180388626432 + feature_store_max_resident_bytes: 240518168576 run_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated output_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/output diff --git a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml index 771dd13de..43fd3bab1 100644 --- a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml @@ -29,7 +29,7 @@ training: strategy: dspark num_epochs: 10 batch_size: 8 - accumulation_steps: 32 + accumulation_steps: 16 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -56,11 +56,11 @@ tracking: runtime: producer_lease: 1 producer_concurrency: 8 - in_flight_high_watermark: 64 - in_flight_low_watermark: 32 - resident_high_watermark_bytes: 34359738368 - resident_low_watermark_bytes: 17179869184 - feature_store_max_resident_bytes: 68719476736 + in_flight_high_watermark: 576 + in_flight_low_watermark: 512 + resident_high_watermark_bytes: 206158430208 + resident_low_watermark_bytes: 180388626432 + feature_store_max_resident_bytes: 240518168576 run_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated output_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/output diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index 72529ed9c..3e6c7a06c 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -107,13 +107,29 @@ def test_training_recipes_preserve_reference_run_contract(filename): config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) assert config.data.max_length == 4096 assert config.training.batch_size == 8 - assert config.training.accumulation_steps == 32 + assert config.training.accumulation_steps == 16 + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + == 512 + ) assert config.training.num_epochs == 10 assert config.training.learning_rate == pytest.approx(6e-4) assert config.training.warmup_ratio == pytest.approx(0.04) assert config.training.num_anchors == 512 assert config.training.save_interval == 250 assert config.training.log_interval == 10 + assert config.runtime.in_flight_high_watermark >= 512 + assert config.runtime.in_flight_low_watermark >= 512 + # A worst-case 4,096-token optimizer window carries about 168 GiB of + # captured features; do not reintroduce byte backpressure below it. + assert config.runtime.resident_high_watermark_bytes >= 180388626432 + assert ( + config.runtime.feature_store_max_resident_bytes + >= config.runtime.resident_high_watermark_bytes + ) assert config.tracking.report_to == "wandb" assert config.tracking.wandb_offline is False assert config.data.chat_template in TEMPLATE_REGISTRY.get_all_template_names() From daa41a35c7af73e17b1b1b77150772503405d4fb Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 10:34:06 -0700 Subject: [PATCH 15/88] fix(runtime): fill optimizer window before byte throttle --- specforge/launch.py | 9 +++- tests/test_runtime/test_disagg_multiserver.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/specforge/launch.py b/specforge/launch.py index c2d73da7c..e2da828cd 100644 --- a/specforge/launch.py +++ b/specforge/launch.py @@ -1150,10 +1150,17 @@ def run_worker(w) -> None: in_flight = channel.in_flight_remote() current_resident_bytes = resident_bytes() - paused = flow_control.should_pause( + # The consumer cannot acknowledge anything until one + # complete optimizer window is available. Byte + # backpressure below that ref quantum would deadlock + # both roles; allow the required window to fill while + # the explicit hard byte cap remains enforced by + # publish_refs(). + policy_paused = flow_control.should_pause( in_flight_refs=in_flight, resident_bytes=current_resident_bytes, ) + paused = in_flight >= consumer_quantum and policy_paused if paused: if backpressure_started is None: backpressure_started = time.monotonic() diff --git a/tests/test_runtime/test_disagg_multiserver.py b/tests/test_runtime/test_disagg_multiserver.py index da7e96d0e..d901c53b1 100644 --- a/tests/test_runtime/test_disagg_multiserver.py +++ b/tests/test_runtime/test_disagg_multiserver.py @@ -338,6 +338,53 @@ def run_producer(): self.assertGreaterEqual(snapshot["pause_transitions"], 1) self.assertGreaterEqual(snapshot["resume_transitions"], 1) + def test_byte_watermark_cannot_block_the_first_optimizer_window(self): + backend = _FakeMooncakeStore() + stub = _StubCaptureServer(backend) + store = MooncakeFeatureStore(store=backend, store_id="run0") + channel = StreamingRefChannel(os.path.join(self._workdir(), "refs.jsonl")) + _workers, drive = _build( + [_adapter(store, stub)], + _prompts(3), + store, + channel, + consumer_quantum=3, + lease=1, + resident_high_watermark_bytes=1, + resident_low_watermark_bytes=0, + ) + + outcome = {} + + def run_producer(): + try: + outcome["produced"] = drive() + except BaseException as exc: # expose a thread failure to the test + outcome["error"] = exc + + thread = threading.Thread(target=run_producer, daemon=True) + thread.start() + deadline = time.monotonic() + 2 + while channel.published < 3 and time.monotonic() < deadline: + time.sleep(0.001) + published_before_ack = channel.published + + # Keep cleanup bounded if this invariant regresses and the producer + # pauses before publishing a complete window. + reader = StreamingRefChannel(channel.path) + cleanup_deadline = time.monotonic() + 2 + while thread.is_alive() and time.monotonic() < cleanup_deadline: + refs = reader.poll() + if refs: + reader.mark_consumed(len(refs)) + time.sleep(0.001) + thread.join(2) + + self.assertEqual(published_before_ack, 3) + self.assertFalse(thread.is_alive()) + self.assertNotIn("error", outcome) + self.assertEqual(outcome.get("produced"), 3) + def test_hard_byte_cap_aborts_unpublished_capture_and_fails_channel(self): backend = _FakeMooncakeStore() stub = _StubCaptureServer(backend) From 6ea5ab16b4e01e6c1c18acd27da6559aac89b297 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 10:39:17 -0700 Subject: [PATCH 16/88] fix(tracking): log resolved config with recursive redaction --- specforge/tracker.py | 37 +++++++++++++++---- specforge/training/assembly.py | 5 +++ .../test_unified_feature_reachability.py | 5 +++ tests/test_runtime/test_tracking_logger.py | 20 ++++++++++ 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/specforge/tracker.py b/specforge/tracker.py index ef886028d..4257715a6 100644 --- a/specforge/tracker.py +++ b/specforge/tracker.py @@ -3,6 +3,7 @@ import abc import netrc import os +from collections.abc import Mapping from typing import Any, Dict, Optional import torch.distributed as dist @@ -40,15 +41,35 @@ # --- End Lazy Imports --- +def _is_secret_field(name: str) -> bool: + lowered = name.lower() + return lowered in { + "key", + "token", + "password", + "secret", + "wandb_key", + "swanlab_key", + "hf_key", + } or lowered.endswith(("_api_key", "_auth_token", "_token", "_password", "_secret")) + + +def _redact_config(value: Any, *, field: str | None = None) -> Any: + if field is not None and _is_secret_field(field): + return None if value is None else "" + if isinstance(value, Mapping): + return { + str(name): _redact_config(item, field=str(name)) + for name, item in value.items() + } + if isinstance(value, (list, tuple)): + return [_redact_config(item) for item in value] + return value + + def _public_config(args) -> Dict[str, Any]: - """Return tracker metadata without copying credentials into run logs.""" - config = dict(vars(args)) - for name in list(config): - lowered = name.lower() - if any(secret in lowered for secret in ("key", "token", "password")): - if config[name] is not None: - config[name] = "" - return config + """Return recursively redacted tracker metadata safe for run logs.""" + return _redact_config(vars(args)) class Tracker(abc.ABC): diff --git a/specforge/training/assembly.py b/specforge/training/assembly.py index 0e39cc480..ac64b4f2d 100644 --- a/specforge/training/assembly.py +++ b/specforge/training/assembly.py @@ -307,6 +307,11 @@ def _configured_logger(cfg: Config): options["swanlab_name"] = options["swanlab_name"] or cfg.run_id options["mlflow_experiment_name"] = options["mlflow_experiment_name"] or "specforge" options["mlflow_run_name"] = options["mlflow_run_name"] or cfg.run_id + if cfg.tracking.report_to == "wandb": + # W&B is the canonical reproduction record for the disaggregated K3 + # runs. Keep the complete resolved config next to the metric stream; + # tracker._public_config recursively redacts credentials before init. + options["specforge_config"] = cfg.model_dump(mode="json") return create_tracker_logger( SimpleNamespace(**options), cfg.output_dir, console_logger=_logger ) diff --git a/tests/test_config/test_unified_feature_reachability.py b/tests/test_config/test_unified_feature_reachability.py index b65b3c76f..6c8c6e08c 100644 --- a/tests/test_config/test_unified_feature_reachability.py +++ b/tests/test_config/test_unified_feature_reachability.py @@ -337,6 +337,11 @@ def test_tracking_config_reaches_the_existing_tracker_adapter(self): self.assertEqual(args.wandb_name, "experiment") self.assertTrue(args.wandb_offline) self.assertEqual(args.wandb_dir, "/tmp/wandb") + self.assertEqual(args.specforge_config["run_id"], "run") + self.assertEqual( + args.specforge_config["training"]["strategy"], + cfg.training.strategy, + ) self.assertEqual(output_dir, "/tmp/output") self.assertIs(create.call_args.kwargs["console_logger"], _logger) diff --git a/tests/test_runtime/test_tracking_logger.py b/tests/test_runtime/test_tracking_logger.py index 9e5da434c..6a8c2b985 100644 --- a/tests/test_runtime/test_tracking_logger.py +++ b/tests/test_runtime/test_tracking_logger.py @@ -73,11 +73,31 @@ def test_tracker_metadata_redacts_credentials(self): wandb_key="secret", auth_token="also-secret", wandb_project="specforge", + specforge_config={ + "tracking": {"wandb_key": "nested-secret"}, + "model": { + "embedding_key": "language_model.embed_tokens.weight", + "lm_head_key": "language_model.lm_head.weight", + }, + "data": {"cache_key": "reproduction-cache"}, + }, ) ) self.assertEqual(config["wandb_key"], "") self.assertEqual(config["auth_token"], "") self.assertEqual(config["wandb_project"], "specforge") + self.assertEqual( + config["specforge_config"]["tracking"]["wandb_key"], + "", + ) + self.assertEqual( + config["specforge_config"]["model"]["embedding_key"], + "language_model.embed_tokens.weight", + ) + self.assertEqual( + config["specforge_config"]["data"]["cache_key"], + "reproduction-cache", + ) def test_normalizes_scalars_and_expands_vectors(self): self.assertEqual( From 3e27ad263d41536faea00265900ded767941b95a Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 10:52:36 -0700 Subject: [PATCH 17/88] fix(runtime): stream large online prompt datasets --- specforge/data/prompt_builder.py | 121 ++++++++++----- specforge/launch.py | 139 ++++++++++++------ specforge/training/assembly.py | 4 +- tests/test_data/test_prompt_builder.py | 10 +- tests/test_runtime/test_disagg_multiserver.py | 29 ++++ 5 files changed, 213 insertions(+), 90 deletions(-) diff --git a/specforge/data/prompt_builder.py b/specforge/data/prompt_builder.py index 74df210cb..4a17c5883 100644 --- a/specforge/data/prompt_builder.py +++ b/specforge/data/prompt_builder.py @@ -30,14 +30,16 @@ def prepare_prompt_tasks( num_proc: int | None, min_loss_tokens: int = 1, max_prompts: int | None = None, -) -> list[PromptTaskDict]: +) -> Sequence[PromptTaskDict]: """Prepare runtime prompt dictionaries from a JSONL file. Each returned item has the control-plane shape ``{"payload": {"input_ids": [...], "loss_mask": [...]}}`` and contains no tensors. Files whose first record contains ``input_ids`` and ``loss_mask`` are treated as pre-tokenized. Other files are treated as raw conversation - data and processed through :func:`build_eagle3_dataset`. + data and processed through :func:`build_eagle3_dataset`, then exposed as a + lazy random-access sequence so large Arrow datasets are not expanded into + Python token lists before rollout starts. ``max_prompts`` caps accepted prompts; ``None`` and ``0`` mean no cap. """ @@ -105,7 +107,7 @@ def _prepare_raw_prompts( num_proc: int | None, min_loss_tokens: int, limit: int | None, -) -> list[PromptTaskDict]: +) -> Sequence[PromptTaskDict]: try: from datasets import load_dataset except ImportError as exc: # pragma: no cover - package dependency in production @@ -131,18 +133,46 @@ def _prepare_raw_prompts( train_only_last_turn=train_only_last_turn, minimum_valid_tokens=min_loss_tokens, ) - rows = ( - (record, f"processed dataset row {index}") - for index, record in enumerate(processed_dataset) - ) - return _materialize_prompt_tasks( - rows, + return _ProcessedPromptSequence( + processed_dataset, max_length=max_length, min_loss_tokens=min_loss_tokens, - limit=limit, ) +class _ProcessedPromptSequence(Sequence[PromptTaskDict]): + """Normalize memory-mapped processed rows only when the producer ingests them.""" + + def __init__(self, dataset, *, max_length: int, min_loss_tokens: int) -> None: + self._dataset = dataset + self._max_length = max_length + self._min_loss_tokens = min_loss_tokens + + def __len__(self) -> int: + return len(self._dataset) + + def __iter__(self) -> Iterator[PromptTaskDict]: + for index in range(len(self)): + yield self[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return [self[item] for item in range(*index.indices(len(self)))] + record = self._dataset[index] + prompt = _prompt_from_record( + record, + source=f"processed dataset row {index}", + max_length=self._max_length, + min_loss_tokens=self._min_loss_tokens, + ) + if prompt is None: + raise ValueError( + f"processed dataset row {index} violates the preprocessing " + f"minimum of {self._min_loss_tokens} trainable tokens" + ) + return prompt + + def _materialize_prompt_tasks( rows: Iterable[tuple[Mapping[str, Any], str]], *, @@ -152,41 +182,56 @@ def _materialize_prompt_tasks( ) -> list[PromptTaskDict]: prompts: list[PromptTaskDict] = [] for record, source in rows: - if "input_ids" not in record or "loss_mask" not in record: - raise ValueError(f"{source} must contain both input_ids and loss_mask") - - input_ids = _normalize_integer_sequence( - record["input_ids"], field="input_ids", source=source, binary=False - ) - loss_mask = _normalize_integer_sequence( - record["loss_mask"], field="loss_mask", source=source, binary=True + prompt = _prompt_from_record( + record, + source=source, + max_length=max_length, + min_loss_tokens=min_loss_tokens, ) - if len(input_ids) != len(loss_mask): - raise ValueError( - f"{source} has mismatched input_ids/loss_mask lengths: " - f"{len(input_ids)} != {len(loss_mask)}" - ) - if not input_ids: - raise ValueError(f"{source} contains an empty token sequence") - - input_ids = input_ids[:max_length] - loss_mask = loss_mask[:max_length] - if sum(loss_mask) < min_loss_tokens: + if prompt is None: continue - - prompts.append( - { - "payload": { - "input_ids": input_ids, - "loss_mask": loss_mask, - } - } - ) + prompts.append(prompt) if limit is not None and len(prompts) >= limit: break return prompts +def _prompt_from_record( + record: Mapping[str, Any], + *, + source: str, + max_length: int, + min_loss_tokens: int, +) -> PromptTaskDict | None: + if "input_ids" not in record or "loss_mask" not in record: + raise ValueError(f"{source} must contain both input_ids and loss_mask") + + input_ids = _normalize_integer_sequence( + record["input_ids"], field="input_ids", source=source, binary=False + ) + loss_mask = _normalize_integer_sequence( + record["loss_mask"], field="loss_mask", source=source, binary=True + ) + if len(input_ids) != len(loss_mask): + raise ValueError( + f"{source} has mismatched input_ids/loss_mask lengths: " + f"{len(input_ids)} != {len(loss_mask)}" + ) + if not input_ids: + raise ValueError(f"{source} contains an empty token sequence") + + input_ids = input_ids[:max_length] + loss_mask = loss_mask[:max_length] + if sum(loss_mask) < min_loss_tokens: + return None + return { + "payload": { + "input_ids": input_ids, + "loss_mask": loss_mask, + } + } + + def _normalize_integer_sequence( value: Any, *, diff --git a/specforge/launch.py b/specforge/launch.py index e2da828cd..44f1afe60 100644 --- a/specforge/launch.py +++ b/specforge/launch.py @@ -409,28 +409,55 @@ def _epoch_online_prompts( seed: int = 0, ): """Build one deterministic, epoch-specific online prompt plan.""" + return [ + _epoch_online_prompt(prompts[index], index, epoch, prompt_epochs) + for index in _epoch_prompt_indices(prompts, epoch, seed=seed) + ] + + +def _epoch_prompt_indices(prompts, epoch: int, *, seed: int = 0): + """Return the legacy deterministic shuffle without materializing payloads.""" import random - indexed_prompts = list(enumerate(prompts)) - random.Random(int(seed) + int(epoch)).shuffle(indexed_prompts) + indices = list(range(len(prompts))) + random.Random(int(seed) + int(epoch)).shuffle(indices) + return indices + + +def _epoch_online_prompt(prompt, index: int, epoch: int, prompt_epochs: int): + """Apply epoch identity while preserving the single-epoch prompt shape.""" if prompt_epochs == 1: - return [prompt for _idx, prompt in indexed_prompts] - - out = [] - for idx, prompt in indexed_prompts: - item = dict(prompt) - metadata = dict(prompt.get("metadata") or {}) - if "task_id" in prompt: - metadata.setdefault("base_task_id", str(prompt["task_id"])) - metadata["prompt_index"] = idx - metadata["epoch"] = epoch - metadata["prompt_epochs"] = prompt_epochs - item["metadata"] = metadata - # The online feature store is consume-once and commit dedups by - # sample_id, so every epoch pass must mint distinct task/sample ids. - item["task_id"] = f"epoch{epoch:04d}-prompt{idx:012d}" - out.append(item) - return out + return prompt + + item = dict(prompt) + metadata = dict(prompt.get("metadata") or {}) + if "task_id" in prompt: + metadata.setdefault("base_task_id", str(prompt["task_id"])) + metadata["prompt_index"] = index + metadata["epoch"] = epoch + metadata["prompt_epochs"] = prompt_epochs + item["metadata"] = metadata + # The online feature store is consume-once and commit dedups by + # sample_id, so every epoch pass must mint distinct task/sample ids. + item["task_id"] = f"epoch{epoch:04d}-prompt{index:012d}" + return item + + +def _iter_epoch_online_prompt_batches( + prompts, + epoch: int, + prompt_epochs: int, + *, + seed: int = 0, + batch_size: int = 4096, +): + """Yield a shuffled epoch while bounding expanded token-list residency.""" + indices = _epoch_prompt_indices(prompts, epoch, seed=seed) + for start in range(0, len(indices), batch_size): + yield [ + _epoch_online_prompt(prompts[index], index, epoch, prompt_epochs) + for index in indices[start : start + batch_size] + ] def _assemble_server_rollout_workers( @@ -791,6 +818,7 @@ def build_disagg_online_producer( sleep=None, prompt_epochs: int = 1, prompt_seed: int = 0, + prompt_ingest_batch_size: int = 4096, ): """Producer side of an ONLINE disaggregated run (rollout pool). @@ -814,7 +842,9 @@ def build_disagg_online_producer( ``prompt_epochs`` repeats the prompt stream on the producer side by minting epoch-tagged task/sample ids. Each pass uses the deterministic ``prompt_seed + epoch`` order, matching sampler-style epoch semantics while - keeping a reconstructed plan stable across restarts. + keeping a reconstructed plan stable across restarts. Prompt payloads are + normalized and ingested in ``prompt_ingest_batch_size`` chunks so a large + memory-mapped dataset does not expand every token list before rollout. Failure semantics: a worker whose source raises (dead/unreachable server) has already failed its leases retryable — the surviving workers re-lease @@ -870,6 +900,9 @@ def elapsed(start: float) -> str: producer_concurrency = int(producer_concurrency) if producer_concurrency < 1: raise ValueError("producer_concurrency must be >= 1") + prompt_ingest_batch_size = int(prompt_ingest_batch_size) + if prompt_ingest_batch_size < 1: + raise ValueError("prompt_ingest_batch_size must be >= 1") flow_control = ProducerFlowControl( FlowControlLimits( high_watermark_refs=in_flight_high_watermark, @@ -896,14 +929,15 @@ def elapsed(start: float) -> str: worker_lease = flow_control.prompt_lease(lease) build_start = time.perf_counter() prompt_epochs = _normalize_prompt_epochs(prompt_epochs) - if prompt_epochs > 1: + if not hasattr(prompts, "__len__") or not hasattr(prompts, "__getitem__"): prompts = list(prompts) - base_prompt_count = len(prompts) if hasattr(prompts, "__len__") else "unknown" + base_prompt_count = len(prompts) producer_timing( "build_disagg_online_producer enter " f"algorithm={algorithm.name} modality={modality} " f"base_prompts={base_prompt_count} " f"prompt_epochs={prompt_epochs} " + f"prompt_ingest_batch_size={prompt_ingest_batch_size} " f"lease={worker_lease} workers={num_rollout_workers} " f"concurrency={producer_concurrency} " f"watermarks={in_flight_high_watermark}/" @@ -1265,26 +1299,19 @@ def run_worker(w) -> None: abort_unpublished(futures) raise - def ingest_epoch(epoch: int) -> None: - epoch_prompts = _epoch_online_prompts( - prompts, - epoch, - prompt_epochs, - seed=prompt_seed, - ) - epoch_count = ( - len(epoch_prompts) if hasattr(epoch_prompts, "__len__") else "unknown" - ) + def ingest_prompt_batch(epoch: int, batch_index: int, epoch_prompts) -> None: phase = time.perf_counter() producer_timing( "controller.ingest_prompts start " - f"epoch={epoch + 1}/{prompt_epochs} prompts={epoch_count}" + f"epoch={epoch + 1}/{prompt_epochs} batch={batch_index + 1} " + f"prompts={len(epoch_prompts)}" ) task_ids = controller.ingest_prompts(epoch_prompts) status = controller.status() producer_timing( "controller.ingest_prompts done " - f"epoch={epoch + 1}/{prompt_epochs} tasks={len(task_ids)} " + f"epoch={epoch + 1}/{prompt_epochs} batch={batch_index + 1} " + f"tasks={len(task_ids)} " f"pending={status['prompts_pending']} elapsed={elapsed(phase)}" ) @@ -1321,21 +1348,35 @@ def run_worker_guarded(w) -> None: for epoch in range(prompt_epochs): if should_stop is not None and should_stop(): break - ingest_epoch(epoch) - if not live_workers: - raise RuntimeError( - f"all rollout workers were already dropped before " - f"epoch {epoch + 1}/{prompt_epochs} could run — " - f"dead workers: {dead}" - ) - run_epoch_workers(live_workers) - stopped = should_stop is not None and should_stop() - live_workers = [w for w in live_workers if w.worker_id not in dead] - if dead and not stopped and not pool_drained(): - raise RuntimeError( - f"all rollout workers exited with {len(dead)} dropped as " - f"dead and prompts remaining — dead workers: {dead}" - ) + epoch_batches = _iter_epoch_online_prompt_batches( + prompts, + epoch, + prompt_epochs, + seed=prompt_seed, + batch_size=prompt_ingest_batch_size, + ) + stopped = False + for batch_index, prompt_batch in enumerate(epoch_batches): + if should_stop is not None and should_stop(): + stopped = True + break + ingest_prompt_batch(epoch, batch_index, prompt_batch) + if not live_workers: + raise RuntimeError( + f"all rollout workers were already dropped before " + f"epoch {epoch + 1}/{prompt_epochs} batch " + f"{batch_index + 1} could run — dead workers: {dead}" + ) + run_epoch_workers(live_workers) + stopped = should_stop is not None and should_stop() + live_workers = [w for w in live_workers if w.worker_id not in dead] + if dead and not stopped and not pool_drained(): + raise RuntimeError( + f"all rollout workers exited with {len(dead)} dropped " + f"as dead and prompts remaining — dead workers: {dead}" + ) + if stopped: + break if stopped: break st = controller.status() diff --git a/specforge/training/assembly.py b/specforge/training/assembly.py index ac64b4f2d..ba28d0e08 100644 --- a/specforge/training/assembly.py +++ b/specforge/training/assembly.py @@ -30,7 +30,7 @@ import os from collections import Counter from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Mapping, Optional +from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence from specforge.algorithms.contracts import FeatureMode from specforge.algorithms.registry import AlgorithmRegistration @@ -360,7 +360,7 @@ def _prepare_prompts( draft_config, path: Optional[str] = None, cache_key: Optional[str] = None, -) -> List[dict]: +) -> Sequence[dict]: """Prepare one prompt source with an optional path/cache namespace override. Training keeps the configured cache key. Evaluation supplies its own path diff --git a/tests/test_data/test_prompt_builder.py b/tests/test_data/test_prompt_builder.py index 5182618bc..45ef5f11a 100644 --- a/tests/test_data/test_prompt_builder.py +++ b/tests/test_data/test_prompt_builder.py @@ -12,6 +12,7 @@ class _FakeDataset: def __init__(self, rows): self.rows = list(rows) + self.getitem_calls = 0 def __iter__(self): return iter(self.rows) @@ -19,6 +20,10 @@ def __iter__(self): def __len__(self): return len(self.rows) + def __getitem__(self, index): + self.getitem_calls += 1 + return self.rows[index] + def select(self, indices): return _FakeDataset(self.rows[index] for index in indices) @@ -141,8 +146,11 @@ def fake_build_eagle3_dataset(**kwargs): max_prompts=1, ) + self.assertEqual(processed_dataset.getitem_calls, 0) + materialized_prompts = list(prompts) + self.assertEqual(processed_dataset.getitem_calls, 1) self.assertEqual( - prompts, + materialized_prompts, [ { "payload": { diff --git a/tests/test_runtime/test_disagg_multiserver.py b/tests/test_runtime/test_disagg_multiserver.py index d901c53b1..5110d6118 100644 --- a/tests/test_runtime/test_disagg_multiserver.py +++ b/tests/test_runtime/test_disagg_multiserver.py @@ -596,6 +596,35 @@ def test_prompt_epochs_republish_with_unique_sample_ids(self): ) self.assertTrue(channel.is_closed()) + def test_prompt_ingest_chunks_preserve_epoch_ids_and_release_payloads(self): + backend = _FakeMooncakeStore() + stub = _StubCaptureServer(backend) + store = MooncakeFeatureStore(store=backend, store_id="run0") + N, E = 5, 2 + channel = StreamingRefChannel(os.path.join(self._workdir(), "refs.jsonl")) + workers, drive = _build( + [_adapter(store, stub)], + _prompts(N), + store, + channel, + lease=2, + prompt_epochs=E, + prompt_ingest_batch_size=2, + ) + + produced = drive() + + self.assertEqual(produced, N * E) + self.assertEqual(workers[0].controller.status()["prompts"], 0) + self.assertEqual( + set(_published_sample_ids(channel.path)), + { + f"run0:epoch{epoch:04d}-prompt{idx:012d}" + for epoch in range(E) + for idx in range(N) + }, + ) + def test_prompt_epoch_order_is_seeded_and_reconstruction_stable(self): prompts = _prompts(12) From bfb662be96eceab52adc0f309004aee405889320 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 11:09:07 -0700 Subject: [PATCH 18/88] fix(model): compile long Kimi K3 MLA attention --- specforge/modeling/draft/kimi_k3_dspark.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/specforge/modeling/draft/kimi_k3_dspark.py b/specforge/modeling/draft/kimi_k3_dspark.py index 188095c03..989c59319 100644 --- a/specforge/modeling/draft/kimi_k3_dspark.py +++ b/specforge/modeling/draft/kimi_k3_dspark.py @@ -35,6 +35,7 @@ from .dflash import build_target_layer_ids, normalize_draft_head_checkpoint_keys from .dspark import DSparkDraftModel +from .flex_attention import compile_friendly_flex_attention from .registry import register_draft @@ -196,7 +197,14 @@ def forward( v_attn = kv_latent.unsqueeze(1) if isinstance(attention_mask, BlockMask): - latent_out = flex_attention( + # Eager FlexAttention falls back to the O(N^2) math kernel. At the + # production 4K sequence length that materializes tens of GiB of + # score tensors per sample. Match the established Eagle3 path and + # compile the block-sparse kernel for long sequences. + flex_attention_func = ( + flex_attention if query_len <= 128 else compile_friendly_flex_attention + ) + latent_out = flex_attention_func( q_attn, k_attn, v_attn, From c76ca3ce034c2471f738a84173d6d1ae6cea0b96 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 11:15:32 -0700 Subject: [PATCH 19/88] fix(model): fuse expanded long Kimi MLA attention --- specforge/modeling/draft/kimi_k3_dspark.py | 67 ++++++++++++------- .../test_kimi_k3_dspark_architectures.py | 23 +++++++ 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/specforge/modeling/draft/kimi_k3_dspark.py b/specforge/modeling/draft/kimi_k3_dspark.py index 989c59319..89a1bcf68 100644 --- a/specforge/modeling/draft/kimi_k3_dspark.py +++ b/specforge/modeling/draft/kimi_k3_dspark.py @@ -191,39 +191,56 @@ def forward( self.kv_lora_rank, ) w_kc, w_vc = kv_b.split([self.qk_nope_head_dim, self.v_head_dim], dim=1) - q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) - q_attn = torch.cat((q_absorbed, q_rope), dim=-1).transpose(1, 2) - k_attn = torch.cat((kv_latent.unsqueeze(1), k_rope), dim=-1) - v_attn = kv_latent.unsqueeze(1) - - if isinstance(attention_mask, BlockMask): - # Eager FlexAttention falls back to the O(N^2) math kernel. At the - # production 4K sequence length that materializes tens of GiB of - # score tensors per sample. Match the established Eagle3 path and - # compile the block-sparse kernel for long sequences. - flex_attention_func = ( - flex_attention if query_len <= 128 else compile_friendly_flex_attention + if isinstance(attention_mask, BlockMask) and query_len > 128: + # The absorbed MLA form has q/k head dim kv_lora_rank + rope_dim + # (576 for K3), which exceeds Triton's shared-memory budget for the + # fused FlexAttention kernel. Expand the two linear projections + # around attention instead. This is algebraically equivalent: + # (q W_k) @ c == q @ (W_k c) + # softmax(scores) c W_v == softmax(scores) (c W_v) + # and reduces the fused q/k head dim to K3's native 192 while + # retaining a 128-dim value. It allocates linear K/V projections, + # but never the quadratic score matrix used by eager FlexAttention. + q_attn = torch.cat((q_nope, q_rope), dim=-1).transpose(1, 2) + k_nope = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc).transpose(1, 2) + k_attn = torch.cat( + (k_nope, k_rope.expand(-1, self.num_heads, -1, -1)), + dim=-1, ) - latent_out = flex_attention_func( + v_attn = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc).transpose(1, 2) + attn_out = compile_friendly_flex_attention( q_attn, k_attn, v_attn, block_mask=attention_mask, scale=self.scaling, - enable_gqa=True, - ) + ).transpose(1, 2) else: - latent_out = F.scaled_dot_product_attention( - q_attn, - k_attn, - v_attn, - attn_mask=attention_mask, - dropout_p=0.0, - scale=self.scaling, - enable_gqa=True, - ) + q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) + q_attn = torch.cat((q_absorbed, q_rope), dim=-1).transpose(1, 2) + k_attn = torch.cat((kv_latent.unsqueeze(1), k_rope), dim=-1) + v_attn = kv_latent.unsqueeze(1) + if isinstance(attention_mask, BlockMask): + latent_out = flex_attention( + q_attn, + k_attn, + v_attn, + block_mask=attention_mask, + scale=self.scaling, + enable_gqa=True, + ) + else: + latent_out = F.scaled_dot_product_attention( + q_attn, + k_attn, + v_attn, + attn_mask=attention_mask, + dropout_p=0.0, + scale=self.scaling, + enable_gqa=True, + ) + attn_out = torch.einsum("bhqk,hvk->bqhv", latent_out, w_vc) - attn_out = torch.einsum("bhqk,hvk->bqhv", latent_out, w_vc) attn_out = attn_out.reshape(batch, query_len, -1) if self.use_output_gate: attn_out = attn_out * torch.sigmoid(self.g_proj(hidden_states)) diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index 3e6c7a06c..52ac0ca6b 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -178,6 +178,29 @@ def test_tiny_5mla_forward_and_backward(): assert all(layer.self_attn.use_output_gate for layer in model.layers) +def test_mla_absorbed_and_expanded_attention_are_algebraically_equivalent(): + torch.manual_seed(7) + batch, queries, keys, heads = 2, 3, 5, 4 + nope_dim, latent_dim, value_dim = 6, 8, 7 + q_nope = torch.randn(batch, queries, heads, nope_dim, dtype=torch.float64) + kv_latent = torch.randn(batch, keys, latent_dim, dtype=torch.float64) + w_kc = torch.randn(heads, nope_dim, latent_dim, dtype=torch.float64) + w_vc = torch.randn(heads, value_dim, latent_dim, dtype=torch.float64) + + q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) + absorbed_scores = torch.einsum("bqhk,bsk->bhqs", q_absorbed, kv_latent) + k_expanded = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc) + expanded_scores = torch.einsum("bqhd,bshd->bhqs", q_nope, k_expanded) + torch.testing.assert_close(absorbed_scores, expanded_scores) + + probabilities = absorbed_scores.softmax(dim=-1) + latent_output = torch.einsum("bhqs,bsk->bhqk", probabilities, kv_latent) + absorbed_output = torch.einsum("bhqk,hvk->bqhv", latent_output, w_vc) + v_expanded = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc) + expanded_output = torch.einsum("bhqs,bshv->bqhv", probabilities, v_expanded) + torch.testing.assert_close(absorbed_output, expanded_output) + + def test_tiny_4kda_1mla_forward_and_backward(): model = KimiK3DSpark4KDA1MLADraftModel( _tiny_config("KimiK3DSpark4KDA1MLADraftModel") From 16e4d74c420e4a815c03d71a8a1270d3facd183f Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 11:51:00 -0700 Subject: [PATCH 20/88] fix(runtime): reclaim durable Mooncake samples per step --- specforge/runtime/control_plane/dp_ack.py | 15 ++++++ specforge/runtime/data_plane/__init__.py | 2 + specforge/runtime/data_plane/feature_store.py | 35 ++++++++++++ .../runtime/data_plane/mooncake_store.py | 54 +++++++++++++++++-- tests/test_runtime/test_mooncake_store.py | 44 +++++++++++++++ tests/test_runtime/test_ref_distributor.py | 26 +++++++++ 6 files changed, 173 insertions(+), 3 deletions(-) diff --git a/specforge/runtime/control_plane/dp_ack.py b/specforge/runtime/control_plane/dp_ack.py index 94a6b95eb..ae70a27ea 100644 --- a/specforge/runtime/control_plane/dp_ack.py +++ b/specforge/runtime/control_plane/dp_ack.py @@ -175,6 +175,21 @@ def ack_train_refs( ) except BaseException as exc: failures.append(f"{sample_id}: {type(exc).__name__}: {exc}") + try: + # Mooncake read leases can defer abort()'s physical removal for + # minutes. Once this exact optimizer window is durable, force + # only its rank-local ids; a store-wide drain could delete + # prefetched refs that still need crash replay. + from specforge.runtime.data_plane.feature_store import ( + drain_feature_store_sample_removals, + ) + + drain_feature_store_sample_removals(self.feature_store, local_ids) + except BaseException as exc: + failures.append( + "optimizer-boundary selective drain: " + f"{type(exc).__name__}: {exc}" + ) if failures: cleanup_error = ", ".join(failures) cleanup_error = self._sync_cleanup_error(cleanup_error) diff --git a/specforge/runtime/data_plane/__init__.py b/specforge/runtime/data_plane/__init__.py index b0592f482..90eaf9b5f 100644 --- a/specforge/runtime/data_plane/__init__.py +++ b/specforge/runtime/data_plane/__init__.py @@ -11,6 +11,7 @@ "FeatureStore", "LocalFeatureStore", "drain_feature_store_removals", + "drain_feature_store_sample_removals", "load_feature_file", "spec_from_tensor", "SampleRefQueue", @@ -27,6 +28,7 @@ "FeatureStore": "feature_store", "LocalFeatureStore": "feature_store", "drain_feature_store_removals": "feature_store", + "drain_feature_store_sample_removals": "feature_store", "load_feature_file": "feature_store", "spec_from_tensor": "feature_store", "SampleRefQueue": "sample_ref_queue", diff --git a/specforge/runtime/data_plane/feature_store.py b/specforge/runtime/data_plane/feature_store.py index eab5cda57..e362e837d 100644 --- a/specforge/runtime/data_plane/feature_store.py +++ b/specforge/runtime/data_plane/feature_store.py @@ -198,6 +198,40 @@ def drain_feature_store_removals( return {"removed": 0, "removed_bytes": 0, "release_pending": 0} +def drain_feature_store_sample_removals( + store: FeatureStore, + sample_ids: List[str], + *, + max_attempts: int = 8, + retry_interval_s: float = 0.25, + sleep: Callable[[float], None] = time.sleep, +) -> Dict[str, int]: + """Physically reclaim only optimizer-durable samples from a remote store. + + A streaming loader can have removal-pending objects from prefetched batches + that have not reached an optimizer boundary yet. Draining the store-wide + pending set at an acknowledgement boundary would delete their crash-replay + source too early. Backends that need lease-authority removal therefore + expose a selective hook; synchronously-freeing stores need no extra work. + """ + if max_attempts < 1: + raise ValueError("max_attempts must be >= 1") + if retry_interval_s < 0: + raise ValueError("retry_interval_s must be >= 0") + ids = list(dict.fromkeys(sample_ids)) + if not ids: + return {"removed": 0, "removed_bytes": 0, "release_pending": 0} + drain = getattr(store, "drain_sample_removals", None) + if not callable(drain): + return {"removed": 0, "removed_bytes": 0, "release_pending": 0} + return drain( + ids, + max_attempts=max_attempts, + retry_interval_s=retry_interval_s, + sleep=sleep, + ) + + def load_feature_file(path: str) -> Dict[str, torch.Tensor]: """Load one prepared SpecForge offline feature file.""" if path.endswith(".gz"): @@ -596,6 +630,7 @@ def health(self) -> Dict[str, Any]: "FeatureStore", "LocalFeatureStore", "drain_feature_store_removals", + "drain_feature_store_sample_removals", "load_feature_file", "spec_from_tensor", ] diff --git a/specforge/runtime/data_plane/mooncake_store.py b/specforge/runtime/data_plane/mooncake_store.py index 62b28ae37..ac3e9cdcc 100644 --- a/specforge/runtime/data_plane/mooncake_store.py +++ b/specforge/runtime/data_plane/mooncake_store.py @@ -650,6 +650,26 @@ def abort(self, sample_id: str, *, reason: str = "aborted") -> None: else: self._release_pending.setdefault(sample_id, 0) + def drain_sample_removals( + self, + sample_ids: List[str], + *, + max_attempts: int = 8, + retry_interval_s: float = 0.25, + sleep: Callable[[float], None] = time.sleep, + ) -> Dict[str, int]: + """Force-remove only the named optimizer-durable samples. + + Other pending samples may belong to prefetched, not-yet-durable + batches and must remain available for crash replay. + """ + return self._drain_removals( + sample_ids=sample_ids, + max_attempts=max_attempts, + retry_interval_s=retry_interval_s, + sleep=sleep, + ) + def drain_pending_removals( self, *, @@ -666,17 +686,37 @@ def drain_pending_removals( ``sleep`` is injectable so protocol tests can advance a fake lease clock without wall-clock delays. """ + return self._drain_removals( + sample_ids=None, + max_attempts=max_attempts, + retry_interval_s=retry_interval_s, + sleep=sleep, + ) + + def _drain_removals( + self, + *, + sample_ids: Optional[List[str]], + max_attempts: int, + retry_interval_s: float, + sleep: Callable[[float], None], + ) -> Dict[str, int]: if max_attempts < 1: raise ValueError("max_attempts must be >= 1") if retry_interval_s < 0: raise ValueError("retry_interval_s must be >= 0") + target_ids = None if sample_ids is None else set(sample_ids) removed = removed_bytes = 0 last_errors: Dict[str, str] = {} attempts_run = 0 for attempt in range(max_attempts): attempts_run = attempt + 1 with self._lock: - pending = list(self._release_pending) + pending = [ + sample_id + for sample_id in self._release_pending + if target_ids is None or sample_id in target_ids + ] if not pending: return { "removed": removed, @@ -716,7 +756,11 @@ def drain_pending_removals( self.max_release_attempts, self._release_pending.get(sample_id, 0) + 1, ) - remaining = list(self._release_pending) + remaining = [ + sample_id + for sample_id in self._release_pending + if target_ids is None or sample_id in target_ids + ] if not remaining: return { "removed": removed, @@ -728,7 +772,11 @@ def drain_pending_removals( sleep(retry_interval_s) with self._lock: - remaining = list(self._release_pending) + remaining = [ + sample_id + for sample_id in self._release_pending + if target_ids is None or sample_id in target_ids + ] preview = remaining[:16] detail = f"; last errors={last_errors}" if last_errors else "" raise RuntimeError( diff --git a/tests/test_runtime/test_mooncake_store.py b/tests/test_runtime/test_mooncake_store.py index a10cf3636..9460e178d 100644 --- a/tests/test_runtime/test_mooncake_store.py +++ b/tests/test_runtime/test_mooncake_store.py @@ -15,6 +15,7 @@ import torch from specforge.runtime.control_plane.controller import DataFlowController +from specforge.runtime.control_plane.dp_ack import DPAckController from specforge.runtime.control_plane.metadata_store import InMemoryMetadataStore from specforge.runtime.data_plane.disaggregated import AuthPolicy from specforge.runtime.data_plane.feature_store import ( @@ -299,6 +300,49 @@ def remove(self, key, force=False): self.assertEqual(fake.force_values[:num_features], [False] * num_features) self.assertEqual(fake.force_values[num_features:], [True] * num_features) + def test_optimizer_ack_forces_only_durable_samples(self): + class ForceAwareFake(_FakeMooncakeStore): + def __init__(self): + super().__init__() + self.force_values = [] + + def remove(self, key, force=False): + self.remove_calls += 1 + self.force_values.append((key, force)) + if not force: + return -706 + self._d.pop(key, None) + return 0 + + fake = ForceAwareFake() + fs = MooncakeFeatureStore(store=fake, store_id="run0") + durable = fs.put(_tensors(), sample_id="durable", metadata=_meta()) + prefetched = fs.put(_tensors(), sample_id="prefetched", metadata=_meta()) + for ref in (durable, prefetched): + _, handle = fs.get(ref) + fs.release(handle) + self.assertEqual(fs.health()["release_pending"], 2) + + controller = DPAckController( + "run0", + feature_store=fs, + metadata_store=InMemoryMetadataStore(), + ) + controller.commit_samples("distributor", [durable, prefetched]) + controller.ack_train_refs( + "trainer", + [durable.sample_id], + global_step=1, + optimizer_durable=True, + ) + + self.assertFalse(_phys_resident(fake, sid="durable")) + self.assertTrue(_phys_resident(fake, sid="prefetched")) + self.assertEqual(fs.health()["release_pending"], 1) + marker = controller.store.durable_marker() + self.assertEqual(marker["global_step"], 1) + self.assertEqual(marker["acked"], {"durable"}) + def test_lifecycle_drain_does_not_renew_read_lease_between_retries(self): clock = _FakeClock() lease_ttl = 1.0 diff --git a/tests/test_runtime/test_ref_distributor.py b/tests/test_runtime/test_ref_distributor.py index a55510369..c6f7d4b52 100644 --- a/tests/test_runtime/test_ref_distributor.py +++ b/tests/test_runtime/test_ref_distributor.py @@ -732,6 +732,32 @@ def abort(self, sample_id, *, reason): self.assertEqual(reason, "optimizer-boundary-durable-ack") controller.store.close() + def test_selective_drain_failure_is_reported_after_durable_commit(self): + class FeatureStore: + def abort(self, sample_id, *, reason): + pass + + def drain_sample_removals(self, sample_ids, **kwargs): + raise OSError(f"remove stayed pinned for {sample_ids}") + + controller = DPAckController( + "run0", + is_authority=True, + feature_store=FeatureStore(), + metadata_store=SQLiteMetadataStore(os.path.join(self.dir, "drain.db")), + ) + controller.commit_samples("w0", [_ref("s0")]) + with self.assertRaisesRegex( + RuntimeError, "optimizer-boundary selective drain.*remove stayed pinned" + ): + controller.ack_train_refs( + "t0", ["s0"], global_step=1, optimizer_durable=True + ) + marker = controller.store.durable_marker() + self.assertEqual(marker["global_step"], 1) + self.assertTrue(marker["optimizer_durable"]) + controller.store.close() + def test_non_authority_participates_but_records_nothing(self): calls = [] From 108eda37c0a671b6fdc5d86b62aa9ffedfc93ffd Mon Sep 17 00:00:00 2001 From: Yi Sun Date: Sat, 1 Aug 2026 18:52:38 +0000 Subject: [PATCH 21/88] fix(dspark): checkpoint config carries BOTH rope schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transformers 5.x save_pretrained keeps only the new rope_parameters schema and drops legacy rope_scaling. Serving stacks and older transformers that read only the legacy key then silently lose YaRN on long-context checkpoints (the draft falls back to unscaled RoPE at serve time — accept length collapses beyond the original context). Mirror whichever schema survives into the other at save time; default (non-scaled) RoPE configs are left untouched. --- specforge/export/checkpoint_io.py | 66 +++++++++++++++++++++++++++++- specforge/export/to_hf.py | 7 +++- specforge/export/to_sglang.py | 7 +++- tests/test_runtime/test_export.py | 68 +++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/specforge/export/checkpoint_io.py b/specforge/export/checkpoint_io.py index 2c294c0f9..f2fefe39d 100644 --- a/specforge/export/checkpoint_io.py +++ b/specforge/export/checkpoint_io.py @@ -11,6 +11,7 @@ from __future__ import annotations import glob +import json import os import re from typing import Any, Dict, Optional @@ -18,6 +19,64 @@ import torch STATE_FILE = "training_state.pt" +_DISABLE_LEGACY_ROPE_SCALING_ENV = "SPECFORGE_DISABLE_LEGACY_ROPE_SCALING" + + +def _env_flag_enabled(name: str) -> bool: + value = os.environ.get(name) + if value is None: + return False + return value.strip().lower() in {"1", "true", "yes", "on"} + + +def apply_legacy_rope_scaling(output_dir: str) -> bool: + """Keep modern and legacy RoPE scaling fields compatible in an export. + + Transformers 5 writes ``rope_parameters`` while older serving stacks read + only ``rope_scaling``. When exactly one non-default representation is + present, write the other. On by default; set + ``SPECFORGE_DISABLE_LEGACY_ROPE_SCALING=1`` to skip. Returns whether the + config was rewritten. + """ + if _env_flag_enabled(_DISABLE_LEGACY_ROPE_SCALING_ENV): + return False + + config_path = os.path.join(output_dir, "config.json") + with open(config_path, encoding="utf-8") as handle: + config = json.load(handle) + + rope_parameters = config.get("rope_parameters") + rope_scaling = config.get("rope_scaling") + + def rope_kind(payload): + return (payload or {}).get("rope_type") or (payload or {}).get("type") + + if ( + rope_parameters + and not rope_scaling + and rope_kind(rope_parameters) not in (None, "default") + ): + config["rope_scaling"] = { + key: value for key, value in rope_parameters.items() if key != "rope_theta" + } + elif ( + rope_scaling + and not rope_parameters + and rope_kind(rope_scaling) not in (None, "default") + ): + mirrored = dict(rope_scaling) + if "rope_theta" in config: + mirrored.setdefault("rope_theta", config["rope_theta"]) + config["rope_parameters"] = mirrored + else: + return False + + temporary = f"{config_path}.{os.getpid()}.tmp" + with open(temporary, "w", encoding="utf-8") as handle: + json.dump(config, handle, indent=2, sort_keys=True) + handle.write("\n") + os.replace(temporary, config_path) + return True def resolve_training_state(checkpoint_path: str) -> Dict[str, Any]: @@ -111,4 +170,9 @@ def materialize_draft( return model -__all__ = ["resolve_training_state", "materialize_draft", "STATE_FILE"] +__all__ = [ + "apply_legacy_rope_scaling", + "resolve_training_state", + "materialize_draft", + "STATE_FILE", +] diff --git a/specforge/export/to_hf.py b/specforge/export/to_hf.py index 4bea4067f..71be220d7 100644 --- a/specforge/export/to_hf.py +++ b/specforge/export/to_hf.py @@ -26,7 +26,11 @@ from huggingface_hub import snapshot_download from safetensors import safe_open -from specforge.export.checkpoint_io import materialize_draft, resolve_training_state +from specforge.export.checkpoint_io import ( + materialize_draft, + apply_legacy_rope_scaling, + resolve_training_state, +) def _load_embedding_tensor(source: str, key: str) -> torch.Tensor: @@ -110,6 +114,7 @@ def export_to_hf( ) full_state.update(state["draft_state_dict"]) # trained keys win model.save_pretrained(output_dir, state_dict=full_state) + apply_legacy_rope_scaling(output_dir) return output_dir diff --git a/specforge/export/to_sglang.py b/specforge/export/to_sglang.py index 1d281f1c8..19c95eea1 100644 --- a/specforge/export/to_sglang.py +++ b/specforge/export/to_sglang.py @@ -23,7 +23,11 @@ import argparse from typing import Dict, Optional -from specforge.export.checkpoint_io import materialize_draft, resolve_training_state +from specforge.export.checkpoint_io import ( + materialize_draft, + apply_legacy_rope_scaling, + resolve_training_state, +) #: per-architecture trainer-key -> serving-key renames ({} = identity). WEIGHT_MAPS: Dict[str, Dict[str, str]] = { @@ -80,6 +84,7 @@ def export_to_sglang( # embeddings exactly as the trainer-side checkpoint filter does. full = {k: v for k, v in model.state_dict().items() if "embed" not in k.lower()} model.save_pretrained(output_dir, state_dict=_serving_state(full, weight_map)) + apply_legacy_rope_scaling(output_dir) return output_dir diff --git a/tests/test_runtime/test_export.py b/tests/test_runtime/test_export.py index c5d7a5dbe..72b5f69e8 100644 --- a/tests/test_runtime/test_export.py +++ b/tests/test_runtime/test_export.py @@ -10,6 +10,7 @@ round trips require GPU and can be run on the H200 box via rcli. """ +import json import os import tempfile import unittest @@ -19,6 +20,73 @@ CUDA = torch.cuda.is_available() +class TestRoPEConfigCompatibility(unittest.TestCase): + def _write_config(self, directory, payload): + path = os.path.join(directory, "config.json") + with open(path, "w", encoding="utf-8") as handle: + json.dump(payload, handle) + return path + + def test_modern_rope_parameters_are_mirrored_for_legacy_readers(self): + from specforge.export.checkpoint_io import apply_legacy_rope_scaling + + with tempfile.TemporaryDirectory() as directory: + path = self._write_config( + directory, + { + "rope_parameters": { + "rope_type": "yarn", + "factor": 128.0, + "rope_theta": 8_000_000, + } + }, + ) + self.assertTrue(apply_legacy_rope_scaling(directory)) + with open(path, encoding="utf-8") as handle: + config = json.load(handle) + + self.assertEqual( + config["rope_scaling"], + {"rope_type": "yarn", "factor": 128.0}, + ) + + def test_legacy_rope_scaling_is_mirrored_for_modern_readers(self): + from specforge.export.checkpoint_io import apply_legacy_rope_scaling + + with tempfile.TemporaryDirectory() as directory: + path = self._write_config( + directory, + { + "rope_theta": 8_000_000, + "rope_scaling": {"type": "yarn", "factor": 128.0}, + }, + ) + self.assertTrue(apply_legacy_rope_scaling(directory)) + with open(path, encoding="utf-8") as handle: + config = json.load(handle) + + self.assertEqual( + config["rope_parameters"], + {"type": "yarn", "factor": 128.0, "rope_theta": 8_000_000}, + ) + + def test_default_rope_config_is_not_rewritten(self): + from specforge.export.checkpoint_io import apply_legacy_rope_scaling + + with tempfile.TemporaryDirectory() as directory: + path = self._write_config( + directory, + {"rope_parameters": {"rope_type": "default"}}, + ) + with open(path, "rb") as handle: + before = handle.read() + self.assertFalse(apply_legacy_rope_scaling(directory)) + with open(path, "rb") as handle: + after = handle.read() + + self.assertEqual(after, before) + + class TestLegacyVocabMappingCompatibility(unittest.TestCase): def setUp(self): from specforge.modeling.auto import AutoDraftModel, AutoDraftModelConfig From dd8b00acce1af60c725a2c4212d36e4e29cef86e Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 13:03:26 -0700 Subject: [PATCH 22/88] fix(dspark): shard production KDA launches --- specforge/modeling/draft/kimi_k3_dspark.py | 71 ++++++++++++------- .../test_kimi_k3_dspark_architectures.py | 50 +++++++++++++ 2 files changed, 97 insertions(+), 24 deletions(-) diff --git a/specforge/modeling/draft/kimi_k3_dspark.py b/specforge/modeling/draft/kimi_k3_dspark.py index 89a1bcf68..699cccfb5 100644 --- a/specforge/modeling/draft/kimi_k3_dspark.py +++ b/specforge/modeling/draft/kimi_k3_dspark.py @@ -38,6 +38,12 @@ from .flex_attention import compile_friendly_flex_attention from .registry import register_draft +# CUDA limits gridDim.z to 65,535. FLA's KDA gate kernel maps one program to +# every (independent proposal block, head) pair on that dimension, so a full +# DSpark optimizer microbatch can exceed the launch limit even though each +# proposal block is only a few tokens long. +_CUDA_MAX_GRID_DIM_Z = 65_535 + def _rotate_half(x: torch.Tensor, *, interleaved: bool) -> torch.Tensor: if interleaved: @@ -359,6 +365,17 @@ def _reference_kda( return torch.stack(outputs, dim=1) +def _load_fla_chunk_kda() -> Callable[..., tuple[torch.Tensor, object]]: + try: + from fla.ops.kda import chunk_kda + except ImportError as exc: + raise ImportError( + "Kimi-K3 KDA training requires fla-core==0.5.1; install " + "SpecForge with the 'kda' extra" + ) from exc + return chunk_kda + + def _fla_kda( q: torch.Tensor, k: torch.Tensor, @@ -369,30 +386,36 @@ def _fla_kda( dt_bias: torch.Tensor, lower_bound: Optional[float], ) -> torch.Tensor: - try: - from fla.ops.kda import chunk_kda - except ImportError as exc: - raise ImportError( - "Kimi-K3 KDA training requires fla-core==0.5.1; install " - "SpecForge with the 'kda' extra" - ) from exc - - output, _ = chunk_kda( - q=q, - k=k, - v=v, - g=raw_gate, - beta=beta, - A_log=A_log, - dt_bias=dt_bias, - output_final_state=False, - use_qk_l2norm_in_kernel=True, - use_gate_in_kernel=True, - use_beta_sigmoid_in_kernel=True, - safe_gate=lower_bound is not None, - lower_bound=lower_bound, - ) - return output + chunk_kda = _load_fla_chunk_kda() + + # FLA's kda_gate_chunk_cumsum launch uses grid_z = batch * num_heads. + # DSpark flattens anchors into the batch because recurrent KDA state must + # reset for every proposal block. At the production shape this is + # 8 * 512 * 96 = 393,216, beyond CUDA's grid-z limit. Splitting only the + # independent block dimension is algebraically exact; concatenation also + # lets autograd sum the shared A_log and dt_bias gradients across slices. + num_heads = int(q.shape[2]) + max_blocks_per_launch = max(1, _CUDA_MAX_GRID_DIM_Z // num_heads) + outputs = [] + for start in range(0, int(q.shape[0]), max_blocks_per_launch): + end = start + max_blocks_per_launch + output, _ = chunk_kda( + q=q[start:end], + k=k[start:end], + v=v[start:end], + g=raw_gate[start:end], + beta=beta[start:end], + A_log=A_log, + dt_bias=dt_bias, + output_final_state=False, + use_qk_l2norm_in_kernel=True, + use_gate_in_kernel=True, + use_beta_sigmoid_in_kernel=True, + safe_gate=lower_bound is not None, + lower_bound=lower_bound, + ) + outputs.append(output) + return outputs[0] if len(outputs) == 1 else torch.cat(outputs, dim=0) class KimiK3DraftKDAAttention(nn.Module): diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index 52ac0ca6b..35909b404 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -7,6 +7,7 @@ from specforge.config import Config from specforge.data.template import TEMPLATE_REGISTRY +from specforge.modeling.draft import kimi_k3_dspark from specforge.modeling.draft.kimi_k3_dspark import ( KimiK3DraftKDAAttention, KimiK3DSpark4KDA1MLADraftModel, @@ -218,6 +219,55 @@ def test_tiny_4kda_1mla_forward_and_backward(): assert "q_conv1d.bias" not in first_kda.state_dict() +def test_fla_kda_splits_independent_blocks_below_cuda_grid_z_limit(monkeypatch): + calls = [] + + def fake_chunk_kda(**kwargs): + q = kwargs["q"] + calls.append(int(q.shape[0])) + assert q.shape[0] * q.shape[2] <= 8 + assert kwargs["output_final_state"] is False + assert kwargs["use_qk_l2norm_in_kernel"] is True + assert kwargs["use_gate_in_kernel"] is True + assert kwargs["use_beta_sigmoid_in_kernel"] is True + output = ( + q + + kwargs["k"] + + kwargs["v"] + + kwargs["g"] + + kwargs["beta"].unsqueeze(-1) + + kwargs["A_log"].view(1, 1, -1, 1) + + kwargs["dt_bias"].view(1, 1, q.shape[2], q.shape[3]) + ) + return output, None + + monkeypatch.setattr(kimi_k3_dspark, "_CUDA_MAX_GRID_DIM_Z", 8) + monkeypatch.setattr(kimi_k3_dspark, "_load_fla_chunk_kda", lambda: fake_chunk_kda) + + shape = (5, 2, 3, 4) + q, k, v, gate = (torch.randn(shape, requires_grad=True) for _ in range(4)) + beta = torch.randn(shape[:-1], requires_grad=True) + A_log = torch.randn(shape[2], requires_grad=True) + dt_bias = torch.randn(shape[2] * shape[3], requires_grad=True) + output = kimi_k3_dspark._fla_kda( + q, + k, + v, + gate, + beta, + A_log, + dt_bias, + lower_bound=-5.0, + ) + + assert calls == [2, 2, 1] + assert output.shape == shape + output.sum().backward() + for tensor in (q, k, v, gate, beta, A_log, dt_bias): + assert tensor.grad is not None + assert torch.isfinite(tensor.grad).all() + + def test_kda_resets_state_between_proposal_blocks(): config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") attention = KimiK3DraftKDAAttention(config, layer_idx=0) From 3faa2340a08633b431fe5b31b2fa3efb87c9df6e Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 13:56:26 -0700 Subject: [PATCH 23/88] fix(tracking): retain owned W&B run handle --- specforge/tracker.py | 19 ++++++++---- tests/test_runtime/test_tracking_logger.py | 35 +++++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/specforge/tracker.py b/specforge/tracker.py index 4257715a6..438fc80ff 100644 --- a/specforge/tracker.py +++ b/specforge/tracker.py @@ -179,6 +179,7 @@ def validate_args(cls, parser, args): def __init__(self, args, output_dir: str): super().__init__(args, output_dir) + self._run = None if wandb is None: raise RuntimeError( "To use --report-to wandb, install the W&B client: " @@ -199,16 +200,22 @@ def __init__(self, args, output_dir: str): } if args.wandb_offline: init_kwargs["mode"] = "offline" - wandb.init(**init_kwargs) - self.is_initialized = True + # Keep the run handle owned by this tracker. The module-level + # ``wandb.run`` singleton is mutable process-global state; relying + # on it after a multiprocessing-heavy setup can silently detach + # explicit training history from the initialized run. + self._run = wandb.init(**init_kwargs) + self.is_initialized = self._run is not None def log(self, log_dict: Dict[str, Any], step: Optional[int] = None): - if self.rank == 0 and self.is_initialized: - wandb.log(log_dict, step=step) + if self.rank == 0 and self.is_initialized and self._run is not None: + self._run.log(log_dict, step=step) def close(self): - if self.rank == 0 and self.is_initialized and wandb.run: - wandb.finish() + if self.rank == 0 and self.is_initialized: + if self._run is not None: + self._run.finish() + self._run = None self.is_initialized = False diff --git a/tests/test_runtime/test_tracking_logger.py b/tests/test_runtime/test_tracking_logger.py index 6a8c2b985..8bfa894b6 100644 --- a/tests/test_runtime/test_tracking_logger.py +++ b/tests/test_runtime/test_tracking_logger.py @@ -2,10 +2,11 @@ """Backend-neutral experiment tracking at the Trainer logger seam.""" import unittest +from tempfile import TemporaryDirectory from types import SimpleNamespace from unittest import mock -from specforge.tracker import _public_config +from specforge.tracker import WandbTracker, _public_config from specforge.training.tracking import ( TrackerLogger, create_tracker_logger, @@ -156,6 +157,38 @@ def test_factory_adapts_existing_tracker_registry(self): logger({"loss": 2.0}, 3) self.assertEqual(tracker.logged, [({"train/loss": 2.0}, 3)]) + def test_wandb_tracker_logs_through_its_owned_run_handle(self): + run = mock.Mock() + wandb = mock.Mock() + wandb.init.return_value = run + args = SimpleNamespace( + wandb_dir=None, + wandb_offline=True, + wandb_key=None, + wandb_project="specforge", + wandb_name="disaggregated-trainer", + ) + + with ( + TemporaryDirectory() as output_dir, + mock.patch("specforge.tracker.wandb", wandb), + mock.patch("specforge.tracker.dist.is_available", return_value=True), + mock.patch("specforge.tracker.dist.is_initialized", return_value=True), + mock.patch("specforge.tracker.dist.get_rank", return_value=0), + ): + tracker = WandbTracker(args, output_dir) + # A later multiprocessing lifecycle may clear W&B's module-global + # current run. The tracker-owned handle must remain authoritative. + wandb.run = None + tracker.log({"train/loss": 1.25}, step=10) + tracker.close() + tracker.close() + + run.log.assert_called_once_with({"train/loss": 1.25}, step=10) + run.finish.assert_called_once_with() + wandb.log.assert_not_called() + wandb.finish.assert_not_called() + def test_noop_tracker_does_not_require_initialized_distributed(self): logger = create_tracker_logger(SimpleNamespace(report_to="none"), "/tmp/output") logger({"loss": 1.0}, 1) From 5301d1ede46a565d9740981ea1bfd6cbbd8794b0 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 14:22:44 -0700 Subject: [PATCH 24/88] fix(tracking): commit W&B step metrics --- specforge/tracker.py | 5 ++++- tests/test_runtime/test_tracking_logger.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/specforge/tracker.py b/specforge/tracker.py index 438fc80ff..1189ed6cf 100644 --- a/specforge/tracker.py +++ b/specforge/tracker.py @@ -209,7 +209,10 @@ def __init__(self, args, output_dir: str): def log(self, log_dict: Dict[str, Any], step: Optional[int] = None): if self.rank == 0 and self.is_initialized and self._run is not None: - self._run.log(log_dict, step=step) + # W&B defaults ``commit`` to False whenever an explicit step is + # supplied. Finalize each trainer record so live runs publish the + # point immediately instead of keeping the newest step buffered. + self._run.log(log_dict, step=step, commit=True) def close(self): if self.rank == 0 and self.is_initialized: diff --git a/tests/test_runtime/test_tracking_logger.py b/tests/test_runtime/test_tracking_logger.py index 8bfa894b6..97d09ffd5 100644 --- a/tests/test_runtime/test_tracking_logger.py +++ b/tests/test_runtime/test_tracking_logger.py @@ -184,7 +184,7 @@ def test_wandb_tracker_logs_through_its_owned_run_handle(self): tracker.close() tracker.close() - run.log.assert_called_once_with({"train/loss": 1.25}, step=10) + run.log.assert_called_once_with({"train/loss": 1.25}, step=10, commit=True) run.finish.assert_called_once_with() wandb.log.assert_not_called() wandb.finish.assert_not_called() From 4095c163b9414c5926590c09c1335d0858d9b778 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sat, 1 Aug 2026 20:10:11 -0700 Subject: [PATCH 25/88] fix(kimi-k3): pipeline capture and trainer throughput --- examples/README.md | 4 +- ...a-1mla-openperfectblend-disaggregated.yaml | 23 ++++---- ...k-5mla-openperfectblend-disaggregated.yaml | 30 +++++++---- specforge/training/controller.py | 54 +++++++++++++++++++ .../test_kimi_k3_dspark_architectures.py | 35 +++++++++--- tests/test_runtime/test_trainer.py | 33 ++++++++++++ 6 files changed, 151 insertions(+), 28 deletions(-) diff --git a/examples/README.md b/examples/README.md index f0852f41a..68116cf58 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,8 +31,8 @@ NPU, offline, and managed/external-service variants, is in | `examples/configs/qwen3-8b-domino-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | Domino | | `examples/configs/qwen3-8b-peagle-disaggregated.yaml` | Disaggregated SGLang server capture | P-EAGLE | | `examples/configs/qwen3-4b-dspark-disaggregated.yaml` | Disaggregated server capture | DSpark | -| `examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml` | Kimi-K3 TP8 server capture + four-rank trainer | DSpark 5×MLA | -| `examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` | Kimi-K3 TP8 server capture + four-rank trainer | DSpark 4×KDA+1×MLA | +| `examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml` | Two Kimi-K3 TP8 capture replicas + eight-rank pipelined trainer | DSpark 5×MLA | +| `examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` | Two Kimi-K3 TP8 capture replicas + eight-rank pipelined trainer | DSpark 4×KDA+1×MLA | | `examples/configs/qwen3-4b-dspark-offline.yaml` | Precomputed features | DSpark | | `examples/configs/qwen3.6-27b-dflash-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | DFlash | | `examples/configs/qwen3.6-27b-dflash-1server-dp2-disaggregated.yaml` | Managed local one capture server + DP2 | DFlash | diff --git a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml index 8cdf7959a..5c5b5e978 100644 --- a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml @@ -29,7 +29,7 @@ training: strategy: dspark num_epochs: 10 batch_size: 8 - accumulation_steps: 16 + accumulation_steps: 8 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -54,13 +54,15 @@ tracking: wandb_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/wandb runtime: - producer_lease: 1 - producer_concurrency: 8 - in_flight_high_watermark: 576 - in_flight_low_watermark: 512 - resident_high_watermark_bytes: 206158430208 - resident_low_watermark_bytes: 180388626432 - feature_store_max_resident_bytes: 240518168576 + producer_lease: 8 + producer_concurrency: 1 + # Two optimizer windows remove the capture/train bubble. The low watermark + # lets both batched capture workers resume after one 512-ref durable ack. + in_flight_high_watermark: 1088 + in_flight_low_watermark: 640 + resident_high_watermark_bytes: 377957122048 + resident_low_watermark_bytes: 206158430208 + feature_store_max_resident_bytes: 412316860416 run_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated output_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/output @@ -69,14 +71,15 @@ deployment: mode: disaggregated trainer: nnodes: 1 - nproc_per_node: 4 + nproc_per_node: 8 disaggregated: control_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/control consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/consumer-state backend: mooncake store_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated server_urls: - - http://10.65.0.2:30000 + - http://capture-node-0:30000 + - http://capture-node-1:30000 mooncake_metadata_server: http://10.65.0.2:35880/metadata mooncake_master_server_addr: 10.65.0.2:35551 mooncake_protocol: tcp diff --git a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml index 43fd3bab1..b5eab8390 100644 --- a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml @@ -29,7 +29,7 @@ training: strategy: dspark num_epochs: 10 batch_size: 8 - accumulation_steps: 16 + accumulation_steps: 8 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -54,13 +54,22 @@ tracking: wandb_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/wandb runtime: - producer_lease: 1 - producer_concurrency: 8 - in_flight_high_watermark: 576 - in_flight_low_watermark: 512 - resident_high_watermark_bytes: 206158430208 - resident_low_watermark_bytes: 180388626432 - feature_store_max_resident_bytes: 240518168576 + # Submit the same batch of eight prompts used by the reference target path. + # Scalar HTTP calls leave the large K3 TP8 engine scheduler-bound. + producer_lease: 8 + producer_concurrency: 1 + # Keep two complete global-batch windows resident. A single-window buffer + # makes capture and training alternate because source refs are acknowledged + # only after the optimizer step is durable. The 64-ref margin covers both + # TP8 capture workers' outstanding leases; low=640 resumes capture after one + # 512-ref optimizer acknowledgement while another window stays available. + in_flight_high_watermark: 1088 + in_flight_low_watermark: 640 + # Two worst-case 4K windows are about 336 GiB. Pause at 352 GiB, resume + # after one window at 192 GiB, and leave 32 GiB for leased-request overshoot. + resident_high_watermark_bytes: 377957122048 + resident_low_watermark_bytes: 206158430208 + feature_store_max_resident_bytes: 412316860416 run_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated output_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/output @@ -69,14 +78,15 @@ deployment: mode: disaggregated trainer: nnodes: 1 - nproc_per_node: 4 + nproc_per_node: 8 disaggregated: control_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/control consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/consumer-state backend: mooncake store_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated server_urls: - - http://10.65.0.2:30000 + - http://capture-node-0:30000 + - http://capture-node-1:30000 mooncake_metadata_server: http://10.65.0.2:35880/metadata mooncake_master_server_addr: 10.65.0.2:35551 mooncake_protocol: tcp diff --git a/specforge/training/controller.py b/specforge/training/controller.py index de6fcb29e..b5f1a5bb1 100644 --- a/specforge/training/controller.py +++ b/specforge/training/controller.py @@ -20,6 +20,7 @@ import logging import os import sys +import time from dataclasses import dataclass, field from typing import Any, Callable, Dict, Iterable, List, Optional @@ -540,6 +541,12 @@ def _fit(self, data: Iterable[TrainBatch], progress: Optional[Any]) -> int: self.eval_interval > 0 and self.eval_data_factory is not None ) pending_ack: List[str] = [] + perf_window_started = time.perf_counter() + perf_window_steps = 0 + perf_window_samples = 0 + perf_data_wait_s = 0.0 + perf_train_compute_s = 0.0 + perf_durable_ack_s = 0.0 for epoch in range(self.epoch, self.num_epochs): self.epoch = epoch if hasattr(data, "set_epoch"): @@ -563,32 +570,40 @@ def _fit(self, data: Iterable[TrainBatch], progress: Optional[Any]) -> int: stream = it _it = iter(stream) while True: + data_wait_started = time.perf_counter() try: batch = next(_it) except StopIteration: break + perf_data_wait_s += time.perf_counter() - data_wait_started + perf_window_samples += len(batch.sample_ids) self._epoch_batch += 1 self._epoch_samples += len(batch.sample_ids) self.micro_step += 1 if self.ack_fn is not None: pending_ack.extend(batch.sample_ids) self._step_profiler.before_micro_step(self.global_step) + train_compute_started = time.perf_counter() result = self.core.train_step( batch, ctx=StepContext( global_step=self.global_step, total_steps=self.total_steps ), ) + perf_train_compute_s += time.perf_counter() - train_compute_started self.last_metrics = result.metrics # grad accumulated but optimizer has not stepped yet; everything # keyed on optimizer steps fires only at the boundary. if not result.optimizer_stepped: continue self.global_step += 1 + perf_window_steps += 1 self._step_profiler.after_optimizer_step(self.global_step) if self.ack_fn is not None: # durable ack transaction at the optimizer-step boundary + durable_ack_started = time.perf_counter() self.ack_fn(pending_ack, self.global_step) + perf_durable_ack_s += time.perf_counter() - durable_ack_started pending_ack = [] if self.logger and self.global_step % max(1, self.log_interval) == 0: log_metrics = dict(result.metrics) @@ -596,7 +611,46 @@ def _fit(self, data: Iterable[TrainBatch], progress: Optional[Any]) -> int: get_learning_rate = getattr(optimizer, "get_learning_rate", None) if callable(get_learning_rate): log_metrics["lr"] = float(get_learning_rate()) + perf_elapsed_s = max( + time.perf_counter() - perf_window_started, + 1e-12, + ) + parallel = getattr(self.core.backend, "parallel_config", None) + world_size = int(getattr(parallel, "world_size", 1)) + tp_size = int(getattr(parallel, "tp_size", 1)) + sp_size = int(getattr(parallel, "sp_size", 1)) + data_parallel_size = max(1, world_size // (tp_size * sp_size)) + log_metrics.update( + { + "perf/optimizer_steps_per_hour": ( + perf_window_steps * 3600.0 / perf_elapsed_s + ), + "perf/optimizer_step_time_s": ( + perf_elapsed_s / max(1, perf_window_steps) + ), + "perf/data_wait_time_s": ( + perf_data_wait_s / max(1, perf_window_steps) + ), + "perf/train_compute_time_s": ( + perf_train_compute_s / max(1, perf_window_steps) + ), + "perf/durable_ack_time_s": ( + perf_durable_ack_s / max(1, perf_window_steps) + ), + "perf/global_samples_per_second": ( + perf_window_samples + * data_parallel_size + / perf_elapsed_s + ), + } + ) self.logger(log_metrics, self.global_step) + perf_window_started = time.perf_counter() + perf_window_steps = 0 + perf_window_samples = 0 + perf_data_wait_s = 0.0 + perf_train_compute_s = 0.0 + perf_durable_ack_s = 0.0 eval_metrics: Optional[Dict[str, Any]] = None if eval_enabled and self.global_step % self.eval_interval == 0: eval_metrics = self.evaluate_configured() diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index 35909b404..be59aea61 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -108,7 +108,7 @@ def test_training_recipes_preserve_reference_run_contract(filename): config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) assert config.data.max_length == 4096 assert config.training.batch_size == 8 - assert config.training.accumulation_steps == 16 + assert config.training.accumulation_steps == 8 assert ( config.deployment.trainer.nnodes * config.deployment.trainer.nproc_per_node @@ -122,11 +122,34 @@ def test_training_recipes_preserve_reference_run_contract(filename): assert config.training.num_anchors == 512 assert config.training.save_interval == 250 assert config.training.log_interval == 10 - assert config.runtime.in_flight_high_watermark >= 512 - assert config.runtime.in_flight_low_watermark >= 512 - # A worst-case 4,096-token optimizer window carries about 168 GiB of - # captured features; do not reintroduce byte backpressure below it. - assert config.runtime.resident_high_watermark_bytes >= 180388626432 + optimizer_quantum = ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + ) + max_capture_overshoot = ( + len(config.deployment.disaggregated.server_urls) + * config.runtime.producer_concurrency + * config.runtime.producer_lease + ) + # Two windows are required for capture and training to overlap. Once one + # window is acknowledged, hysteresis must resume capture even at the + # maximum number of concurrently leased refs. + assert config.runtime.in_flight_high_watermark >= 2 * optimizer_quantum + assert ( + config.runtime.in_flight_low_watermark + >= config.runtime.in_flight_high_watermark + + max_capture_overshoot + - optimizer_quantum + ) + assert config.runtime.producer_lease == config.training.batch_size + assert config.runtime.producer_concurrency == 1 + assert len(config.deployment.disaggregated.server_urls) >= 2 + # Two worst-case 4,096-token optimizer windows carry about 336 GiB of + # captured features; byte throttling must not serialize the pipeline. + assert config.runtime.resident_high_watermark_bytes >= 360777252864 + assert config.runtime.resident_low_watermark_bytes >= 180388626432 assert ( config.runtime.feature_store_max_resident_bytes >= config.runtime.resident_high_watermark_bytes diff --git a/tests/test_runtime/test_trainer.py b/tests/test_runtime/test_trainer.py index efc8ebcfc..69444c093 100644 --- a/tests/test_runtime/test_trainer.py +++ b/tests/test_runtime/test_trainer.py @@ -327,6 +327,39 @@ def test_validate_batch_missing_feature(self): class TestTrainerController(unittest.TestCase): + def test_training_log_reports_pipeline_throughput_breakdown(self): + strat = FakeStrategy() + backend = FakeBackend(strat.model) + core = TrainerCore(strat, backend, accumulation_steps=1) + logged = [] + with tempfile.TemporaryDirectory() as d: + ctrl = TrainerController( + core, + run_id="r", + output_dir=d, + max_steps=2, + num_epochs=1, + log_interval=2, + logger=lambda metrics, step: logged.append((dict(metrics), step)), + ) + self.assertEqual(ctrl.fit([_batch(), _batch()]), 2) + + self.assertEqual(len(logged), 1) + metrics, step = logged[0] + self.assertEqual(step, 2) + for name in ( + "perf/optimizer_steps_per_hour", + "perf/optimizer_step_time_s", + "perf/data_wait_time_s", + "perf/train_compute_time_s", + "perf/durable_ack_time_s", + "perf/global_samples_per_second", + ): + self.assertIn(name, metrics) + self.assertGreaterEqual(metrics[name], 0.0) + self.assertGreater(metrics["perf/optimizer_steps_per_hour"], 0.0) + self.assertGreater(metrics["perf/global_samples_per_second"], 0.0) + def test_progress_bar_tracks_optimizer_steps_on_rank_zero(self): strat = FakeStrategy() backend = FakeBackend(strat.model) From e62fe83cbe4a0e99bb4262aad43b281fe84f9823 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sun, 2 Aug 2026 10:21:42 -0700 Subject: [PATCH 26/88] fix(kimi-k3): complete disaggregated training adoption --- configs/kimi-k3-dspark-fullattn-gqa16.json | 49 ++ docs/basic_usage/disaggregated_training.md | 22 +- .../kimi-k3-dspark-mla-kda-disaggregated.md | 150 +++- .../kimi-k3-dspark-v1c-disaggregated.md | 10 +- examples/configs/README.md | 3 +- ...a-1mla-openperfectblend-disaggregated.yaml | 21 +- ...k-5mla-openperfectblend-disaggregated.yaml | 25 +- ...llattn-openperfectblend-disaggregated.yaml | 107 +++ examples/disagg/README.md | 43 +- .../run_kimi_k3_dspark_capture_server.sh | 69 ++ .../disagg/sync_distributed_checkpoints.py | 365 +++++++++ .../sglang/kimi-k3-f8493a4/spec-capture.patch | 748 ++++++++++++++---- scripts/apply_sglang_spec_capture_patch.sh | 11 +- specforge/config/schema.py | 49 +- specforge/launch.py | 37 +- specforge/launch_plan.py | 15 +- specforge/runtime/data_plane/http_inbox.py | 326 ++++++++ tests/test_config/test_launch_topology.py | 77 +- .../test_unified_feature_reachability.py | 2 +- .../test_kimi_k3_dspark_architectures.py | 82 +- tests/test_runtime/test_http_inbox.py | 118 +++ tests/test_runtime/test_launch_plan.py | 50 +- .../test_runtime/test_package_architecture.py | 1 + tests/test_scripts/test_disagg_launchers.py | 46 +- .../test_prepare_hidden_states.py | 9 +- .../test_sync_distributed_checkpoints.py | 185 +++++ 26 files changed, 2353 insertions(+), 267 deletions(-) create mode 100644 configs/kimi-k3-dspark-fullattn-gqa16.json create mode 100644 examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml create mode 100755 examples/disagg/run_kimi_k3_dspark_capture_server.sh create mode 100755 examples/disagg/sync_distributed_checkpoints.py create mode 100644 specforge/runtime/data_plane/http_inbox.py create mode 100644 tests/test_runtime/test_http_inbox.py create mode 100644 tests/test_scripts/test_sync_distributed_checkpoints.py diff --git a/configs/kimi-k3-dspark-fullattn-gqa16.json b/configs/kimi-k3-dspark-fullattn-gqa16.json new file mode 100644 index 000000000..32527337b --- /dev/null +++ b/configs/kimi-k3-dspark-fullattn-gqa16.json @@ -0,0 +1,49 @@ +{ + "architectures": ["DSparkDraftModel"], + "attention_bias": false, + "attention_dropout": 0.0, + "auto_map": {"AutoModel": "dspark.DSparkDraftModel"}, + "block_size": 7, + "bos_token_id": 163584, + "dflash_config": { + "attention_mode": "gqa", + "confidence_head_alpha": 1.0, + "confidence_head_with_markov": true, + "enable_confidence_head": true, + "markov_head_type": "vanilla", + "markov_rank": 256, + "mask_token_id": 163824, + "projector_type": "dspark", + "target_layer_ids": [7, 23, 51, 67, 83] + }, + "dtype": "bfloat16", + "eos_token_id": 163586, + "head_dim": 64, + "hidden_act": "silu", + "hidden_size": 7168, + "initializer_range": 0.02, + "intermediate_size": 14336, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 1048576, + "max_window_layers": 5, + "model_type": "qwen3", + "num_attention_heads": 64, + "num_hidden_layers": 5, + "num_key_value_heads": 16, + "num_target_layers": 93, + "pad_token_id": 163839, + "rms_norm_eps": 0.00001, + "rope_scaling": null, + "rope_theta": 10000.0, + "sliding_window": null, + "tie_word_embeddings": false, + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 163840 +} diff --git a/docs/basic_usage/disaggregated_training.md b/docs/basic_usage/disaggregated_training.md index a3a20b54f..1340682d0 100644 --- a/docs/basic_usage/disaggregated_training.md +++ b/docs/basic_usage/disaggregated_training.md @@ -58,9 +58,12 @@ The control directory is attempt-scoped. The launcher deterministically derives the online reference channel and lifecycle markers, or the offline manifest, beneath that root. Online consumers put the rank-0 SQLite/WAL under `consumer_state_dir`; that path should be node-local and is required for -multi-node trainers. Rank inboxes stay under the shared `control_dir` when -`deployment.trainer.nnodes > 1`, so remote ranks never need to access the -SQLite filesystem. A fresh attempt requires fresh control and consumer-state +multi-node trainers. By default rank inboxes stay under the shared +`control_dir`. When a cluster cannot mount that path on every trainer node, set +`inbox_server_url` to a private rank-0 HTTP origin: rank 0 keeps the inbox files +locally and relays only tensor-free references and consumed counters to remote +ranks. The producer and consumer rank 0 must still resolve `control_dir` to the +same path. A fresh attempt requires fresh control and consumer-state directories. `store_id` defaults to `run_id` and can be set explicitly when the Mooncake deployment requires another namespace. @@ -236,7 +239,11 @@ deployment: master_addr: trainer-0.example master_port: 29500 disaggregated: - control_dir: /shared/control/attempt-001 + # With inbox_server_url, only the producer and consumer rank 0 need this + # path; remote ranks receive their tensor-free reference stream over HTTP. + control_dir: /local/rank0/control/attempt-001 + consumer_state_dir: /local/rank0/consumer-state/attempt-001 + inbox_server_url: http://trainer-0.example:35900 backend: mooncake server_urls: [http://capture-server:30000] ``` @@ -257,6 +264,13 @@ detected and used as the worker environment rather than nesting another torchrun. A producer is rejected inside a multi-rank torchrun to prevent duplicate capture/ingestion roles. +`inbox_server_url` is optional; omit it when every trainer rank shares +`control_dir`. When set, rank 0 binds the configured port on all interfaces and +remote ranks pull only their private metadata stream. The endpoint has no +built-in authentication or TLS, so expose it only on a trusted cluster network. +The producer must run on the rank-0 host (or otherwise share rank 0's +`control_dir`); payload tensors never traverse this HTTP service. + The separate-inference-node Qwen3-8B example keeps that scheduler boundary but restores full development-stack orchestration: diff --git a/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md b/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md index c0d9b139e..f2ef630f9 100644 --- a/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md +++ b/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md @@ -50,61 +50,130 @@ renders the conversation schema and right-truncates to 4096 tokens. ## Reference training settings -The full recipes preserve the K3 reference run's training settings: +The exact full-attention recipe preserves the K3 reference run's training +settings and trainer shape: -- 4096 token examples, batch size 8, accumulation 32; +- 4096 token examples, per-rank batch size 1, accumulation 32, DP16; +- four-batch feature prefetch to overlap Mooncake TCP transfer with trainer + compute; - 10 epochs, learning rate `6e-4`, warmup ratio `0.04`; +- 9,173 optimizer steps (`469,695 * 10 // 512`), explicitly pinned so all + roles share one schedule horizon at startup; - 512 anchors, block size 7, Markov rank 256; - CE/L1/confidence weights `0.1/0.9/1.0`; - log every 10 steps and save every 250 steps; +- retain the latest three assembled checkpoints on each trainer node; - online W&B project `specforge-dspark`. -The supplied topology is one TP8 K3 capture server and a separate four-rank -trainer. Because the trainer world size is part of the effective global batch, -record any topology override in the W&B run config. +The supplied exact topology is two TP8 K3 capture replicas and a separate +two-node, 16-rank trainer. The source job's `BATCH_SIZE=8` was per TP8 target +replica, not per FSDP rank: its two replicas formed a 16-sample optimizer +microbatch. One sample on each of 16 trainer GPUs with accumulation 32 restores +the source global batch of 512 and its per-GPU draft compute. The experimental +MLA/KDA recipes remain portable one-node DP8 configurations with two samples +per rank and the same global batch. Two optimizer windows stay in Mooncake so +target capture for the next update overlaps draft training; record any topology +override in the W&B run config. ## Smoke then full training Start Mooncake as described in -[the K3 V1C runbook](kimi-k3-dspark-v1c-disaggregated.md), then launch the -patched latest K3 SGLang server. The capture layer ids are part of this recipe's -contract and intentionally differ from the V1C reproduction: +[the K3 V1C runbook](kimi-k3-dspark-v1c-disaggregated.md), then apply the K3 +patch to revision `ee560a2b2df5dafe18fd835d2e546eff019ca5ba`. Launch +`run_kimi_k3_dspark_capture_server.sh` on each of the two capture nodes. The +capture layer ids are part of this recipe's contract and intentionally differ +from the V1C reproduction: ```bash -export CAPTURE_IP=10.65.0.2 -export TARGET_MODEL=/workspace/models/Kimi-K3 -export MOONCAKE_MASTER_SERVER_ADDR="$CAPTURE_IP:35551" -export MOONCAKE_METADATA_SERVER="http://$CAPTURE_IP:35880/metadata" -export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" -export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" -export MOONCAKE_PROTOCOL=tcp -export MOONCAKE_GLOBAL_SEGMENT_SIZE=1099511627776 -export MOONCAKE_LOCAL_BUFFER_SIZE=1073741824 -CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m sglang.launch_server \ - --host 0.0.0.0 \ - --port 30000 \ - --model-path "$TARGET_MODEL" \ - --trust-remote-code \ - --skip-tokenizer-init \ - --tp-size 8 \ - --mem-fraction-static 0.76 \ - --context-length 4608 \ - --max-running-requests 8 \ - --max-total-tokens 40960 \ - --prefill-attention-backend flashinfer \ - --decode-attention-backend trtllm_mla \ - --moe-runner-backend marlin \ - --enable-symm-mem \ - --mamba-radix-cache-strategy extra_buffer \ - --max-mamba-cache-size 40 \ - --chunked-prefill-size -1 \ - --enable-spec-capture \ - --spec-capture-method dspark \ - --spec-capture-aux-layer-ids 11 23 47 71 83 +export MODEL_PATH=/workspace/models/Kimi-K3-cdd2e49a +export MOONCAKE_MASTER_IP=10.65.0.2 +export SGLANG_ROOT=/workspace/sglang-kimi-k3-ee560a2-spec-capture +export CAPTURE_IP=10.65.0.5 # use the current node's routable address +examples/disagg/run_kimi_k3_dspark_capture_server.sh ``` Use capture IP/ports that match the selected YAML. +For the exact five-layer full-attention reference architecture, use capture +layers `[7, 23, 51, 67, 83]` and the checked-in full-attention recipe. Run the +producer beside trainer rank 0 so both can use the rank-0 local `control_dir`. +The rank-0 inbox HTTP relay serves only tensor-free `SampleRef` metadata to the +second trainer node; feature tensors still move directly through Mooncake. +Override the placeholder host names with private, trusted-network addresses: + +```bash +export AUX_LAYER_IDS="7 23 51 67 83" +specforge train \ + --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ + --role producer \ + deployment.trainer.master_addr=10.65.0.3 \ + deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 + +# Trainer node 0 +WANDB_MODE=online MOONCAKE_LOCAL_HOSTNAME=10.65.0.3 specforge train \ + --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ + --role consumer --node-rank 0 \ + deployment.trainer.master_addr=10.65.0.3 \ + deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 + +# Trainer node 1 +WANDB_MODE=online MOONCAKE_LOCAL_HOSTNAME=10.65.0.4 specforge train \ + --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ + --role consumer --node-rank 1 \ + deployment.trainer.master_addr=10.65.0.3 \ + deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 +``` + +When the two trainer nodes do not share `output_dir`, start +`examples/disagg/sync_distributed_checkpoints.py` on both nodes with local rank +ranges `0-7` and `8-15`, respectively. The +[disaggregated examples guide](../../examples/disagg/README.md#checkpoints-without-shared-storage) +contains the complete commands and private-network boundary. + +## Capture throughput contract + +The source run used two independent TP8 target replicas. A normal TP8 prefill +and a capture-enabled TP8 prefill should have comparable model-compute speed, +but one replica still provides only half of the source job's aggregate sample +rate. The capture patch therefore: + +- performs D2H only on the output TP rank and reuses contiguous per-request + views instead of concatenating every single-chunk tensor; +- publishes the scheduler batch through Mooncake `batch_put_from` rather than + one RPC per feature object; +- gives each target endpoint two producer request slots, overlapping one + bounded background publish with the next TP8 prefill; and +- holds the HTTP completion response until every feature key is durable. + +The exact 4,096-token validation captured 128 unique samples (211,715 tokens, +18,214,264,880 feature bytes) in 25.010 seconds after the first request began: +5.118 samples/s per TP8 replica. Two replicas project to 10.236 samples/s, or +71.97 optimizer steps/hour at global batch 512. The source W&B run measured +71.50 steps/hour. Steady target prefill remained approximately 10.9–11.2k +tokens/s, so the remaining scale factor is replica count rather than an SGLang +compute regression. + +A DP16 end-to-end smoke completed with finite losses and gradient norms across +all 16 ranks. Both trainer nodes assembled and opened the portable checkpoint: +one shared training-state file plus rank files 0--15. Before prefetch, its warm +step took 56.44 seconds: 35--39 seconds of trainer compute plus up to 21 seconds +waiting for Mooncake TCP feature materialization. + +With `data.dataloader_num_workers: 4`, four subsequent warm steps took 49.61, +47.70, 48.52, and 49.25 seconds. Their mean was 48.77 seconds, or 73.82 +steps/hour and 10.50 samples/s at global batch 512. This is 3.1% faster than +the source W&B run's 50.35 seconds/step (71.50 steps/hour). The prefetch smoke +is recorded in W&B as run `npia5q21`. Keep prefetch enabled when Mooncake uses +TCP; it overlaps feature transfer with the current optimizer step without +changing capture outputs or the training objective. + +The resulting 9,173-step full run is recorded in W&B as run `fth4aze4`. Steps +20 through 70 reported finite losses and gradient norms. Step time fell from +45.53 to approximately 40.3 seconds as feature prefetch warmed; steps 60 and +70 sustained 89.28 and 88.85 steps/hour (about 12.7 samples/s), roughly 24% +above the source run. These measurements are early-run validation; use the W&B +performance panels and assembled checkpoints to monitor the remaining run. + Keep the W&B API key in a protected file and export it only in the trainer shell; do not put it in YAML or a command transcript. @@ -132,5 +201,6 @@ WANDB_MODE=online specforge train \ Use the corresponding `kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` config for the -hybrid. The two full jobs need separate output/control directories and should -not share one four-rank trainer allocation concurrently. +hybrid, or the full-attention config above for the exact old architecture. The +full jobs need separate output/control directories and should not share one +trainer allocation concurrently. diff --git a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md index 13fa63e97..8f885a0c1 100644 --- a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md +++ b/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md @@ -9,16 +9,17 @@ effective global batch, constant learning rate, and DSpark loss weights. - SpecForge with configurable LR scheduling, an independent online prompt seed, and dedicated DSpark capture support. -- Kimi K3 SGLang revision `9acd9cba39f522da71c6d9b9695f2ceb41d36b18` +- Kimi K3 SGLang revision `ee560a2b2df5dafe18fd835d2e546eff019ca5ba` (the current public `kimi-k3` branch tip validated by this recipe). - The K3 SGLang tree patched with: ```bash - scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-9acd9cb + scripts/apply_sglang_spec_capture_patch.sh --target kimi-k3-ee560a2 ``` -The patch was originally authored against `f8493a4`, remains byte-identical, -and applies cleanly to `9acd9cb`. It makes `--spec-capture-method dspark` call K3's +The patch is generated against `ee560a2` and compatibility-checked against +`f8493a4`, `9acd9cb`, and `ee560a2`; its historical directory name is retained +for existing automation. It makes `--spec-capture-method dspark` call K3's `set_dspark_layers_to_capture` hook. The generic DFlash capture method is not equivalent for K3. The same versioned patch carries the three required 64K correctness guards: 64-bit Triton token offsets, scale-stable residual scoring, @@ -83,6 +84,7 @@ CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 python -m sglang.launch_server \ --moe-runner-backend marlin \ --mamba-radix-cache-strategy extra_buffer \ --max-mamba-cache-size 5 \ + --disable-cuda-graph \ --chunked-prefill-size -1 \ --enable-spec-capture \ --spec-capture-method dspark \ diff --git a/examples/configs/README.md b/examples/configs/README.md index 2ca3d48c2..b128a930d 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -278,9 +278,10 @@ For `deployment.mode: disaggregated`, also write: | Field | Default | What to write | | --- | --- | --- | -| `deployment.disaggregated.control_dir` | required | Fresh attempt-scoped shared directory for refs/manifest and lifecycle markers. | +| `deployment.disaggregated.control_dir` | required | Fresh attempt-scoped directory for refs/manifest and lifecycle markers. Shared by default; with `inbox_server_url`, only producer and consumer rank 0 must share it. | | `deployment.disaggregated.backend` | required | `mooncake` or `shared_dir`. Online disaggregated runs require Mooncake. | | `deployment.disaggregated.consumer_state_dir` | `null` | Node-local rank-0 SQLite/WAL root. Required for multi-node online consumers; their rank inboxes remain under shared `control_dir`. | +| `deployment.disaggregated.inbox_server_url` | `null` | Optional private `http://host:port` rank-0 relay for tensor-free inbox refs when remote trainer ranks cannot share `control_dir`. Online multi-node only; no credentials, path, query, TLS, or built-in authentication. | | `deployment.disaggregated.store_root` | `null` | Shared feature directory; required when `backend: shared_dir`. | | `deployment.disaggregated.store_id` | `null` | Feature-store namespace; defaults to `run_id`. | | `deployment.disaggregated.server_urls` | `[]` | External patched SGLang capture endpoints. One rollout worker is created per entry. Do not set with `managed_local`. | diff --git a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml index 5c5b5e978..a501683fe 100644 --- a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml @@ -10,12 +10,14 @@ model: sglang_attention_backend: flashinfer sglang_mem_fraction_static: 0.76 sglang_context_length: 4608 - sglang_max_running_requests: 8 - sglang_max_total_tokens: 40960 + sglang_max_running_requests: 16 + sglang_max_total_tokens: 73728 sglang_moe_runner_backend: marlin - sglang_enable_symm_mem: true + # Latest K3 uses its faster fused CustomAllReduceV2 path only when the + # generic symmetric-memory allocator is disabled. + sglang_enable_symm_mem: false sglang_mamba_radix_cache_strategy: extra_buffer - sglang_max_mamba_cache_size: 40 + sglang_max_mamba_cache_size: 80 data: train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl @@ -28,8 +30,10 @@ data: training: strategy: dspark num_epochs: 10 - batch_size: 8 - accumulation_steps: 8 + # Source TP8x2 capture produced 16 samples per optimizer microbatch. DP8 + # consumes the same shape as 2 samples per rank; 8 * 2 * 32 = 512. + batch_size: 2 + accumulation_steps: 32 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -54,8 +58,9 @@ tracking: wandb_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/wandb runtime: - producer_lease: 8 - producer_concurrency: 1 + producer_lease: 16 + # Per endpoint: publish batch N while TP8 prefills batch N+1. + producer_concurrency: 2 # Two optimizer windows remove the capture/train bubble. The low watermark # lets both batched capture workers resume after one 512-ref durable ack. in_flight_high_watermark: 1088 diff --git a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml index b5eab8390..d55ff6457 100644 --- a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml @@ -10,12 +10,14 @@ model: sglang_attention_backend: flashinfer sglang_mem_fraction_static: 0.76 sglang_context_length: 4608 - sglang_max_running_requests: 8 - sglang_max_total_tokens: 40960 + sglang_max_running_requests: 16 + sglang_max_total_tokens: 73728 sglang_moe_runner_backend: marlin - sglang_enable_symm_mem: true + # Latest K3 uses its faster fused CustomAllReduceV2 path only when the + # generic symmetric-memory allocator is disabled. + sglang_enable_symm_mem: false sglang_mamba_radix_cache_strategy: extra_buffer - sglang_max_mamba_cache_size: 40 + sglang_max_mamba_cache_size: 80 data: train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl @@ -28,8 +30,10 @@ data: training: strategy: dspark num_epochs: 10 - batch_size: 8 - accumulation_steps: 8 + # Source TP8x2 capture produced 16 samples per optimizer microbatch. DP8 + # consumes the same shape as 2 samples per rank; 8 * 2 * 32 = 512. + batch_size: 2 + accumulation_steps: 32 learning_rate: 0.0006 warmup_ratio: 0.04 max_grad_norm: 1 @@ -54,10 +58,11 @@ tracking: wandb_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/wandb runtime: - # Submit the same batch of eight prompts used by the reference target path. - # Scalar HTTP calls leave the large K3 TP8 engine scheduler-bound. - producer_lease: 8 - producer_concurrency: 1 + # Amortize K3's fixed auxiliary-capture and HTTP overhead across 16 prompts. + # The 40K prefill cap still splits unusually token-heavy batches safely. + producer_lease: 16 + # Per endpoint: publish batch N while TP8 prefills batch N+1. + producer_concurrency: 2 # Keep two complete global-batch windows resident. A single-window buffer # makes capture and training alternate because source refs are acknowledged # only after the optimizer step is durable. The 64-ref margin covers both diff --git a/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml new file mode 100644 index 000000000..dc952b221 --- /dev/null +++ b/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml @@ -0,0 +1,107 @@ +model: + # Exact target revision used by the reference run njyq006k. + target_model_path: /workspace/models/Kimi-K3 + draft_model_config: configs/kimi-k3-dspark-fullattn-gqa16.json + target_backend: sglang + trust_remote_code: true + embedding_key: language_model.model.embed_tokens.weight + lm_head_key: language_model.lm_head.weight + mask_token_id: 163824 + torch_dtype: bfloat16 + sglang_attention_backend: flashinfer + sglang_mem_fraction_static: 0.76 + sglang_context_length: 4608 + sglang_max_running_requests: 16 + sglang_max_total_tokens: 73728 + sglang_moe_runner_backend: marlin + sglang_enable_symm_mem: false + sglang_mamba_radix_cache_strategy: extra_buffer + sglang_max_mamba_cache_size: 80 + +data: + # Prepared from skx618/Kimi-K3-OpenPerfectBlend-Regen@9caaf705... + # after the reference structural gate: 499,591 source rows -> 469,695 + # trainable rows after the dataset builder's 14-token supervision gate. + train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-9caaf705/prepared-4096.jsonl + max_length: 4096 + chat_template: kimi-k3-thinking + cache_dir: /workspace/k3_dspark/cache + build_dataset_num_proc: 64 + # Materialize the next optimizer window while the current one trains. This + # hides Mooncake TCP transfer latency on the two-node DP16 topology. + dataloader_num_workers: 4 + +training: + strategy: dspark + num_epochs: 10 + # floor(469,695 prompts * 10 epochs / global batch 512) + total_steps: 9173 + # Preserve the source FSDP16 trainer shape: one sample on each of 16 ranks, + # accumulated 32 times. Keeping a DP8 consumer would preserve the global + # batch mathematically but halve trainer compute and miss the source rate. + batch_size: 1 + accumulation_steps: 32 + learning_rate: 0.0006 + lr_scheduler: cosine + warmup_ratio: 0.04 + max_grad_norm: 1 + attention_backend: flex_attention + num_anchors: 512 + loss_decay_gamma: 4.0 + objective_chunk_blocks: 128 + dspark_ce_loss_alpha: 0.1 + dspark_l1_loss_alpha: 0.9 + dspark_confidence_head_alpha: 1.0 + save_interval: 250 + # Keep the latest three portable DP16 checkpoints; relay archives remain as + # an additional recovery source without duplicating every checkpoint tree. + max_checkpoints: 3 + log_interval: 10 + dist_timeout: 30 + seed: 42 + prompt_seed: 1 + +tracking: + report_to: wandb + wandb_project: specforge-dspark + wandb_name: kimi-k3-dspark-5layer-fullattn-reference-disaggregated + wandb_offline: false + wandb_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/wandb + +runtime: + producer_lease: 16 + # Per endpoint: publish batch N while TP8 prefills batch N+1. + producer_concurrency: 2 + in_flight_high_watermark: 1088 + in_flight_low_watermark: 640 + resident_high_watermark_bytes: 377957122048 + resident_low_watermark_bytes: 206158430208 + feature_store_max_resident_bytes: 412316860416 + +run_id: kimi-k3-dspark-5layer-fullattn-reference-disaggregated +output_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/output + +deployment: + mode: disaggregated + trainer: + nnodes: 2 + nproc_per_node: 8 + master_addr: trainer-node-0 + master_port: 29500 + disaggregated: + control_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/control + consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/consumer-state + # Rank 0 relays tensor-free inbox metadata to the second trainer node. The + # target tensors continue to move directly through Mooncake. + inbox_server_url: http://trainer-node-0:35900 + backend: mooncake + store_id: kimi-k3-dspark-5layer-fullattn-reference-disaggregated + server_urls: + - http://capture-node-0:30000 + - http://capture-node-1:30000 + mooncake_metadata_server: http://10.65.0.2:35880/metadata + mooncake_master_server_addr: 10.65.0.2:35551 + mooncake_protocol: tcp + client_buffer_size: 1073741824 + idle_timeout_s: 7200 + peer_wait_timeout_s: 7200 diff --git a/examples/disagg/README.md b/examples/disagg/README.md index 6be6e0841..e9cd96aa9 100644 --- a/examples/disagg/README.md +++ b/examples/disagg/README.md @@ -88,7 +88,43 @@ directory, while the wrapper's lifecycle markers stay at the shared run root. The consumer's SQLite/WAL and rank inboxes default to the trainer-node-local `/tmp/specforge/$DISAGG_STORE_ID/consumer-state`; override `DISAGG_CONSUMER_STATE_DIR` or `LOCAL_SCRATCH` when `/tmp` is unsuitable. -Node-local consumer state currently supports one trainer node only. +For a multi-node trainer, set `deployment.disaggregated.inbox_server_url` to a +private HTTP origin on trainer node 0. Rank 0 owns the SQLite/WAL and relays +only tensor-free `SampleRef` metadata to the other trainer nodes; feature +tensors continue to move directly through the selected feature store. + +## Checkpoints without shared storage + +Each distributed rank writes its own `training_state_rankN.pt`. If every +trainer node resolves `output_dir` to the same shared filesystem, no extra +step is required. If `output_dir` is node-local, run the checked-in relay on +both trainer nodes before training. It exchanges rank-local archives over the +private trainer network, verifies SHA-256, and atomically assembles a complete +checkpoint directory on each node: + +```bash +# Trainer node 0 (ranks 0-7) +python examples/disagg/sync_distributed_checkpoints.py \ + --run-root /workspace/runs/$RUN_ID \ + --run-id "$RUN_ID" \ + --local-ranks 0-7 --peer-ranks 8-15 \ + --serve-host 10.0.0.3 --serve-port 35914 \ + --peer-url http://10.0.0.4:35915 \ + --max-archives 3 + +# Trainer node 1 (ranks 8-15) +python examples/disagg/sync_distributed_checkpoints.py \ + --run-root /workspace/runs/$RUN_ID \ + --run-id "$RUN_ID" \ + --local-ranks 8-15 --peer-ranks 0-7 \ + --serve-host 10.0.0.4 --serve-port 35915 \ + --peer-url http://10.0.0.3:35914 \ + --max-archives 3 +``` + +The relay has no authentication or TLS; bind it only to a trusted private +interface. Set `--max-archives` to the same retention window as +`training.max_checkpoints` so relay archives cannot grow without bound. ## External and managed-local services @@ -162,4 +198,7 @@ URL userinfo are redacted. See the [disaggregated training guide](../../docs/basic_usage/disaggregated_training.md) for service prerequisites, recovery rules, and the online/offline data-plane -contracts. +contracts. Kimi-K3's two-replica production recipes use +`run_kimi_k3_dspark_capture_server.sh` on each TP8 capture node; set the +node-local model path and routable capture/Mooncake addresses through the +script's required environment variables. diff --git a/examples/disagg/run_kimi_k3_dspark_capture_server.sh b/examples/disagg/run_kimi_k3_dspark_capture_server.sh new file mode 100755 index 000000000..04eb5a946 --- /dev/null +++ b/examples/disagg/run_kimi_k3_dspark_capture_server.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Launch one Kimi-K3 TP8 DSpark capture replica for the pipelined recipes. + +set -euo pipefail + +MODEL_PATH=${MODEL_PATH:?set MODEL_PATH to the node-local Kimi-K3 snapshot} +CAPTURE_IP=${CAPTURE_IP:?set CAPTURE_IP to this node routable address} +MOONCAKE_MASTER_IP=${MOONCAKE_MASTER_IP:?set MOONCAKE_MASTER_IP} +SGLANG_ROOT=${SGLANG_ROOT:?set SGLANG_ROOT to the patched SGLang checkout} +SERVER_PORT=${SERVER_PORT:-30000} +AUX_LAYER_IDS=${AUX_LAYER_IDS:-"11 23 47 71 83"} +MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16} +MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728} +MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960} +MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80} + +[[ -f "$MODEL_PATH/config.json" ]] || { + printf 'missing target config: %s/config.json\n' "$MODEL_PATH" >&2 + exit 1 +} +[[ -f "$SGLANG_ROOT/python/sglang/srt/spec_capture_sink.py" ]] || { + printf 'SGLang checkout is not patched for spec capture: %s\n' "$SGLANG_ROOT" >&2 + exit 1 +} + +export PYTHONPATH="$SGLANG_ROOT/python${PYTHONPATH:+:$PYTHONPATH}" +export MOONCAKE_MASTER_SERVER_ADDR="$MOONCAKE_MASTER_IP:35551" +export MOONCAKE_METADATA_SERVER="http://$MOONCAKE_MASTER_IP:35880/metadata" +export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" +export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" +export MC_TRANSFER_TIMEOUT=${MC_TRANSFER_TIMEOUT:-300} +export MOONCAKE_PROTOCOL=${MOONCAKE_PROTOCOL:-tcp} +export MOONCAKE_GLOBAL_SEGMENT_SIZE=${MOONCAKE_GLOBAL_SEGMENT_SIZE:-1099511627776} +export MOONCAKE_LOCAL_BUFFER_SIZE=${MOONCAKE_LOCAL_BUFFER_SIZE:-1073741824} +export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7} +# Retain at most two host-side capture batches: Mooncake publishes batch N on +# its background writer while TP8 computes prefill N+1. Larger queues retain +# tens of GiB per batch without increasing a single writer's throughput. +export SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES=${SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2} + +# AUX_LAYER_IDS is a trusted operator-provided whitespace-separated integer +# list and intentionally expands to separate CLI values. Capture workers spend +# almost all of their time in full-prompt prefill, so skip the long K3 decode +# CUDA-graph compile that cannot accelerate the one token completing /generate. +# Leave SGLang symmetric memory off: the latest K3 branch can then auto-enable +# its fused CustomAllReduceV2 path; symmetric memory disables that faster path. +# shellcheck disable=SC2086 +exec python3 -m sglang.launch_server \ + --host 0.0.0.0 \ + --port "$SERVER_PORT" \ + --model-path "$MODEL_PATH" \ + --trust-remote-code \ + --skip-tokenizer-init \ + --tp-size 8 \ + --mem-fraction-static 0.76 \ + --context-length 4608 \ + --max-running-requests "$MAX_RUNNING_REQUESTS" \ + --max-total-tokens "$MAX_TOTAL_TOKENS" \ + --max-prefill-tokens "$MAX_PREFILL_TOKENS" \ + --prefill-attention-backend flashinfer \ + --decode-attention-backend trtllm_mla \ + --moe-runner-backend marlin \ + --mamba-radix-cache-strategy extra_buffer \ + --max-mamba-cache-size "$MAX_MAMBA_CACHE_SIZE" \ + --disable-cuda-graph \ + --chunked-prefill-size -1 \ + --enable-spec-capture \ + --spec-capture-method dspark \ + --spec-capture-aux-layer-ids $AUX_LAYER_IDS diff --git a/examples/disagg/sync_distributed_checkpoints.py b/examples/disagg/sync_distributed_checkpoints.py new file mode 100755 index 000000000..3437b4e17 --- /dev/null +++ b/examples/disagg/sync_distributed_checkpoints.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python3 +"""Mirror rank-local SpecForge checkpoints between two trainer nodes. + +SpecForge writes ``training_state.pt`` on rank 0 and one +``training_state_rankN.pt`` file per rank. On platforms without a shared output +filesystem, each trainer node therefore owns only part of a multi-node +checkpoint. Run one relay on each node to package the files written locally, +serve them over the private trainer network, fetch the peer package, and +atomically assemble a complete checkpoint directory on both nodes. + +The HTTP server intentionally has no authentication or TLS. Bind it only to a +trusted private network interface and do not expose its port publicly. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import signal +import tarfile +import threading +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + +CHUNK_BYTES = 8 * 1024 * 1024 +STATE_FILE = "training_state.pt" + + +class _PrivateHTTPServer(ThreadingHTTPServer): + request_queue_size = 64 + daemon_threads = True + + +class _QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, _format, *_args) -> None: + return + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(CHUNK_BYTES): + digest.update(chunk) + return digest.hexdigest() + + +def _atomic_json(path: Path, payload: object) -> None: + tmp = path.with_name(path.name + ".tmp") + with tmp.open("w", encoding="utf-8") as stream: + json.dump(payload, stream, sort_keys=True) + stream.flush() + os.fsync(stream.fileno()) + os.replace(tmp, path) + + +def _atomic_text(path: Path, value: str) -> None: + tmp = path.with_name(path.name + ".tmp") + with tmp.open("w", encoding="utf-8") as stream: + stream.write(value) + stream.flush() + os.fsync(stream.fileno()) + os.replace(tmp, path) + + +def _rank_range(value: str) -> tuple[int, ...]: + match = re.fullmatch(r"(\d+)-(\d+)", value) + if match is None: + raise argparse.ArgumentTypeError("rank range must look like 0-7") + first, last = (int(item) for item in match.groups()) + if first < 0 or last < first: + raise argparse.ArgumentTypeError("rank range must be increasing") + return tuple(range(first, last + 1)) + + +def _positive_int(value: str) -> int: + parsed = int(value) + if parsed < 1: + raise argparse.ArgumentTypeError("value must be at least 1") + return parsed + + +class CheckpointRelay: + def __init__(self, args: argparse.Namespace) -> None: + self.run_root = Path(args.run_root).resolve() + self.output_dir = self.run_root / "output" + self.relay_dir = self.run_root / "checkpoint-relay" + self.run_id = args.run_id + self.local_ranks = tuple(args.local_ranks) + self.peer_ranks = tuple(args.peer_ranks) + self.peer_url = args.peer_url.rstrip("/") + self.poll_s = float(args.poll_s) + self.max_archives = int(args.max_archives) + self.relay_dir.mkdir(parents=True, exist_ok=True) + self._stop = threading.Event() + handler = partial(_QuietHandler, directory=str(self.relay_dir)) + self._httpd = _PrivateHTTPServer( + (args.serve_host, int(args.serve_port)), handler + ) + self._server_thread = threading.Thread( + target=self._httpd.serve_forever, + name="checkpoint-relay-http", + daemon=True, + ) + self._step_pattern = re.compile(rf"^{re.escape(self.run_id)}-step(\d+)$") + self._local_archive_pattern = self._archive_pattern(self.local_ranks) + self._peer_archive_pattern = self._archive_pattern(self.peer_ranks) + + def _archive_pattern(self, ranks: tuple[int, ...]) -> re.Pattern[str]: + return re.compile( + rf"^{re.escape(self.run_id)}-step(\d+)-" + rf"ranks{ranks[0]}-{ranks[-1]}\.tar$" + ) + + def _local_names(self) -> tuple[str, ...]: + names = [f"training_state_rank{rank}.pt" for rank in self.local_ranks] + if 0 in self.local_ranks: + names.insert(0, STATE_FILE) + return tuple(names) + + def _peer_names(self) -> tuple[str, ...]: + names = [f"training_state_rank{rank}.pt" for rank in self.peer_ranks] + if 0 in self.peer_ranks: + names.insert(0, STATE_FILE) + return tuple(names) + + def _checkpoint_dirs(self) -> list[tuple[int, Path]]: + found = [] + try: + children = list(self.output_dir.iterdir()) + except FileNotFoundError: + return [] + for child in children: + match = self._step_pattern.fullmatch(child.name) + if match is not None and child.is_dir(): + found.append((int(match.group(1)), child)) + return sorted(found) + + def _archive_name(self, step: int) -> str: + return ( + f"{self.run_id}-step{step}-" + f"ranks{self.local_ranks[0]}-{self.local_ranks[-1]}.tar" + ) + + def _local_archives(self) -> list[tuple[int, Path]]: + found = [] + for path in self.relay_dir.iterdir(): + match = self._local_archive_pattern.fullmatch(path.name) + if match is not None and path.is_file(): + found.append((int(match.group(1)), path)) + return sorted(found) + + def _prune_local_archives(self, keep: set[str]) -> None: + for path in self.relay_dir.iterdir(): + if self._local_archive_pattern.fullmatch(path.name) is None: + continue + if path.name in keep: + continue + path.unlink(missing_ok=True) + path.with_name(path.name + ".sha256").unlink(missing_ok=True) + + def _prune_peer_archives(self, keep: set[str]) -> None: + for path in self.relay_dir.iterdir(): + if not path.name.startswith("peer-"): + continue + archive_name = path.name.removeprefix("peer-") + if archive_name.endswith(".partial"): + archive_name = archive_name.removesuffix(".partial") + if self._peer_archive_pattern.fullmatch(archive_name) is None: + continue + if archive_name not in keep: + path.unlink(missing_ok=True) + + def _publish_local(self) -> None: + local_names = self._local_names() + checkpoint_dirs = self._checkpoint_dirs()[-self.max_archives :] + for step, checkpoint_dir in checkpoint_dirs: + sources = [checkpoint_dir / name for name in local_names] + if not all( + path.is_file() and path.stat().st_size > 0 for path in sources + ): + continue + archive = self.relay_dir / self._archive_name(step) + sha_path = archive.with_name(archive.name + ".sha256") + if not archive.is_file(): + tmp = archive.with_name(archive.name + ".tmp") + with tarfile.open(tmp, mode="w") as bundle: + for source in sources: + bundle.add(source, arcname=source.name, recursive=False) + os.replace(tmp, archive) + _atomic_text(sha_path, _sha256(archive)) + print( + f"PACKED step={step} bytes={archive.stat().st_size} " + f"archive={archive.name}", + flush=True, + ) + archives = self._local_archives()[-self.max_archives :] + self._prune_local_archives({archive.name for _, archive in archives}) + entries = [] + for step, archive in archives: + sha_path = archive.with_name(archive.name + ".sha256") + try: + archive_sha = sha_path.read_text(encoding="utf-8").strip() + except FileNotFoundError: + archive_sha = _sha256(archive) + _atomic_text(sha_path, archive_sha) + entries.append( + { + "step": step, + "archive": archive.name, + "sha256": archive_sha, + "files": list(local_names), + } + ) + _atomic_json( + self.relay_dir / "manifest.json", + {"run_id": self.run_id, "entries": entries}, + ) + + def _peer_manifest(self) -> dict | None: + try: + with urlopen(f"{self.peer_url}/manifest.json", timeout=5.0) as response: + payload = json.load(response) + except (HTTPError, URLError, OSError, TimeoutError, ValueError): + return None + if payload.get("run_id") != self.run_id: + return None + return payload + + def _download(self, name: str, expected_sha: str) -> Path: + match = self._peer_archive_pattern.fullmatch(name) + if match is None or Path(name).name != name: + raise ValueError(f"unexpected peer archive name {name!r}") + destination = self.relay_dir / f"peer-{name}" + if destination.is_file() and _sha256(destination) == expected_sha: + return destination + partial_path = destination.with_name(destination.name + ".partial") + digest = hashlib.sha256() + with ( + urlopen(f"{self.peer_url}/{name}", timeout=300.0) as response, + partial_path.open("wb") as stream, + ): + while chunk := response.read(CHUNK_BYTES): + stream.write(chunk) + digest.update(chunk) + if digest.hexdigest() != expected_sha: + partial_path.unlink(missing_ok=True) + raise ValueError(f"SHA-256 mismatch for peer archive {name}") + os.replace(partial_path, destination) + return destination + + def _install_peer_archive(self, entry: dict) -> None: + step = int(entry["step"]) + archive_name = str(entry["archive"]) + archive_match = self._peer_archive_pattern.fullmatch(archive_name) + if archive_match is None or int(archive_match.group(1)) != step: + raise ValueError( + f"unexpected peer archive for step {step}: {archive_name!r}" + ) + expected_names = set(self._peer_names()) + if set(entry.get("files", ())) != expected_names: + raise ValueError(f"unexpected peer file set at step {step}") + checkpoint_dir = self.output_dir / f"{self.run_id}-step{step}" + if not checkpoint_dir.is_dir(): + return + marker = checkpoint_dir / ( + f".checkpoint-relay-ranks{self.peer_ranks[0]}-" + f"{self.peer_ranks[-1]}.json" + ) + expected_sha = str(entry["sha256"]) + try: + with marker.open(encoding="utf-8") as stream: + if json.load(stream).get("sha256") == expected_sha: + return + except (FileNotFoundError, OSError, ValueError): + pass + archive = self._download(archive_name, expected_sha) + with tarfile.open(archive, mode="r") as bundle: + members = bundle.getmembers() + names = {member.name for member in members} + if names != expected_names or any( + not member.isfile() for member in members + ): + raise ValueError(f"unsafe or incomplete peer archive {archive.name}") + for member in members: + source = bundle.extractfile(member) + if source is None: + raise ValueError(f"cannot read {member.name} from {archive.name}") + destination = checkpoint_dir / member.name + tmp = destination.with_name(destination.name + ".relay-tmp") + with source, tmp.open("wb") as stream: + while chunk := source.read(CHUNK_BYTES): + stream.write(chunk) + os.replace(tmp, destination) + _atomic_json( + marker, + {"sha256": expected_sha, "archive": entry["archive"], "step": step}, + ) + print( + f"INSTALLED step={step} peer_files={len(expected_names)} " + f"sha256={expected_sha}", + flush=True, + ) + + def _pull_peer(self) -> None: + manifest = self._peer_manifest() + if manifest is None: + return + entries = sorted( + manifest.get("entries", ()), key=lambda item: int(item["step"]) + )[-self.max_archives :] + self._prune_peer_archives( + {str(entry["archive"]) for entry in entries} + ) + for entry in entries: + self._install_peer_archive(entry) + + def stop(self, *_args) -> None: + self._stop.set() + + def run(self) -> None: + signal.signal(signal.SIGTERM, self.stop) + signal.signal(signal.SIGINT, self.stop) + self._server_thread.start() + print( + f"STARTED run={self.run_id} local_ranks={self.local_ranks[0]}-" + f"{self.local_ranks[-1]} peer={self.peer_url} " + f"max_archives={self.max_archives}", + flush=True, + ) + try: + while not self._stop.is_set(): + try: + self._publish_local() + self._pull_peer() + except Exception as exc: # noqa: BLE001 - retry loop boundary + print(f"RETRY {type(exc).__name__}: {exc}", flush=True) + self._stop.wait(self.poll_s) + finally: + self._httpd.shutdown() + self._httpd.server_close() + self._server_thread.join(timeout=5.0) + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--run-root", required=True) + parser.add_argument("--run-id", required=True) + parser.add_argument("--local-ranks", required=True, type=_rank_range) + parser.add_argument("--peer-ranks", required=True, type=_rank_range) + parser.add_argument("--serve-host", required=True) + parser.add_argument("--serve-port", required=True, type=int) + parser.add_argument("--peer-url", required=True) + parser.add_argument("--poll-s", type=float, default=15.0) + parser.add_argument("--max-archives", type=_positive_int, default=3) + return parser.parse_args() + + +if __name__ == "__main__": + CheckpointRelay(_parse_args()).run() diff --git a/patches/sglang/kimi-k3-f8493a4/spec-capture.patch b/patches/sglang/kimi-k3-f8493a4/spec-capture.patch index 93c5f499e..dcb3c4870 100644 --- a/patches/sglang/kimi-k3-f8493a4/spec-capture.patch +++ b/patches/sglang/kimi-k3-f8493a4/spec-capture.patch @@ -1,5 +1,5 @@ diff --git a/python/sglang/srt/layers/attn_residual.py b/python/sglang/srt/layers/attn_residual.py -index 266d2f2..2a75af7 100644 +index 266d2f2cd..2a75af790 100644 --- a/python/sglang/srt/layers/attn_residual.py +++ b/python/sglang/srt/layers/attn_residual.py @@ -115,10 +115,28 @@ def _score_kernel( @@ -44,8 +44,8 @@ index 266d2f2..2a75af7 100644 + dotv += tl.sum(v_scaled * cw) + rrms = 1.0 / tl.sqrt(sumsq / H + eps / scale / scale) tl.store(scores_ptr + pid_t * stride_sm + j, dotv * rrms) - - + + @@ -159,7 +178,8 @@ def _combine_kernel( Softmax is redundantly computed by each H-chunk CTA (≤16 elements, trivial). This gives full H-parallelism: 7 CTAs for H=7168/1024. @@ -55,22 +55,22 @@ index 266d2f2..2a75af7 100644 + pid_t = tl.program_id(0).to(tl.int64) pid_h = tl.program_id(1) h0 = pid_h * BLOCK_H - + diff --git a/python/sglang/srt/layers/logits_processor.py b/python/sglang/srt/layers/logits_processor.py -index cf55b97..51a1872 100644 +index 480ed81a5..f84d489a1 100644 --- a/python/sglang/srt/layers/logits_processor.py +++ b/python/sglang/srt/layers/logits_processor.py -@@ -159,6 +159,9 @@ class LogitsProcessorOutput: +@@ -163,6 +163,9 @@ class LogitsProcessorOutput: # Used by speculative decoding (EAGLE) # The last hidden layers hidden_states: Optional[torch.Tensor] = None + # Spec-training capture: under FULL+aux, `hidden_states` is the aux + # concatenation, so the post-norm last hidden is exposed here separately. + last_hidden_states: Optional[torch.Tensor] = None - + ## Part 2: This part will be assigned in python/sglang/srt/layers/sampler.py::Sampler # he log probs of output tokens, if SGLANG_RETURN_ORIGINAL_LOGPROB = True, will get the log probs before applying temperature. If False, will get the log probs before applying temperature. -@@ -443,6 +446,16 @@ class LogitsProcessor(nn.Module): +@@ -447,6 +450,16 @@ class LogitsProcessor(nn.Module): sample_indices, logits_metadata, ) @@ -85,9 +85,9 @@ index cf55b97..51a1872 100644 + else None + ) del hidden_states - + if not logits_metadata.extend_return_logprob: -@@ -456,6 +469,7 @@ class LogitsProcessor(nn.Module): +@@ -460,6 +473,7 @@ class LogitsProcessor(nn.Module): return LogitsProcessorOutput( next_token_logits=sampled_logits, hidden_states=hidden_states_to_store, @@ -96,7 +96,7 @@ index cf55b97..51a1872 100644 # workaround since ForwardBatch is local to forward_batch_generation(). # They should be moved to GenerationBatchResult to keep this class clean. diff --git a/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py b/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py -index 1bf6aa7..e7a3030 100644 +index 1bf6aa7d2..e7a303074 100644 --- a/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py +++ b/python/sglang/srt/layers/moe/fused_moe_triton/fused_marlin_moe.py @@ -402,6 +402,9 @@ def fused_marlin_moe( @@ -108,12 +108,12 @@ index 1bf6aa7..e7a3030 100644 + and intermediate_cache3.shape[0] <= 65_535 ): from sglang.kernels.ops.moe.moe_topk_sum import moe_topk_sum - + diff --git a/python/sglang/srt/managers/detokenizer_manager.py b/python/sglang/srt/managers/detokenizer_manager.py -index 7b3ca52..84e0ae6 100644 +index 19a7f4938..b3c5220f3 100644 --- a/python/sglang/srt/managers/detokenizer_manager.py +++ b/python/sglang/srt/managers/detokenizer_manager.py -@@ -472,6 +472,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): +@@ -475,6 +475,7 @@ class DetokenizerManager(MultiHttpWorkerDetokenizerMixin): output_token_sampling_mask=recv_obj.output_token_sampling_mask, output_token_sampling_logprobs=recv_obj.output_token_sampling_logprobs, output_hidden_states=recv_obj.output_hidden_states, @@ -122,13 +122,13 @@ index 7b3ca52..84e0ae6 100644 indexer_topk=indexer_topk, customized_info=recv_obj.customized_info, diff --git a/python/sglang/srt/managers/io_struct.py b/python/sglang/srt/managers/io_struct.py -index fa359c5..d9881c0 100644 +index 06282f331..4675de4ef 100644 --- a/python/sglang/srt/managers/io_struct.py +++ b/python/sglang/srt/managers/io_struct.py -@@ -303,6 +303,10 @@ class GenerateReqInput: +@@ -309,6 +309,10 @@ class GenerateReqInput: # For Unlimited-OCR images_config: Optional[dict] = None - + + # Spec-training capture sink instructions (see spec_capture_sink.py). + # Batch-level: List[Optional[dict]]; per-request after __getitem__. + spec_capture: Optional[Union[List[Optional[Dict]], Dict]] = None @@ -136,7 +136,7 @@ index fa359c5..d9881c0 100644 # Pre-computed delimiter indices for multi-item scoring. # Batch-level: List[List[int]] (one per request). After __getitem__: List[int]. multi_item_delimiter_indices: Optional[Union[List[List[int]], List[int]]] = None -@@ -780,6 +784,11 @@ class GenerateReqInput: +@@ -804,6 +808,11 @@ class GenerateReqInput: if self.multi_item_delimiter_indices is not None else None ), @@ -148,41 +148,41 @@ index fa359c5..d9881c0 100644 ) cache[i] = sub return sub -@@ -874,6 +883,9 @@ class TokenizedGenerateReqInput(BaseReq, kw_only=True): +@@ -902,6 +911,9 @@ class TokenizedGenerateReqInput(BaseReq, kw_only=True): # Internal IPC only. encoder_urls: Optional[List[str]] = None - + + # Spec-training capture sink instructions (see GenerateReqInput.spec_capture) + spec_capture: Optional[Dict] = None + # Pre-computed delimiter indices for multi-item scoring multi_item_delimiter_indices: Optional[List[int]] = None - -@@ -1267,6 +1279,9 @@ class BatchTokenIDOutput(BaseBatchReq, kw_only=True): + +@@ -1328,6 +1340,9 @@ class BatchTokenIDOutput(BaseBatchReq, kw_only=True): # Number of times each request was retracted. retraction_counts: Optional[List[int]] = None - + + # Spec-training capture: one result dict per request (see spec_capture_sink). + spec_capture: Optional[List[Any]] = None + # The trainer step id. Used to know which step's weights are used for sampling. token_steps: Optional[List[List[int]]] = None - -@@ -1349,6 +1364,9 @@ class BatchStrOutput(BaseBatchReq, kw_only=True): + +@@ -1419,6 +1434,9 @@ class BatchStrOutput(BaseBatchReq, kw_only=True): # Number of times each request was retracted. retraction_counts: Optional[List[int]] = None - + + # Spec-training capture: one result dict per request (see spec_capture_sink). + spec_capture: Optional[List[Any]] = None + # The trainer step id. Used to know which step's weights are used for sampling. token_steps: Optional[List[List[int]]] = None - + diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py -index af2eca5..1b50341 100755 +index 0ef94dd95..d3cada1fa 100755 --- a/python/sglang/srt/managers/schedule_batch.py +++ b/python/sglang/srt/managers/schedule_batch.py -@@ -759,6 +759,7 @@ class Req(ReqDllmMixin): +@@ -768,6 +768,7 @@ class Req(ReqDllmMixin): Union[APIServerReqTimeStats, DPControllerReqTimeStats] ] = None, return_pooled_hidden_states: bool = False, @@ -190,7 +190,7 @@ index af2eca5..1b50341 100755 multi_item_delimiter_indices: Optional[List[int]] = None, session_id: Optional[str] = None, ): -@@ -821,7 +822,13 @@ class Req(ReqDllmMixin): +@@ -832,7 +833,13 @@ class Req(ReqDllmMixin): } self.sampling_params = sampling_params self.custom_logit_processor = custom_logit_processor @@ -202,17 +202,17 @@ index af2eca5..1b50341 100755 + self.spec_capture_aux: List[torch.Tensor] = [] + self.spec_capture_last_hidden: List[torch.Tensor] = [] + self.spec_capture_result = None # per-request sink result -> output field - + # extra key for classifying the request (e.g. cache_salt) if lora_id is not None: diff --git a/python/sglang/srt/managers/scheduler.py b/python/sglang/srt/managers/scheduler.py -index ef024da..6ab1dce 100644 +index 6589c6a78..d31be1b92 100644 --- a/python/sglang/srt/managers/scheduler.py +++ b/python/sglang/srt/managers/scheduler.py -@@ -585,6 +585,19 @@ class Scheduler( - +@@ -637,6 +637,19 @@ class Scheduler( + self.init_batch_result_processor() - + + if server_args.enable_spec_capture: + # Capture needs single-pass prefill; chunking would drop all but the + # final chunk's hidden rows. @@ -227,23 +227,148 @@ index ef024da..6ab1dce 100644 + spec_capture_sink.maybe_init_sink(server_args) + self.is_initializing = False - + def init_zbal_on_npu(self): -@@ -2196,6 +2209,7 @@ class Scheduler( +@@ -2283,6 +2296,7 @@ class Scheduler( dllm_config=self.dllm_config, time_stats=recv_req.time_stats, multi_item_delimiter_indices=recv_req.multi_item_delimiter_indices, + spec_capture=recv_req.spec_capture, ) req.tokenizer = self.tokenizer - + +@@ -3385,6 +3399,13 @@ class Scheduler( + # GenerationBatchResult.extra_keep_alive_refs after forward returns. + self.batch_record_buf[self.batch_record_ct] = [batch, attr_snapshot] + ++ def _should_copy_hidden_states_to_cpu(self, batch: ScheduleBatch) -> bool: ++ """Avoid redundant multi-GiB capture D2H on non-writer TP ranks.""" ++ return batch.return_hidden_states and ( ++ self.ps.attn_tp_rank == 0 ++ or any(req.spec_capture is None for req in batch.reqs) ++ ) ++ + @contextmanager + def _forward_isolation(self, batch: ScheduleBatch, *, overlap: bool): + """Make SB transactional across one forward (overlap and non-overlap). +@@ -3531,7 +3552,9 @@ class Scheduler( + # overlaps. + batch_result.copy_to_cpu( + return_logprob=batch.return_logprob, +- return_hidden_states=batch.return_hidden_states, ++ return_hidden_states=self._should_copy_hidden_states_to_cpu( ++ batch ++ ), + ) + else: + # Result D2H on copy_stream overlaps the next forward +@@ -3541,7 +3564,9 @@ class Scheduler( + with self.copy_stream_ctx: + batch_result.copy_to_cpu( + return_logprob=batch.return_logprob, +- return_hidden_states=batch.return_hidden_states, ++ return_hidden_states=self._should_copy_hidden_states_to_cpu( ++ batch ++ ), + ) + else: + batch_result.future_indices = future_indices +@@ -3580,7 +3605,9 @@ class Scheduler( + batch_result.copy_done = self.device_module.Event() + batch_result.copy_to_cpu( + return_logprob=batch.return_logprob, +- return_hidden_states=batch.return_hidden_states, ++ return_hidden_states=self._should_copy_hidden_states_to_cpu( ++ batch ++ ), + ) + else: + kwargs = ( +@@ -3699,7 +3726,9 @@ class Scheduler( + self._relay_forward_payload(batch_result.future_indices, batch_result) + batch_result.copy_to_cpu( + return_logprob=cur_batch.return_logprob, +- return_hidden_states=cur_batch.return_hidden_states, ++ return_hidden_states=self._should_copy_hidden_states_to_cpu( ++ cur_batch ++ ), + ) + + # Release the closure and large GPU tensors that are no longer needed. +@@ -3718,6 +3747,10 @@ class Scheduler( + batch: ScheduleBatch, + result: Union[GenerationBatchResult, EmbeddingBatchResult], + ): ++ # Complete capture transfers on the scheduler thread before processing ++ # any kind of next result (prefill, decode, or idle). This preserves ++ # the output socket's single-thread ownership under mixed traffic. ++ self.batch_result_processor.drain_spec_captures() + self.publish_load_snapshot(force=batch.forward_mode.is_extend()) + + if batch.forward_mode.is_decode(): +@@ -3826,6 +3859,15 @@ class Scheduler( + + def on_idle(self): + """Idle housekeeping: guard, check, metrics, reset, sleep.""" ++ # A capture response is intentionally delayed until its background ++ # Mooncake batch write completes. Poll it on the scheduler thread so ++ # the ZeroMQ output socket remains single-threaded. Do not enter the ++ # idle sleeper while a future is outstanding or the producer waiting ++ # for that response could deadlock. ++ self.batch_result_processor.drain_spec_captures() ++ if self.batch_result_processor.has_pending_spec_captures(): ++ time.sleep(0.001) ++ return + if not self.is_fully_idle(): + return + +@@ -3879,6 +3921,7 @@ class Scheduler( + and not self.dllm_manager.any_staging_reqs() + and (self.last_batch is None or self.last_batch.is_empty()) + and (not self.enable_overlap or len(self.result_queue) == 0) ++ and not self.batch_result_processor.has_pending_spec_captures() + and self._pp_microbatches_drained() + ) + diff --git a/python/sglang/srt/managers/scheduler_components/batch_result_processor.py b/python/sglang/srt/managers/scheduler_components/batch_result_processor.py -index 248a929..b2d8f42 100644 +index f75eab004..5af4c1d6b 100644 --- a/python/sglang/srt/managers/scheduler_components/batch_result_processor.py +++ b/python/sglang/srt/managers/scheduler_components/batch_result_processor.py -@@ -263,6 +263,18 @@ class SchedulerBatchResultProcessor: +@@ -1,7 +1,9 @@ + from __future__ import annotations + + import logging +-from dataclasses import dataclass ++import os ++import time ++from dataclasses import dataclass, field + from typing import ( + TYPE_CHECKING, + Callable, +@@ -87,6 +89,12 @@ class SchedulerBatchResultProcessor: + logprob_result_processor: SchedulerLogprobResultProcessor + output_streamer: SchedulerOutputStreamer + abort_request: Callable ++ _spec_capture_batches: List = field( ++ default_factory=list, ++ init=False, ++ repr=False, ++ compare=False, ++ ) + + def process_batch_result_prebuilt(self, batch: ScheduleBatch): + assert self.disaggregation_mode == DisaggregationMode.DECODE +@@ -190,6 +198,7 @@ class SchedulerBatchResultProcessor: + result: Union[GenerationBatchResult, EmbeddingBatchResult], + ): + skip_stream_req = None ++ pending_spec_captures = [] + + if self.is_generation: + if result.copy_done is not None: +@@ -269,6 +278,20 @@ class SchedulerBatchResultProcessor: self.add_sampling_mask_return_values(i, req, logits_output) - + if ( + req.spec_capture is not None + and logits_output.hidden_states is not None @@ -255,15 +380,46 @@ index 248a929..b2d8f42 100644 + hidden_state_offset=hidden_state_offset, + ) + if req.finished(): -+ self._sink_spec_capture(req) ++ pending = self._sink_spec_capture(req) ++ if pending is not None: ++ pending_spec_captures.append(pending) + elif ( req.return_hidden_states and logits_output.hidden_states is not None ): -@@ -470,6 +482,62 @@ class SchedulerBatchResultProcessor: +@@ -343,9 +366,25 @@ class SchedulerBatchResultProcessor: + req.inflight_middle_chunks -= 1 + req.time_stats.set_last_chunked_prefill_finish_time() + +- self.output_streamer.stream_output( +- batch.reqs, batch.return_logprob, skip_stream_req +- ) ++ if pending_spec_captures: ++ self._queue_spec_captures( ++ pending_spec_captures, ++ return_logprob=batch.return_logprob, ++ ) ++ capture_req_ids = {id(item[0]) for item in pending_spec_captures} ++ ready_reqs = [ ++ req ++ for req in batch.reqs ++ if id(req) not in capture_req_ids and req is not skip_stream_req ++ ] ++ if ready_reqs: ++ self.output_streamer.stream_output( ++ ready_reqs, batch.return_logprob ++ ) ++ else: ++ self.output_streamer.stream_output( ++ batch.reqs, batch.return_logprob, skip_stream_req ++ ) + + can_run_cuda_graph = result.can_run_cuda_graph + self.metrics_reporter.report_prefill_stats( +@@ -476,6 +515,181 @@ class SchedulerBatchResultProcessor: f"Placeholder zeros would be appended to output_ids." ) - + + def _append_spec_capture_states( + self, + *, @@ -279,16 +435,37 @@ index 248a929..b2d8f42 100644 + """ + start = hidden_state_offset + end = start + len(req.origin_input_ids) -+ req.spec_capture_aux.append( -+ logits_output.hidden_states[start:end].cpu().clone() ++ # Only attention-TP rank 0 owns the Mooncake sink. Copying these very ++ # large tensors to host on every TP rank creates eight identical D2H ++ # transfers and seven immediately-discarded CPU copies on TP8. ++ if self.output_streamer.ps.attn_tp_rank != 0: ++ return end ++ # Materialize each scheduler result on host once, rather than issuing ++ # one synchronous D2H transfer per request. In overlap mode the writer ++ # rank has already copied both tensors asynchronously on copy_stream; ++ # ``.cpu()`` is then a no-op. The fallback keeps non-overlap correct. ++ features = dict(req.spec_capture.get("features") or {}) ++ aux_cpu = getattr(logits_output, "_spec_capture_aux_cpu", None) ++ if "aux" in features and aux_cpu is None: ++ aux_cpu = logits_output.hidden_states.cpu() ++ logits_output._spec_capture_aux_cpu = aux_cpu ++ last_hidden_cpu = getattr( ++ logits_output, "_spec_capture_last_hidden_cpu", None + ) -+ if logits_output.last_hidden_states is not None: -+ req.spec_capture_last_hidden.append( -+ logits_output.last_hidden_states[start:end].cpu().clone() -+ ) ++ if ( ++ "last_hidden" in features ++ and logits_output.last_hidden_states is not None ++ and last_hidden_cpu is None ++ ): ++ last_hidden_cpu = logits_output.last_hidden_states.cpu() ++ logits_output._spec_capture_last_hidden_cpu = last_hidden_cpu ++ if "aux" in features and aux_cpu is not None: ++ req.spec_capture_aux.append(aux_cpu[start:end]) ++ if "last_hidden" in features and last_hidden_cpu is not None: ++ req.spec_capture_last_hidden.append(last_hidden_cpu[start:end]) + return end + -+ def _sink_spec_capture(self, req: Req) -> None: ++ def _sink_spec_capture(self, req: Req): + """Write a finished capture request's tensors to the Mooncake sink. + + Runs on the attention-TP rank that streams output; the per-request result @@ -300,34 +477,147 @@ index 248a929..b2d8f42 100644 + + sink = spec_capture_sink.get_sink() + if sink is None or self.output_streamer.ps.attn_tp_rank != 0: ++ return None ++ timing_enabled = os.environ.get("SGLANG_SPEC_CAPTURE_TIMING", "0") == "1" ++ cat_start = time.perf_counter() ++ # With chunked prefill disabled (the K3 training configuration), each ++ # request owns exactly one contiguous view into the batch-level pinned ++ # D2H buffer. torch.cat([view]) needlessly copied the whole capture a ++ # second time on CPU -- about 18 GiB per 128 reference samples. Keep ++ # that view zero-copy; concatenate only the genuinely chunked case. ++ aux = self._coalesce_spec_capture_chunks(req.spec_capture_aux) ++ last_hidden = self._coalesce_spec_capture_chunks( ++ req.spec_capture_last_hidden ++ ) ++ cat_ms = (time.perf_counter() - cat_start) * 1000.0 ++ req.spec_capture_aux = [] ++ req.spec_capture_last_hidden = [] ++ return req, req.spec_capture, aux, last_hidden, timing_enabled, cat_ms ++ ++ def _queue_spec_captures(self, pending, *, return_logprob: bool) -> None: ++ """Queue one batch transfer while the scheduler runs the next prefill.""" ++ if not pending: + return -+ aux = torch.cat(req.spec_capture_aux, dim=0) if req.spec_capture_aux else None -+ last_hidden = ( -+ torch.cat(req.spec_capture_last_hidden, dim=0) -+ if req.spec_capture_last_hidden -+ else None ++ from sglang.srt import spec_capture_sink ++ ++ sink = spec_capture_sink.get_sink() ++ samples = [ ++ (spec, aux, last_hidden) ++ for _, spec, aux, last_hidden, _, _ in pending ++ ] ++ self._spec_capture_batches.append( ++ ( ++ pending, ++ sink.submit_samples(samples), ++ return_logprob, ++ time.perf_counter(), ++ ) + ) -+ try: -+ req.spec_capture_result = sink.put_sample( -+ req.spec_capture, aux=aux, last_hidden=last_hidden ++ # Bound retained D2H buffers. With producer concurrency=2, reaching ++ # this point means target prefill N+1 already overlapped host transfer ++ # N; wait only if N is still finishing before admitting N+2. ++ max_pending = int( ++ os.environ.get("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES", "2") ++ ) ++ if max_pending < 1: ++ raise ValueError("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES must be >= 1") ++ if len(self._spec_capture_batches) >= max_pending: ++ self.drain_spec_captures(block=True, max_batches=1) ++ ++ def has_pending_spec_captures(self) -> bool: ++ return bool(self._spec_capture_batches) ++ ++ def drain_spec_captures( ++ self, *, block: bool = False, max_batches: Optional[int] = None ++ ) -> int: ++ """Finish ready transfers and stream their responses on this thread.""" ++ completed = 0 ++ while self._spec_capture_batches: ++ if max_batches is not None and completed >= max_batches: ++ break ++ pending, future, return_logprob, queued_at = self._spec_capture_batches[0] ++ if not block and not future.done(): ++ break ++ self._spec_capture_batches.pop(0) ++ self._complete_spec_capture_batch( ++ pending, ++ future, ++ return_logprob=return_logprob, ++ queued_at=queued_at, + ) ++ completed += 1 ++ return completed ++ ++ def _complete_spec_capture_batch( ++ self, pending, future, *, return_logprob: bool, queued_at: float ++ ) -> None: ++ try: ++ results = future.result() ++ if len(results) != len(pending): ++ raise RuntimeError( ++ f"spec-capture sink returned {len(results)} results for " ++ f"{len(pending)} samples" ++ ) + except Exception as e: -+ logger.error("spec-capture sink failed for %s: %s", req.rid, e) -+ req.spec_capture_result = { -+ "sample_id": req.spec_capture.get("sample_id"), -+ "error": str(e), -+ } -+ req.spec_capture_aux = [] -+ req.spec_capture_last_hidden = [] ++ logger.error( ++ "spec-capture batch sink failed for %d requests: %s", len(pending), e ++ ) ++ for req, spec, _, _, _, _ in pending: ++ req.spec_capture_result = { ++ "sample_id": spec.get("sample_id"), ++ "error": str(e), ++ } ++ else: ++ for pending_item, result in zip(pending, results): ++ req, _, _, _, _, _ = pending_item ++ req.spec_capture_result = result ++ ++ timing_enabled = any(item[4] for item in pending) ++ if timing_enabled: ++ logger.info( ++ "[spec-capture-timing] async_batch_complete samples=%d " ++ "queue_to_stream_ms=%.3f cat_ms=%.3f", ++ len(pending), ++ (time.perf_counter() - queued_at) * 1000.0, ++ sum(item[5] for item in pending), ++ ) ++ self.output_streamer.stream_output( ++ [item[0] for item in pending], return_logprob ++ ) ++ ++ @staticmethod ++ def _coalesce_spec_capture_chunks( ++ chunks: List[torch.Tensor], ++ ) -> Optional[torch.Tensor]: ++ if not chunks: ++ return None ++ if len(chunks) == 1: ++ return chunks[0] ++ return torch.cat(chunks, dim=0) + def _append_prefill_hidden_states( self, *, diff --git a/python/sglang/srt/managers/scheduler_components/output_streamer.py b/python/sglang/srt/managers/scheduler_components/output_streamer.py -index 278ccf4..7a9d8d5 100644 +index 9f5b2329a..cce5b4afe 100644 --- a/python/sglang/srt/managers/scheduler_components/output_streamer.py +++ b/python/sglang/srt/managers/scheduler_components/output_streamer.py -@@ -289,6 +289,7 @@ class _GenerationStreamAccumulator: +@@ -162,6 +162,14 @@ class SchedulerOutputStreamer: + for req in reqs: + if req is skip_req: + continue ++ if self.server_args.enable_spec_capture and req.spec_capture is not None: ++ # Capture requests are not complete until the background sink ++ # has durably published every feature object. Several ++ # scheduler paths can ask the common streamer to emit a ++ # finished request; centralize the completion barrier here so ++ # none of them can win the race and send a metadata-less 200. ++ if req.finished() and req.spec_capture_result is None: ++ continue + if req.finished() and req.finished_output: + # With the overlap schedule, a request will try to output twice and hit this line twice + # because of the one additional delayed token. This "continue" prevented the dummy output. +@@ -302,6 +310,7 @@ class _GenerationStreamAccumulator: spec_cap_lens_histogram: list = field(default_factory=list) retraction_counts: list = field(default_factory=list) output_hidden_states: Optional[list] = None @@ -335,7 +625,7 @@ index 278ccf4..7a9d8d5 100644 routed_experts: Optional[list] = None indexer_topk: Optional[list] = None customized_info: dict = field(default_factory=dict) -@@ -525,6 +526,8 @@ class _GenerationStreamAccumulator: +@@ -571,6 +580,8 @@ class _GenerationStreamAccumulator: self.output_hidden_states.append(hs) else: self.output_hidden_states.append(None) @@ -344,7 +634,7 @@ index 278ccf4..7a9d8d5 100644 if self.return_routed_experts: self.routed_experts.append( req.routed_experts if req.return_routed_experts else None -@@ -600,6 +603,7 @@ class _GenerationStreamAccumulator: +@@ -663,6 +674,7 @@ class _GenerationStreamAccumulator: output_token_sampling_mask=self.output_token_sampling_mask, output_token_sampling_logprobs=self.output_token_sampling_logprobs, output_hidden_states=self.output_hidden_states, @@ -353,10 +643,10 @@ index 278ccf4..7a9d8d5 100644 indexer_topk=self.indexer_topk, customized_info=( diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py -index 2228008..15cb30a 100644 +index 41cc4c246..5d429a91f 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py +++ b/python/sglang/srt/managers/tokenizer_manager.py -@@ -1231,6 +1231,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): +@@ -1337,6 +1337,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): multi_item_delimiter_indices=obj.multi_item_delimiter_indices, mm_data_mooncake=obj.mm_data_mooncake, encoder_urls=obj.encoder_urls, @@ -364,7 +654,7 @@ index 2228008..15cb30a 100644 ) elif isinstance(obj, EmbeddingReqInput): # Resolve unresolved embed overrides now that input_ids are available -@@ -2018,6 +2019,10 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): +@@ -2124,6 +2125,10 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin): hidden_states = recv_obj.output_hidden_states[i] if hidden_states is not None: meta_info["hidden_states"] = hidden_states @@ -375,11 +665,26 @@ index 2228008..15cb30a 100644 if getattr(recv_obj, "routed_experts", None): val = recv_obj.routed_experts[i] if val is not None: +diff --git a/python/sglang/srt/managers/utils.py b/python/sglang/srt/managers/utils.py +index fe883c264..2b7206a13 100644 +--- a/python/sglang/srt/managers/utils.py ++++ b/python/sglang/srt/managers/utils.py +@@ -148,6 +148,10 @@ class GenerationBatchResult: + self.logits_output.hidden_states = _async_d2h( + self.logits_output.hidden_states + ) ++ if self.logits_output.last_hidden_states is not None: ++ self.logits_output.last_hidden_states = _async_d2h( ++ self.logits_output.last_hidden_states ++ ) + self.next_token_ids = _async_d2h(self.next_token_ids) + + if self.accept_lens is not None: diff --git a/python/sglang/srt/model_executor/model_runner.py b/python/sglang/srt/model_executor/model_runner.py -index 3d668b2..7d1f889 100644 +index 661bd5ad1..e1fce6365 100644 --- a/python/sglang/srt/model_executor/model_runner.py +++ b/python/sglang/srt/model_executor/model_runner.py -@@ -487,6 +487,26 @@ class ModelRunner: +@@ -489,6 +489,26 @@ class ModelRunner: is_draft_worker=self.is_draft_worker, ) ) @@ -403,10 +708,10 @@ index 3d668b2..7d1f889 100644 + "--spec-capture-method must be one of: eagle3, dflash, dspark; " + f"got {capture_method!r}" + ) - + def init_weight_exporter(self): self.weight_exporter = WeightExporter( -@@ -863,7 +883,13 @@ class ModelRunner: +@@ -866,7 +886,13 @@ class ModelRunner: eagle_aux_hidden_state_layer_ids=self.spec_aux_config.eagle_aux_hidden_state_layer_ids, dflash_use_aux_hidden_state=self.spec_aux_config.dflash_use_aux_hidden_state, dflash_target_layer_ids=self.spec_aux_config.dflash_target_layer_ids, @@ -422,10 +727,10 @@ index 3d668b2..7d1f889 100644 backends = build_attention_backends(model_runner=self) self.attn_backend = backends.attn_backend diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py -index 62a0177..3f40061 100644 +index e9f84cb17..dd64be9f1 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py -@@ -3333,6 +3333,26 @@ class ServerArgs: +@@ -3349,6 +3349,26 @@ class ServerArgs: enable_return_hidden_states: A[ bool, "Enable returning hidden states with responses.", NS("exec.features") ] = False @@ -454,10 +759,10 @@ index 62a0177..3f40061 100644 "Enable returning routed experts of each layer with responses.", diff --git a/python/sglang/srt/spec_capture_sink.py b/python/sglang/srt/spec_capture_sink.py new file mode 100644 -index 0000000..038a084 +index 000000000..8ab06e25d --- /dev/null +++ b/python/sglang/srt/spec_capture_sink.py -@@ -0,0 +1,243 @@ +@@ -0,0 +1,388 @@ +# Copyright 2024 SGLang Team +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. @@ -493,7 +798,9 @@ index 0000000..038a084 +import logging +import os +import threading -+from typing import Any, Dict, List, Optional ++import time ++from concurrent.futures import Future, ThreadPoolExecutor ++from typing import Any, Dict, List, Optional, Tuple + +import torch + @@ -527,9 +834,13 @@ index 0000000..038a084 + self._store = None + self._put_config = None + self._lock = threading.Lock() -+ # Retried HTTP requests reuse deterministic keys. Striped locks keep -+ # replacement atomic per key without retaining one lock per sample. -+ self._write_locks = [threading.Lock() for _ in range(256)] ++ # One store writer is sufficient: Mooncake already stripes a batched ++ # transfer internally. The executor decouples that host transfer from ++ # the scheduler so the next target prefill can run concurrently. ++ self._executor = ThreadPoolExecutor( ++ max_workers=1, ++ thread_name_prefix="spec-capture-batch-put", ++ ) + + # -- connection --------------------------------------------------------- + def _connect(self): @@ -576,80 +887,117 @@ index 0000000..038a084 + def _tkey(store_id: str, sample_id: str, gen: int, name: str) -> str: + return f"{store_id}/{sample_id}/g{gen}/{name}" + -+ def _put_tensor( -+ self, key: str, t: torch.Tensor, *, replace: bool = False -+ ) -> None: -+ store = self._connect() -+ t = t.detach().to("cpu").contiguous() -+ nbytes = t.element_size() * t.numel() -+ lock = self._write_locks[hash(key) % len(self._write_locks)] -+ with lock: -+ if replace: -+ # Do not probe with is_exist(): Mooncake existence checks can -+ # acquire a read lease that prevents the following removal. -+ self._remove_quiet(key) -+ try: -+ store.register_buffer(t.data_ptr(), nbytes) -+ except Exception: -+ pass # some builds auto-register -+ try: -+ rc = store.put_from(key, t.data_ptr(), nbytes, self._put_config) -+ finally: -+ try: -+ store.unregister_buffer(t.data_ptr()) -+ except Exception: -+ pass -+ if rc is not None and int(rc) < 0: -+ raise RuntimeError(f"spec-capture put_from failed (status {rc}) for {key}") -+ + def _remove_quiet(self, key: str) -> None: + try: + self._connect().remove(key) + except Exception: + pass + -+ # -- the one entry point -------------------------------------------------- -+ def put_sample( ++ def _remove_many_quiet(self, keys: List[str]) -> None: ++ if not keys: ++ return ++ store = self._connect() ++ batch_remove = getattr(store, "batch_remove", None) ++ if batch_remove is not None: ++ try: ++ batch_remove(keys) ++ return ++ except Exception: ++ pass ++ for key in keys: ++ self._remove_quiet(key) ++ ++ # -- the batch entry point ------------------------------------------------ ++ def submit_samples( + self, -+ spec: Dict[str, Any], -+ *, -+ aux: Optional[torch.Tensor], -+ last_hidden: Optional[torch.Tensor], -+ ) -> Dict[str, Any]: -+ """Write one sample's artifacts; return the meta_info result dict. ++ samples: List[ ++ Tuple[ ++ Dict[str, Any], Optional[torch.Tensor], Optional[torch.Tensor] ++ ] ++ ], ++ ) -> Future[List[Dict[str, Any]]]: ++ """Queue one scheduler batch without blocking the scheduler thread.""" ++ return self._executor.submit(self.put_samples, samples) + -+ ``aux``/``last_hidden`` are the per-request (L, W) captured tensors, -+ stored with a leading batch dim of 1. On any failure the keys already -+ written are best-effort removed (no partial sample is consumable). ++ def put_samples( ++ self, ++ samples: List[ ++ Tuple[ ++ Dict[str, Any], Optional[torch.Tensor], Optional[torch.Tensor] ++ ] ++ ], ++ ) -> List[Dict[str, Any]]: ++ """Publish a scheduler batch with one native Mooncake batch RPC. ++ ++ A K3 prefill normally finishes 16 samples together. Calling ++ ``put_from`` four times per sample paid 64 metadata/transport round ++ trips on the writer rank and serialized the response behind them. ++ ``batch_put_from`` preserves the existing per-feature keys and raw ++ tensor layout while amortizing that fixed cost across the whole ++ scheduler batch. The response is still emitted only after every ++ status succeeds, so refs can never point at incomplete samples. + """ -+ store_id = str(spec["store_id"]) -+ sample_id = str(spec["sample_id"]) -+ gen = int(spec.get("gen", 1)) -+ replace = bool(spec.get("replace", False)) -+ features: Dict[str, str] = dict(spec.get("features") or {}) ++ if not samples: ++ return [] + -+ written: List[str] = [] -+ result_feats: Dict[str, Dict[str, Any]] = {} ++ store = self._connect() ++ timing_enabled = os.environ.get("SGLANG_SPEC_CAPTURE_TIMING", "0") == "1" ++ started = time.perf_counter() ++ keys: List[str] = [] ++ tensors: List[torch.Tensor] = [] ++ sizes: List[int] = [] ++ replace_keys: List[str] = [] ++ results: List[Dict[str, Any]] = [] + -+ def _write(name: str, t: torch.Tensor) -> None: ++ def _stage( ++ result_feats: Dict[str, Dict[str, Any]], ++ *, ++ store_id: str, ++ sample_id: str, ++ gen: int, ++ replace: bool, ++ name: str, ++ tensor: torch.Tensor, ++ ) -> None: ++ tensor = tensor.detach().to("cpu").contiguous() + key = self._tkey(store_id, sample_id, gen, name) -+ self._put_tensor(key, t, replace=replace) -+ written.append(key) ++ keys.append(key) ++ tensors.append(tensor) ++ sizes.append(tensor.element_size() * tensor.numel()) ++ if replace: ++ replace_keys.append(key) + result_feats[name] = { -+ "shape": list(t.shape), -+ "dtype": _DTYPE_STR.get(t.dtype, str(t.dtype).replace("torch.", "")), ++ "shape": list(tensor.shape), ++ "dtype": _DTYPE_STR.get( ++ tensor.dtype, str(tensor.dtype).replace("torch.", "") ++ ), + } + -+ try: ++ for spec, aux, last_hidden in samples: ++ store_id = str(spec["store_id"]) ++ sample_id = str(spec["sample_id"]) ++ gen = int(spec.get("gen", 1)) ++ replace = bool(spec.get("replace", False)) ++ features: Dict[str, str] = dict(spec.get("features") or {}) ++ result_feats: Dict[str, Dict[str, Any]] = {} ++ + aux_name = features.get(_ARTIFACT_AUX) + if aux_name is not None: + if aux is None: + raise RuntimeError( + "spec_capture requested 'aux' but no aux hidden states were " -+ "captured — launch the server with --enable-spec-capture " ++ "captured -- launch the server with --enable-spec-capture " + "(and optionally --spec-capture-aux-layer-ids)" + ) -+ _write(aux_name, aux.unsqueeze(0)) ++ _stage( ++ result_feats, ++ store_id=store_id, ++ sample_id=sample_id, ++ gen=gen, ++ replace=replace, ++ name=aux_name, ++ tensor=aux.unsqueeze(0), ++ ) + lh_name = features.get(_ARTIFACT_LAST_HIDDEN) + if lh_name is not None: + if last_hidden is None: @@ -657,7 +1005,15 @@ index 0000000..038a084 + "spec_capture requested 'last_hidden' but the logits " + "processor did not return it (is aux capture enabled?)" + ) -+ _write(lh_name, last_hidden.unsqueeze(0)) ++ _stage( ++ result_feats, ++ store_id=store_id, ++ sample_id=sample_id, ++ gen=gen, ++ replace=replace, ++ name=lh_name, ++ tensor=last_hidden.unsqueeze(0), ++ ) + for item in spec.get("passthrough") or []: + dtype = _STR_DTYPE.get(str(item.get("dtype", "int64"))) + if dtype is None: @@ -665,22 +1021,116 @@ index 0000000..038a084 + f"spec_capture passthrough {item.get('name')!r}: " + f"unsupported dtype {item.get('dtype')!r}" + ) -+ t = torch.tensor(item["data"], dtype=dtype).reshape( ++ tensor = torch.tensor(item["data"], dtype=dtype).reshape( + [int(d) for d in item["shape"]] + ) -+ _write(str(item["name"]), t) ++ _stage( ++ result_feats, ++ store_id=store_id, ++ sample_id=sample_id, ++ gen=gen, ++ replace=replace, ++ name=str(item["name"]), ++ tensor=tensor, ++ ) ++ results.append( ++ { ++ "sample_id": sample_id, ++ "store_id": store_id, ++ "gen": gen, ++ "aux_layer_ids": self.aux_layer_ids, ++ "features": result_feats, ++ } ++ ) ++ ++ materialize_ms = (time.perf_counter() - started) * 1000.0 ++ self._remove_many_quiet(replace_keys) ++ registered: List[torch.Tensor] = [] ++ register_started = time.perf_counter() ++ try: ++ for tensor, nbytes in zip(tensors, sizes): ++ try: ++ store.register_buffer(tensor.data_ptr(), nbytes) ++ registered.append(tensor) ++ except Exception: ++ pass # TCP and some Mooncake builds auto-register ++ register_ms = (time.perf_counter() - register_started) * 1000.0 ++ put_started = time.perf_counter() ++ batch_put = getattr(store, "batch_put_from", None) ++ if batch_put is None: ++ statuses = [ ++ store.put_from(key, tensor.data_ptr(), nbytes, self._put_config) ++ for key, tensor, nbytes in zip(keys, tensors, sizes) ++ ] ++ else: ++ statuses = batch_put( ++ keys, ++ [tensor.data_ptr() for tensor in tensors], ++ sizes, ++ self._put_config, ++ ) ++ put_ms = (time.perf_counter() - put_started) * 1000.0 + except Exception: -+ for key in written: -+ self._remove_quiet(key) ++ self._remove_many_quiet(keys) + raise ++ finally: ++ for tensor in registered: ++ try: ++ store.unregister_buffer(tensor.data_ptr()) ++ except Exception: ++ pass ++ ++ if statuses is None: ++ statuses = [0] * len(keys) ++ if len(statuses) != len(keys): ++ self._remove_many_quiet(keys) ++ raise RuntimeError( ++ "spec-capture batch_put_from returned " ++ f"{len(statuses)} statuses for {len(keys)} keys" ++ ) ++ failed = [ ++ (key, status) ++ for key, status in zip(keys, statuses) ++ if status is not None and int(status) < 0 ++ ] ++ if failed: ++ self._remove_many_quiet(keys) ++ raise RuntimeError( ++ "spec-capture batch_put_from failed for " ++ f"{len(failed)}/{len(keys)} keys; first={failed[0]}" ++ ) ++ ++ if timing_enabled: ++ logger.info( ++ "[spec-capture-timing] batch_sink samples=%d objects=%d " ++ "bytes=%d materialize_ms=%.3f register_ms=%.3f put_ms=%.3f " ++ "total_ms=%.3f", ++ len(samples), ++ len(keys), ++ sum(sizes), ++ materialize_ms, ++ register_ms, ++ put_ms, ++ (time.perf_counter() - started) * 1000.0, ++ ) ++ return results + -+ return { -+ "sample_id": sample_id, -+ "store_id": store_id, -+ "gen": gen, -+ "aux_layer_ids": self.aux_layer_ids, -+ "features": result_feats, -+ } ++ def put_sample( ++ self, ++ spec: Dict[str, Any], ++ *, ++ aux: Optional[torch.Tensor], ++ last_hidden: Optional[torch.Tensor], ++ ) -> Dict[str, Any]: ++ """Write one sample's artifacts; return the meta_info result dict. ++ ++ ``aux``/``last_hidden`` are the per-request (L, W) captured tensors, ++ stored with a leading batch dim of 1. On any failure the keys already ++ written are best-effort removed (no partial sample is consumable). ++ """ ++ # Keep the single-sample entry point for compatibility with tests and ++ # callers outside the scheduler; production uses put_samples(). ++ return self.put_samples([(spec, aux, last_hidden)])[0] + + +_SINK: Optional[SpecCaptureSink] = None diff --git a/scripts/apply_sglang_spec_capture_patch.sh b/scripts/apply_sglang_spec_capture_patch.sh index f18e66e2b..1f2594d6b 100755 --- a/scripts/apply_sglang_spec_capture_patch.sh +++ b/scripts/apply_sglang_spec_capture_patch.sh @@ -13,7 +13,8 @@ # anything else fails loudly rather than testing against unknown server code. # # Usage: scripts/apply_sglang_spec_capture_patch.sh -# [--target v0.5.14|kimi-k3-9acd9cb|kimi-k3-f8493a4] [--reverse] +# [--target v0.5.14|kimi-k3-ee560a2|kimi-k3-9acd9cb|kimi-k3-f8493a4] +# [--reverse] set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" @@ -46,13 +47,15 @@ case "$TARGET" in EXPECTED_VERSION_PREFIX="0.5.14" PATCH_TARGET="$TARGET" ;; - kimi-k3-9acd9cb|kimi-k3-f8493a4) + kimi-k3-ee560a2|kimi-k3-9acd9cb|kimi-k3-f8493a4) # Kimi K3's SGLang fork currently reports a base-package version that # does not uniquely identify this source revision, so patch --check is # the authoritative compatibility gate below. EXPECTED_VERSION_PREFIX="" - # The patch remains byte-identical and applies cleanly to both the - # original f8493a4 integration point and current K3 tip 9acd9cb. + # One patch is generated against ee560a2 and compatibility-checked + # against the original f8493a4 integration point and the 9acd9cb tip. + # Keep the historical directory name so existing automation remains + # source-compatible. PATCH_TARGET="kimi-k3-f8493a4" ;; *) diff --git a/specforge/config/schema.py b/specforge/config/schema.py index f70f80521..a4a634d93 100644 --- a/specforge/config/schema.py +++ b/specforge/config/schema.py @@ -20,6 +20,7 @@ import json import os from typing import List, Literal, Optional +from urllib.parse import urlparse from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -383,9 +384,13 @@ class DisaggregatedDeploymentConfig(StrictConfigModel): #: Attempt-scoped shared directory. The launcher derives refs, manifest, #: and lifecycle markers beneath it. control_dir: str - #: Optional node-local root for online consumer SQLite/WAL and rank inboxes. - #: When omitted, the historical control_dir-derived paths remain in use. + #: Optional node-local root for the online consumer SQLite/WAL. When + #: omitted, the historical control_dir-derived path remains in use. consumer_state_dir: Optional[str] = None + #: Optional rank-0 HTTP relay for per-rank online inboxes. This removes the + #: shared-filesystem requirement between trainer nodes while keeping the + #: authority-owned source channel and SQLite/WAL on trainer node 0. + inbox_server_url: Optional[str] = None backend: Literal["shared_dir", "mooncake"] store_root: Optional[str] = None store_id: Optional[str] = None @@ -423,6 +428,24 @@ def _validate_store(self): "deployment.disaggregated.consumer_state_dir must be non-empty " "and must not contain surrounding whitespace" ) + if self.inbox_server_url is not None: + parsed = urlparse(self.inbox_server_url) + if ( + parsed.scheme != "http" + or not parsed.hostname + or parsed.port is None + or parsed.username is not None + or parsed.password is not None + or parsed.path not in ("", "/") + or parsed.params + or parsed.query + or parsed.fragment + ): + raise ValueError( + "deployment.disaggregated.inbox_server_url must be an " + "http://host:port origin without credentials, path, query, " + "or fragment" + ) if self.backend == "shared_dir" and not self.store_root: raise ValueError( "deployment.disaggregated.store_root is required for shared_dir" @@ -447,8 +470,7 @@ def _validate_store(self): explicit = [name for name, value in configured_endpoints.items() if value] if explicit: raise ValueError( - "managed_local derives Mooncake endpoints; do not set " - f"{explicit}" + f"managed_local derives Mooncake endpoints; do not set {explicit}" ) if self.producer_segment_size is not None: raise ValueError( @@ -726,8 +748,7 @@ def _validate_run_structure(self): ) if self.data.eval_hidden_states_path and mode != "offline": raise ValueError( - "data.eval_hidden_states_path requires an offline training data " - "source" + "data.eval_hidden_states_path requires an offline training data source" ) if ( not self.training.compact_teacher @@ -757,12 +778,28 @@ def _validate_run_structure(self): if self.deployment.disaggregated is not None else None ) + inbox_server_url = ( + self.deployment.disaggregated.inbox_server_url + if self.deployment.disaggregated is not None + else None + ) if consumer_state_dir is not None: if mode != "online" or deployment != "disaggregated": raise ValueError( "deployment.disaggregated.consumer_state_dir is valid only " "for online disaggregated training" ) + if inbox_server_url is not None: + if mode != "online" or deployment != "disaggregated": + raise ValueError( + "deployment.disaggregated.inbox_server_url is valid only " + "for online disaggregated training" + ) + if self.deployment.trainer.nnodes < 2: + raise ValueError( + "deployment.disaggregated.inbox_server_url requires a " + "multi-node trainer" + ) if ( mode == "online" and deployment == "disaggregated" diff --git a/specforge/launch.py b/specforge/launch.py index 44f1afe60..001c56696 100644 --- a/specforge/launch.py +++ b/specforge/launch.py @@ -11,6 +11,7 @@ from __future__ import annotations import logging +import os from typing import Any, Callable, List, Mapping, Optional, Tuple from specforge.algorithms.registry import AlgorithmRegistration @@ -923,8 +924,7 @@ def elapsed(start: float) -> str: and feature_store_max_resident_bytes < resident_high_watermark_bytes ): raise ValueError( - "feature_store_max_resident_bytes must be >= " - "resident_high_watermark_bytes" + "feature_store_max_resident_bytes must be >= resident_high_watermark_bytes" ) worker_lease = flow_control.prompt_lease(lease) build_start = time.perf_counter() @@ -1275,8 +1275,7 @@ def run_worker(w) -> None: # retryable. Other active calls keep draining. failures += 1 logger.warning( - "rollout worker %s capture call failed " - "(%d/%d): %s", + "rollout worker %s capture call failed (%d/%d): %s", w.worker_id, failures, max_worker_failures, @@ -1540,6 +1539,7 @@ def build_disagg_online_consumer( inbox_dir = channel.path + ".inboxes" distributor = None + inbox_server = None store = None setup_exc = None if dp_rank == 0: @@ -1611,6 +1611,18 @@ def build_disagg_online_consumer( requeued_ids=requeued_ids, idle_timeout_s=idle_timeout_s, ) + inbox_server_url = os.environ.get("DISAGG_INBOX_SERVER_URL") + if inbox_server_url: + from specforge.runtime.data_plane.http_inbox import InboxHTTPServer + + inbox_server = InboxHTTPServer( + inbox_dir, + dp_size, + inbox_server_url, + bind_host=os.environ.get( + "DISAGG_INBOX_SERVER_BIND_HOST", "0.0.0.0" + ), + ).start() channel.publish_consumer_quantum( dp_size * batch_size * accumulation_steps, allow_existing=resume_from is not None, @@ -1624,6 +1636,8 @@ def build_disagg_online_consumer( dist.broadcast_object_list(payload, src=0) setup_error = payload[0] if setup_error is not None: + if dp_rank == 0 and inbox_server is not None: + inbox_server.stop() if dp_rank == 0 and store is not None and hasattr(store, "close"): store.close() if not distributed or world == 1: @@ -1640,7 +1654,16 @@ def build_disagg_online_consumer( # The successful rank-0 setup broadcast guarantees inbox recreation and the # optimizer-window sidecar are visible before any rank opens its reader. - inbox = InboxChannel(RefDistributor.inbox_path(inbox_dir, dp_rank)) + # An optional rank-0 HTTP relay removes the shared-mount requirement for + # non-authority ranks; rank 0 remains local so the durable authority never + # depends on its own network service. + inbox_server_url = os.environ.get("DISAGG_INBOX_SERVER_URL") + if inbox_server_url and dp_rank != 0: + from specforge.runtime.data_plane.http_inbox import RemoteInboxChannel + + inbox = RemoteInboxChannel(inbox_server_url, dp_rank) + else: + inbox = InboxChannel(RefDistributor.inbox_path(inbox_dir, dp_rank)) queue = StreamingRefQueue(inbox, idle_timeout_s=idle_timeout_s) drain_state = {"attempted": False} @@ -1689,6 +1712,8 @@ def mark_consumer_failed(exc: BaseException) -> None: def stop_distributor_and_drain() -> None: if distributor is not None: distributor.stop() + if inbox_server is not None: + inbox_server.stop() # The success hook already drained before publishing consumer_done. On # an exception, finalization still makes one bounded local attempt and # reports a cleanup failure loudly without replacing the primary fit @@ -1711,7 +1736,7 @@ def stop_distributor_and_drain() -> None: ) except Exception as signal_exc: print( - "failed to publish consumer cleanup failure: " f"{signal_exc}", + f"failed to publish consumer cleanup failure: {signal_exc}", flush=True, ) logging.getLogger(__name__).error("%s", combined) diff --git a/specforge/launch_plan.py b/specforge/launch_plan.py index 293a91b01..72f79bd7b 100644 --- a/specforge/launch_plan.py +++ b/specforge/launch_plan.py @@ -203,7 +203,7 @@ def _resolve_role( raise ValueError("--role producer/consumer/both requires disaggregated mode") if requested == "both" and cfg.training.resume_from: raise ValueError( - "--role both cannot resume a disaggregated producer; use " "--role consumer" + "--role both cannot resume a disaggregated producer; use --role consumer" ) if distributed and requested == "both": raise ValueError( @@ -222,7 +222,7 @@ def _resolved_node_rank( node_rank = int(env["NODE_RANK"]) if node_rank is not None and not 0 <= node_rank < cfg.deployment.trainer.nnodes: raise ValueError( - f"node_rank={node_rank} must be in [0, " f"{cfg.deployment.trainer.nnodes})" + f"node_rank={node_rank} must be in [0, {cfg.deployment.trainer.nnodes})" ) return node_rank @@ -265,6 +265,8 @@ def _disaggregated_env( "DISAGG_BACKEND": deployment.backend, "DISAGG_STORE_ID": deployment.store_id or cfg.run_id, } + if deployment.inbox_server_url: + values["DISAGG_INBOX_SERVER_URL"] = deployment.inbox_server_url if cfg.mode == "online": if deployment.backend != "mooncake": raise ValueError("online disaggregated training requires Mooncake") @@ -275,9 +277,9 @@ def _disaggregated_env( { "DISAGG_REF_CHANNEL": str(control_dir / "refs.jsonl"), "DISAGG_DB": str(consumer_state_dir / "consumer.sqlite"), - # SQLite/WAL stays on rank 0's local filesystem. Inboxes are - # ordinary append-only channels and must remain visible to - # ranks on every trainer node. + # SQLite/WAL stays on rank 0's local filesystem. Inboxes are + # shared normally or served by rank 0 when the HTTP relay is + # configured. "DISAGG_INBOX_DIR": str( ( control_dir @@ -593,8 +595,7 @@ def _validate_consumer_database( if cfg.training.resume_from: if state_owner and not os.path.exists(database): raise ValueError( - "consumer resume requires the retained metadata database: " - f"{database}" + f"consumer resume requires the retained metadata database: {database}" ) return stale = [ diff --git a/specforge/runtime/data_plane/http_inbox.py b/specforge/runtime/data_plane/http_inbox.py new file mode 100644 index 000000000..4f24b1a00 --- /dev/null +++ b/specforge/runtime/data_plane/http_inbox.py @@ -0,0 +1,326 @@ +# Copyright 2024 The SpecForge team. All rights reserved. +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Private-network HTTP transport for rank-local online reference inboxes. + +Online feature tensors remain in Mooncake. This module relays only the small, +tensor-free ``SampleRef`` JSONL records that :class:`RefDistributor` already +writes. It lets multi-node consumers run on container platforms where trainer +nodes cannot mount a shared control filesystem. + +Rank 0 owns the server and its local inbox files. Every other rank tail-reads +its private stream by byte offset and posts only its durable consumed-count +target. Requests are idempotent with respect to a client retry: a read offset +is advanced only after the response arrives, and an absolute consumed target is +applied under a per-rank server lock. +""" + +from __future__ import annotations + +import base64 +import json +import os +import threading +import time +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.error import HTTPError, URLError +from urllib.parse import parse_qs, urlparse +from urllib.request import Request, urlopen + +from specforge.runtime.contracts import SampleRef +from specforge.runtime.data_plane.ref_distributor import RefDistributor +from specforge.runtime.data_plane.ref_serialization import ref_from_dict +from specforge.runtime.data_plane.streaming_ref_channel import StreamingRefChannel + +_CLOSED_SUFFIX = ".closed" +_FAILED_SUFFIX = ".failed" + + +class _InboxThreadingHTTPServer(ThreadingHTTPServer): + # socketserver.TCPServer defaults to a backlog of five. A DP consumer can + # have dozens of ranks polling in lockstep, so that default turns a healthy + # rank-0 relay into intermittent connection resets at every step boundary. + request_queue_size = 256 + daemon_threads = True + + +def _validated_origin(origin: str): + parsed = urlparse(origin) + if ( + parsed.scheme != "http" + or not parsed.hostname + or parsed.port is None + or parsed.username is not None + or parsed.password is not None + or parsed.path not in ("", "/") + or parsed.params + or parsed.query + or parsed.fragment + ): + raise ValueError( + "inbox HTTP origin must be http://host:port without credentials, " + "path, query, or fragment" + ) + return parsed + + +class InboxHTTPServer: + """Serve rank-0 inbox files and consumed counters on a private origin.""" + + def __init__( + self, + inbox_dir: str, + dp_size: int, + origin: str, + *, + bind_host: str = "0.0.0.0", + ) -> None: + parsed = _validated_origin(origin) + if dp_size < 2: + raise ValueError("inbox HTTP server requires dp_size >= 2") + self.inbox_dir = os.path.abspath(inbox_dir) + self.dp_size = dp_size + self.origin = origin.rstrip("/") + self._ack_channels = [ + StreamingRefChannel(RefDistributor.inbox_path(self.inbox_dir, rank)) + for rank in range(dp_size) + ] + # A target ack may be retried after an ambiguous connection reset. The + # per-rank lock makes the read/advance/respond sequence atomic across + # the original request and its retry. + self._ack_locks = [threading.Lock() for _ in range(dp_size)] + self._httpd = _InboxThreadingHTTPServer( + (bind_host, parsed.port), self._handler_type() + ) + self._thread: threading.Thread | None = None + + def _handler_type(self): + owner = self + + class Handler(BaseHTTPRequestHandler): + server_version = "SpecForgeInbox/1" + + def log_message(self, _format, *_args): + return + + def _rank(self, *, consumed: bool = False) -> int | None: + suffix = "/consumed" if consumed else "" + path = urlparse(self.path).path + prefix = "/v1/inboxes/" + if not path.startswith(prefix) or not path.endswith(suffix): + return None + token = path[len(prefix) :] + if suffix: + token = token[: -len(suffix)] + try: + rank = int(token) + except ValueError: + return None + return rank if 0 <= rank < owner.dp_size else None + + def _json(self, status: HTTPStatus, payload) -> None: + body = json.dumps(payload, separators=(",", ":")).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + rank = self._rank() + if rank is None: + self._json(HTTPStatus.NOT_FOUND, {"error": "unknown inbox"}) + return + query = parse_qs(urlparse(self.path).query) + try: + offset = int(query.get("offset", ["0"])[0]) + except ValueError: + offset = -1 + if offset < 0: + self._json(HTTPStatus.BAD_REQUEST, {"error": "invalid offset"}) + return + path = RefDistributor.inbox_path(owner.inbox_dir, rank) + data = b"" + next_offset = offset + try: + with open(path, "rb") as stream: + stream.seek(offset) + data = stream.read() + next_offset = stream.tell() + except FileNotFoundError: + pass + failure = None + try: + with open(path + _FAILED_SUFFIX, encoding="utf-8") as stream: + failure = stream.read() + except FileNotFoundError: + pass + self._json( + HTTPStatus.OK, + { + "data": base64.b64encode(data).decode("ascii"), + "next_offset": next_offset, + "closed": os.path.exists(path + _CLOSED_SUFFIX), + "failure": failure, + }, + ) + + def do_POST(self): + rank = self._rank(consumed=True) + if rank is None: + self._json(HTTPStatus.NOT_FOUND, {"error": "unknown inbox"}) + return + try: + length = int(self.headers.get("Content-Length", "0")) + if length < 1 or length > 1024: + raise ValueError("invalid body size") + payload = json.loads(self.rfile.read(length)) + target = int(payload["target"]) + if target < 1: + raise ValueError("target must be positive") + except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc: + self._json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + with owner._ack_locks[rank]: + channel = owner._ack_channels[rank] + current = channel.consumed_remote() + if target < current: + self._json( + HTTPStatus.CONFLICT, + { + "error": "consumed target moved backwards", + "consumed": current, + }, + ) + return + if target > current: + channel.mark_consumed(target - current) + self._json(HTTPStatus.OK, {"consumed": target}) + + return Handler + + def start(self) -> InboxHTTPServer: + if self._thread is not None: + return self + self._thread = threading.Thread( + target=self._httpd.serve_forever, + name="specforge-inbox-http", + daemon=True, + ) + self._thread.start() + return self + + def stop(self) -> None: + if self._thread is None: + return + self._httpd.shutdown() + self._httpd.server_close() + self._thread.join(timeout=5.0) + self._thread = None + + +class RemoteInboxChannel: + """Read one rank inbox and acknowledge it through ``InboxHTTPServer``.""" + + def __init__(self, origin: str, dp_rank: int, *, timeout_s: float = 10.0) -> None: + _validated_origin(origin) + if dp_rank < 0: + raise ValueError("dp_rank must be non-negative") + self.path = f"{origin.rstrip('/')}/v1/inboxes/{dp_rank}" + self.timeout_s = timeout_s + self._read_offset = 0 + self._buf = "" + self._closed = False + self._failure: str | None = None + self._pending: list[SampleRef] = [] + self._pull_lock = threading.Lock() + self._consumed_target = 0 + + def _pull(self) -> None: + with self._pull_lock: + request = Request(f"{self.path}?offset={self._read_offset}") + try: + with urlopen(request, timeout=self.timeout_s) as response: + payload = json.load(response) + except (URLError, OSError, TimeoutError): + # Let StreamingRefQueue's configured idle timeout distinguish a + # transient network outage from a dead rank-0 service. + return + data = base64.b64decode(payload["data"]) + next_offset = int(payload["next_offset"]) + if ( + next_offset < self._read_offset + or next_offset - self._read_offset != len(data) + ): + raise RuntimeError("inbox HTTP server returned an invalid byte range") + self._read_offset = next_offset + self._closed = bool(payload["closed"]) + self._failure = payload.get("failure") + self._buf += data.decode("utf-8") + lines = self._buf.split("\n") + self._buf = lines.pop() + self._pending.extend( + ref_from_dict(json.loads(line)) for line in lines if line + ) + + def poll(self) -> list[SampleRef]: + self._pull() + refs, self._pending = self._pending, [] + return refs + + def is_closed(self) -> bool: + self._pull() + return self._closed + + def failure(self) -> str | None: + self._pull() + if self._failure is None: + return None + return f"ref-distributor died:\n{self._failure}" + + def mark_consumed(self, n: int) -> None: + if n < 1: + return + target = self._consumed_target + n + body = json.dumps({"target": target}, separators=(",", ":")).encode("utf-8") + transient_error = None + for attempt in range(4): + request = Request( + f"{self.path}/consumed", + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urlopen(request, timeout=self.timeout_s) as response: + payload = json.load(response) + except HTTPError: + raise + except (URLError, OSError, TimeoutError) as exc: + transient_error = exc + if attempt < 3: + time.sleep(0.05 * (attempt + 1)) + continue + if int(payload.get("consumed", -1)) != target: + raise RuntimeError( + "inbox HTTP server did not confirm consumed target" + ) + self._consumed_target = target + return + raise RuntimeError( + "inbox HTTP consumed acknowledgement failed after retries" + ) from transient_error + + +__all__ = ["InboxHTTPServer", "RemoteInboxChannel"] diff --git a/tests/test_config/test_launch_topology.py b/tests/test_config/test_launch_topology.py index ee1a06c26..2d7978986 100644 --- a/tests/test_config/test_launch_topology.py +++ b/tests/test_config/test_launch_topology.py @@ -21,8 +21,9 @@ "lfm2.5-1.2b-instruct-dflash-online.yaml": 8, "inkling-dspark-disaggregated.yaml": 1, "kimi-k3-dspark-v1c-disaggregated.yaml": 4, - "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml": 4, - "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml": 4, + "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml": 8, + "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml": 8, + "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml": 8, "ling-flash-2.0-eagle3-offline.yaml": 8, "ling-flash-2.0-eagle3-online.yaml": 8, "llama3.1-8b-eagle3-offline.yaml": 1, @@ -315,7 +316,7 @@ def _recipes() -> dict[str, Path]: class ExampleLaunchTopologyTest(unittest.TestCase): def test_every_recipe_has_the_explicit_golden_topology(self): recipes = _recipes() - self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 66) + self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 67) self.assertEqual(set(recipes), set(EXPECTED_NPROC_PER_NODE)) for filename, nproc_per_node in EXPECTED_NPROC_PER_NODE.items(): @@ -339,9 +340,20 @@ def test_every_recipe_has_the_explicit_golden_topology(self): else "local_colocated" ) self.assertEqual(deployment["mode"], expected_mode) + expected_trainer = {"nnodes": 1, "nproc_per_node": nproc_per_node} + if ( + filename + == "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" + ): + expected_trainer = { + "nnodes": 2, + "nproc_per_node": 8, + "master_addr": "trainer-node-0", + "master_port": 29500, + } self.assertEqual( deployment["trainer"], - {"nnodes": 1, "nproc_per_node": nproc_per_node}, + expected_trainer, ) expected_keys = {"mode", "trainer"} @@ -367,7 +379,13 @@ def test_golden_topologies_validate_for_their_declared_world_size(self): config = Config.from_file(str(path)) topology = config.deployment.trainer expected_nproc = EXPECTED_NPROC_PER_NODE[filename] - self.assertEqual(topology.nnodes, 1) + expected_nnodes = ( + 2 + if filename + == "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" + else 1 + ) + self.assertEqual(topology.nnodes, expected_nnodes) self.assertEqual(topology.nproc_per_node, expected_nproc) config.validate_world_size(topology.nnodes * expected_nproc) @@ -439,6 +457,55 @@ def test_migrated_dspark_recipes_match_source_training_contract(self): self.assertEqual(kimi.data.max_length, 65536) self.assertEqual(kimi.training.num_anchors, 512) + # The portable MLA/KDA recipes preserve the 16-sample optimizer + # microbatch on one DP8 trainer with two samples per rank. + for filename in ( + "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", + "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", + ): + with self.subTest(config=filename): + config = Config.from_file(str(EXAMPLE_CONFIG_DIR / filename)) + topology = config.deployment.trainer + world_size = topology.nnodes * topology.nproc_per_node + self.assertEqual(world_size, 8) + self.assertEqual(config.training.batch_size, 2) + self.assertEqual(config.training.accumulation_steps, 32) + self.assertEqual(world_size * config.training.batch_size, 16) + self.assertEqual( + world_size + * config.training.batch_size + * config.training.accumulation_steps, + 512, + ) + + # The exact reference recipe restores the source job's 16 trainer + # ranks. Keeping one sample per GPU avoids doubling per-rank draft + # compute, while the HTTP inbox relay removes any shared-filesystem + # requirement for the second trainer node. + reference = Config.from_file( + str( + EXAMPLE_CONFIG_DIR + / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" + ) + ) + topology = reference.deployment.trainer + world_size = topology.nnodes * topology.nproc_per_node + self.assertEqual(world_size, 16) + self.assertEqual(reference.training.batch_size, 1) + self.assertEqual(reference.training.accumulation_steps, 32) + self.assertEqual(world_size * reference.training.batch_size, 16) + self.assertEqual( + world_size + * reference.training.batch_size + * reference.training.accumulation_steps, + 512, + ) + self.assertEqual(topology.master_addr, "trainer-node-0") + self.assertEqual( + reference.deployment.disaggregated.inbox_server_url, + "http://trainer-node-0:35900", + ) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_config/test_unified_feature_reachability.py b/tests/test_config/test_unified_feature_reachability.py index 6c8c6e08c..4e07c23cb 100644 --- a/tests/test_config/test_unified_feature_reachability.py +++ b/tests/test_config/test_unified_feature_reachability.py @@ -146,7 +146,7 @@ def test_all_example_configs_validate_through_the_typed_entry(self): for path in EXAMPLE_CONFIG_DIR.glob("*.yaml") if not path.name.startswith(".") ) - self.assertEqual(len(paths), 66) + self.assertEqual(len(paths), 67) resolved_runs = { path.name: resolve_run(Config.from_file(str(path))) for path in paths diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index be59aea61..c9b008ebd 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -97,6 +97,66 @@ def test_production_configs_match_k3_target(filename, architecture): } +def test_reference_full_attention_config_is_exact_old_architecture(): + config = json.loads( + (ROOT / "configs" / "kimi-k3-dspark-fullattn-gqa16.json").read_text() + ) + assert config["architectures"] == ["DSparkDraftModel"] + assert config["block_size"] == 7 + assert config["num_hidden_layers"] == 5 + assert config["layer_types"] == ["full_attention"] * 5 + assert config["num_attention_heads"] == 64 + assert config["num_key_value_heads"] == 16 + assert config["dflash_config"]["target_layer_ids"] == [7, 23, 51, 67, 83] + assert config["rope_scaling"] is None + + +def test_reference_full_attention_recipe_preserves_exact_old_contract(): + config = Config.from_file( + str( + ROOT + / "examples" + / "configs" + / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" + ) + ) + assert config.model.target_model_path == "/workspace/models/Kimi-K3" + assert config.data.max_length == 4096 + assert config.data.dataloader_num_workers == 4 + assert config.training.batch_size == 1 + assert config.training.accumulation_steps == 32 + assert config.deployment.trainer.nnodes == 2 + assert config.deployment.trainer.nproc_per_node == 8 + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + == 16 + ) + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + == 512 + ) + assert config.training.num_epochs == 10 + assert config.training.total_steps == 9173 + assert config.training.learning_rate == pytest.approx(6e-4) + assert config.training.lr_scheduler == "cosine" + assert config.training.warmup_ratio == pytest.approx(0.04) + assert config.training.num_anchors == 512 + assert config.training.max_checkpoints == 3 + assert config.runtime.producer_lease == 16 + assert len(config.deployment.disaggregated.server_urls) == 2 + assert ( + config.deployment.disaggregated.inbox_server_url + == "http://trainer-node-0:35900" + ) + assert "openperfectblend-regen-9caaf705" in config.data.train_data_path + assert config.tracking.report_to == "wandb" + + @pytest.mark.parametrize( "filename", [ @@ -107,8 +167,14 @@ def test_production_configs_match_k3_target(filename, architecture): def test_training_recipes_preserve_reference_run_contract(filename): config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) assert config.data.max_length == 4096 - assert config.training.batch_size == 8 - assert config.training.accumulation_steps == 8 + assert config.training.batch_size == 2 + assert config.training.accumulation_steps == 32 + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + == 16 + ) assert ( config.deployment.trainer.nnodes * config.deployment.trainer.nproc_per_node @@ -143,8 +209,16 @@ def test_training_recipes_preserve_reference_run_contract(filename): + max_capture_overshoot - optimizer_quantum ) - assert config.runtime.producer_lease == config.training.batch_size - assert config.runtime.producer_concurrency == 1 + # Each capture HTTP request uses the source job's complete 16-sample + # optimizer microbatch to amortize auxiliary-state aggregation. This is a + # producer request size, independent of the DP8 consumer's per-rank batch. + assert config.runtime.producer_lease == 16 + assert config.runtime.producer_concurrency == 2 + assert config.model.sglang_enable_symm_mem is False + assert config.model.sglang_max_running_requests == 16 + # K3 consumes five linear-attention cache entries per live request; 40 + # silently caps SGLang at eight even when max_running_requests is 16. + assert config.model.sglang_max_mamba_cache_size == 80 assert len(config.deployment.disaggregated.server_urls) >= 2 # Two worst-case 4,096-token optimizer windows carry about 336 GiB of # captured features; byte throttling must not serialize the pipeline. diff --git a/tests/test_runtime/test_http_inbox.py b/tests/test_runtime/test_http_inbox.py new file mode 100644 index 000000000..10e91b04a --- /dev/null +++ b/tests/test_runtime/test_http_inbox.py @@ -0,0 +1,118 @@ +"""Private-network inbox relay contracts.""" + +from __future__ import annotations + +import json +import socket +import tempfile +import threading +import unittest +from unittest import mock +from urllib.request import Request, urlopen + +from specforge.runtime.contracts import FeatureSpec, SampleRef +from specforge.runtime.data_plane.http_inbox import ( + InboxHTTPServer, + RemoteInboxChannel, +) +from specforge.runtime.data_plane.ref_distributor import RefDistributor +from specforge.runtime.data_plane.streaming_ref_channel import StreamingRefChannel + + +def _ref(sample_id: str) -> SampleRef: + return SampleRef( + sample_id=sample_id, + run_id="run0", + source_task_id=f"task-{sample_id}", + feature_store_uri=f"mooncake://run0/{sample_id}", + feature_keys={"hidden_state": f"{sample_id}/hidden_state"}, + feature_specs={ + "hidden_state": FeatureSpec( + name="hidden_state", shape=(2, 4), dtype="float32" + ) + }, + strategy="dspark", + metadata={"target_repr": "hidden_state"}, + ) + + +def _free_port() -> int: + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +class TestHTTPInbox(unittest.TestCase): + def setUp(self): + self.work = tempfile.mkdtemp(prefix="http-inbox-") + self.path = RefDistributor.inbox_path(self.work, 1) + self.local = StreamingRefChannel(self.path) + self.origin = f"http://127.0.0.1:{_free_port()}" + self.server = InboxHTTPServer( + self.work, 2, self.origin, bind_host="127.0.0.1" + ).start() + self.remote = RemoteInboxChannel(self.origin, 1) + + def tearDown(self): + self.server.stop() + + def test_tail_read_close_and_consumed_counter(self): + self.local.publish_batch([_ref("s0"), _ref("s1")]) + self.assertEqual([ref.sample_id for ref in self.remote.poll()], ["s0", "s1"]) + self.assertEqual(self.remote.poll(), []) + + self.remote.mark_consumed(2) + self.assertEqual(self.local.consumed_remote(), 2) + + self.remote.mark_consumed(1) + self.assertEqual(self.local.consumed_remote(), 3) + + self.local.close() + self.assertTrue(self.remote.is_closed()) + + def test_status_probe_does_not_discard_unpolled_refs(self): + self.local.publish(_ref("s0")) + self.assertFalse(self.remote.is_closed()) + self.assertIsNone(self.remote.failure()) + self.assertEqual([ref.sample_id for ref in self.remote.poll()], ["s0"]) + + def test_failure_is_forwarded(self): + failure = self.path + ".failed" + with open(failure, "w", encoding="utf-8") as stream: + stream.write("capture failed") + self.assertIn("capture failed", self.remote.failure()) + + def test_consumed_target_is_idempotent_under_concurrent_retries(self): + body = json.dumps({"target": 5}).encode("utf-8") + + def post_target(): + request = Request( + f"{self.origin}/v1/inboxes/1/consumed", + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urlopen(request, timeout=2.0) as response: + self.assertEqual(json.load(response)["consumed"], 5) + + threads = [threading.Thread(target=post_target) for _ in range(12)] + for thread in threads: + thread.start() + for thread in threads: + thread.join() + self.assertEqual(self.local.consumed_remote(), 5) + + def test_pull_treats_connection_reset_as_transient(self): + with mock.patch( + "specforge.runtime.data_plane.http_inbox.urlopen", + side_effect=ConnectionResetError("peer reset"), + ): + self.assertEqual(self.remote.poll(), []) + + def test_invalid_origin_is_rejected(self): + with self.assertRaisesRegex(ValueError, "http://host:port"): + RemoteInboxChannel("https://trainer.example:35900/path", 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_runtime/test_launch_plan.py b/tests/test_runtime/test_launch_plan.py index 226410221..741732caf 100644 --- a/tests/test_runtime/test_launch_plan.py +++ b/tests/test_runtime/test_launch_plan.py @@ -26,9 +26,9 @@ ReadinessSpec, ServiceSpec, _http_ready, + run_commands, ) from specforge.launch_plan import build_launch_plan as _build_launch_plan -from specforge.launch_plan import run_commands from specforge.training.capture_contract import ServerCaptureContract ALGORITHM = builtin_algorithm_registry().resolve("dflash") @@ -75,9 +75,9 @@ def _offline_disaggregated_config(*, backend="mooncake", producer_segment_size=N if backend == "shared_dir": raw["deployment"]["disaggregated"]["store_root"] = "/shared/features" if producer_segment_size is not None: - raw["deployment"]["disaggregated"][ - "producer_segment_size" - ] = producer_segment_size + raw["deployment"]["disaggregated"]["producer_segment_size"] = ( + producer_segment_size + ) return Config.model_validate(raw) @@ -332,9 +332,9 @@ def test_disaggregated_auto_supervises_both_roles_on_one_node(self): def test_consumer_state_dir_keeps_refs_shared_and_state_node_local(self): raw = _config(mode="disaggregated", nproc=2).model_dump() - raw["deployment"]["disaggregated"][ - "consumer_state_dir" - ] = "/local/attempt-state" + raw["deployment"]["disaggregated"]["consumer_state_dir"] = ( + "/local/attempt-state" + ) plan = build_launch_plan( Config.model_validate(raw), config_path="run.yaml", @@ -374,9 +374,9 @@ def test_consumer_state_dir_rejects_unsupported_modes_and_whitespace(self): for name, (mutate, message) in invalid_cases.items(): with self.subTest(case=name): raw = cfg.model_dump() - raw["deployment"]["disaggregated"][ - "consumer_state_dir" - ] = "/local/attempt-state" + raw["deployment"]["disaggregated"]["consumer_state_dir"] = ( + "/local/attempt-state" + ) mutate(raw) with self.assertRaisesRegex(ValidationError, message): Config.model_validate(raw) @@ -391,8 +391,12 @@ def test_multi_node_requires_node_local_consumer_state(self): Config.model_validate(raw) def test_multi_node_keeps_wal_local_and_inboxes_shared(self): + raw = _config(mode="disaggregated", nproc=2, nnodes=2).model_dump() + raw["deployment"]["disaggregated"]["inbox_server_url"] = ( + "http://trainer-0:35900" + ) plan = build_launch_plan( - _config(mode="disaggregated", nproc=2, nnodes=2), + Config.model_validate(raw), config_path="run.yaml", requested_role="consumer", node_rank=0, @@ -404,6 +408,30 @@ def test_multi_node_keeps_wal_local_and_inboxes_shared(self): command = plan.commands[0] self.assertEqual("/local/attempt-1/consumer.sqlite", command.env["DISAGG_DB"]) self.assertEqual("/shared/attempt-1/inboxes", command.env["DISAGG_INBOX_DIR"]) + self.assertEqual( + "http://trainer-0:35900", command.env["DISAGG_INBOX_SERVER_URL"] + ) + + def test_inbox_server_url_is_typed_and_online_multinode_only(self): + cfg = _config(mode="disaggregated", nproc=2, nnodes=2) + invalid = { + "https": "https://trainer-0:35900", + "missing port": "http://trainer-0", + "path": "http://trainer-0:35900/inboxes", + } + for name, value in invalid.items(): + with self.subTest(case=name): + raw = cfg.model_dump() + raw["deployment"]["disaggregated"]["inbox_server_url"] = value + with self.assertRaisesRegex(ValidationError, "http://host:port"): + Config.model_validate(raw) + + raw = _config(mode="disaggregated", nproc=2, nnodes=1).model_dump() + raw["deployment"]["disaggregated"]["inbox_server_url"] = ( + "http://trainer-0:35900" + ) + with self.assertRaisesRegex(ValidationError, "multi-node trainer"): + Config.model_validate(raw) def test_disaggregated_roles_are_independently_selectable(self): producer = build_launch_plan( diff --git a/tests/test_runtime/test_package_architecture.py b/tests/test_runtime/test_package_architecture.py index e421927aa..64fd8fc64 100644 --- a/tests/test_runtime/test_package_architecture.py +++ b/tests/test_runtime/test_package_architecture.py @@ -713,6 +713,7 @@ def test_dspark_configs_are_qwen3_gqa_only(self): { "glm-5.2-dspark.json", "inkling-dspark.json", + "kimi-k3-dspark-fullattn-gqa16.json", "kimi-k3-dspark-v1c.json", "qwen3-4b-dspark.json", "qwen3-8b-dspark.json", diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index fa06f9be1..12b9ade20 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -13,6 +13,12 @@ OFFLINE = ROOT / "examples" / "disagg" / "run_offline.sh" OFFLINE_TWO_NODE = ROOT / "examples" / "disagg" / "run_offline_2node.sh" TWO_NODE = ROOT / "examples" / "disagg" / "run_qwen3_8b_dflash_disagg_2node.sh" +KIMI_K3_CAPTURE = ( + ROOT / "examples" / "disagg" / "run_kimi_k3_dspark_capture_server.sh" +) +KIMI_K3_CAPTURE_PATCH = ( + ROOT / "patches" / "sglang" / "kimi-k3-f8493a4" / "spec-capture.patch" +) class DisaggregatedWrapperTest(unittest.TestCase): @@ -57,7 +63,7 @@ def _run(self, wrapper, *args, include_config=True): ) def test_wrappers_are_executable_and_syntax_valid(self): - for wrapper in (ONLINE, OFFLINE): + for wrapper in (ONLINE, OFFLINE, KIMI_K3_CAPTURE): with self.subTest(wrapper=wrapper.name): self.assertTrue(os.access(wrapper, os.X_OK)) result = subprocess.run( @@ -123,6 +129,44 @@ def test_help_describes_auto_and_explicit_roles(self): self.assertIn("--role", result.stdout) self.assertIn("producer and consumer", result.stdout) + def test_kimi_k3_capture_launcher_keeps_prefill_on_the_fast_path(self): + source = KIMI_K3_CAPTURE.read_text(encoding="utf-8") + self.assertIn('--max-running-requests "$MAX_RUNNING_REQUESTS"', source) + self.assertIn('MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16}', source) + self.assertIn('MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728}', source) + self.assertIn('MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960}', source) + self.assertIn('MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80}', source) + self.assertIn('--max-prefill-tokens "$MAX_PREFILL_TOKENS"', source) + self.assertIn('--max-mamba-cache-size "$MAX_MAMBA_CACHE_SIZE"', source) + self.assertIn( + "SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2", source + ) + self.assertIn("--disable-cuda-graph", source) + self.assertNotIn("--enable-symm-mem", source) + + def test_kimi_k3_capture_patch_only_copies_features_on_the_writer_rank(self): + source = KIMI_K3_CAPTURE_PATCH.read_text(encoding="utf-8") + self.assertIn("self.output_streamer.ps.attn_tp_rank != 0", source) + self.assertNotIn(".cpu().clone()", source) + self.assertIn( + 'getattr(logits_output, "_spec_capture_aux_cpu", None)', source + ) + self.assertIn("logits_output.hidden_states.cpu()", source) + self.assertIn('"aux" in features', source) + self.assertIn('"last_hidden" in features', source) + self.assertIn("_should_copy_hidden_states_to_cpu", source) + self.assertIn("self.ps.attn_tp_rank == 0", source) + self.assertIn( + "self.logits_output.last_hidden_states = _async_d2h(", source + ) + self.assertIn("len(chunks) == 1", source) + self.assertIn("ThreadPoolExecutor(", source) + self.assertIn('getattr(store, "batch_put_from", None)', source) + self.assertIn("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES", source) + self.assertIn( + "req.finished() and req.spec_capture_result is None", source + ) + def test_two_node_wrapper_keeps_training_on_the_unified_cli(self): self.assertTrue(os.access(TWO_NODE, os.X_OK)) syntax = subprocess.run( diff --git a/tests/test_scripts/test_prepare_hidden_states.py b/tests/test_scripts/test_prepare_hidden_states.py index d30738054..325b81f76 100644 --- a/tests/test_scripts/test_prepare_hidden_states.py +++ b/tests/test_scripts/test_prepare_hidden_states.py @@ -109,10 +109,11 @@ def test_strategy_capture_plans_use_draft_owned_layers_and_schemas(self): with self.subTest(strategy=strategy): plan = resolve_offline_capture_plan(args, target_config) self.assertEqual(strategy, plan.strategy) - self.assertEqual( - "eagle3" if strategy == "eagle3" else "dflash", - plan.capture_method, - ) + expected_capture_method = { + "eagle3": "eagle3", + "dspark": "dspark", + }.get(strategy, "dflash") + self.assertEqual(expected_capture_method, plan.capture_method) self.assertEqual(layers, plan.capture_layers) self.assertEqual(feature_names, set(plan.layout.output_names)) diff --git a/tests/test_scripts/test_sync_distributed_checkpoints.py b/tests/test_scripts/test_sync_distributed_checkpoints.py new file mode 100644 index 000000000..1b0104d79 --- /dev/null +++ b/tests/test_scripts/test_sync_distributed_checkpoints.py @@ -0,0 +1,185 @@ +"""Dependency-light tests for the non-shared-filesystem checkpoint relay.""" + +from __future__ import annotations + +import importlib.util +import json +import os +import shutil +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "examples" / "disagg" / "sync_distributed_checkpoints.py" +SPEC = importlib.util.spec_from_file_location("checkpoint_relay_example", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +RELAY_MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(RELAY_MODULE) + + +class DistributedCheckpointRelayTest(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory(prefix="checkpoint_relay_") + self.root = Path(self._tmp.name) + self.run_id = "relay-test" + self.relays = [] + + def tearDown(self): + for relay in self.relays: + relay._httpd.server_close() + self._tmp.cleanup() + + def _checkpoint(self, root: Path, step: int, ranks: range) -> Path: + checkpoint = root / "output" / f"{self.run_id}-step{step}" + checkpoint.mkdir(parents=True) + if 0 in ranks: + (checkpoint / "training_state.pt").write_bytes( + f"shared-step-{step}".encode() + ) + for rank in ranks: + (checkpoint / f"training_state_rank{rank}.pt").write_bytes( + f"rank-{rank}-step-{step}".encode() + ) + return checkpoint + + def _relay( + self, + root: Path, + *, + local_ranks: range, + peer_ranks: range, + max_archives: int = 2, + ): + relay = RELAY_MODULE.CheckpointRelay( + SimpleNamespace( + run_root=str(root), + run_id=self.run_id, + local_ranks=tuple(local_ranks), + peer_ranks=tuple(peer_ranks), + peer_url="file:///not-configured", + poll_s=0.01, + max_archives=max_archives, + serve_host="127.0.0.1", + serve_port=0, + ) + ) + self.relays.append(relay) + return relay + + def test_two_nodes_assemble_complete_checkpoints_and_bound_archives(self): + node0 = self.root / "node0" + node1 = self.root / "node1" + for step in (1, 2, 3): + self._checkpoint(node0, step, range(0, 2)) + self._checkpoint(node1, step, range(2, 4)) + + relay0 = self._relay( + node0, local_ranks=range(0, 2), peer_ranks=range(2, 4) + ) + relay1 = self._relay( + node1, local_ranks=range(2, 4), peer_ranks=range(0, 2) + ) + relay0.peer_url = relay1.relay_dir.as_uri() + relay1.peer_url = relay0.relay_dir.as_uri() + + relay0._publish_local() + relay1._publish_local() + relay0._pull_peer() + relay1._pull_peer() + + for root in (node0, node1): + for step in (2, 3): + checkpoint = root / "output" / f"{self.run_id}-step{step}" + expected = {"training_state.pt"} + expected.update( + f"training_state_rank{rank}.pt" for rank in range(4) + ) + self.assertTrue( + expected.issubset(path.name for path in checkpoint.iterdir()) + ) + + for relay in (relay0, relay1): + manifest = json.loads( + (relay.relay_dir / "manifest.json").read_text(encoding="utf-8") + ) + self.assertEqual( + [entry["step"] for entry in manifest["entries"]], [2, 3] + ) + local_archives = [ + path + for path in relay.relay_dir.glob("*.tar") + if not path.name.startswith("peer-") + ] + peer_archives = list(relay.relay_dir.glob("peer-*.tar")) + self.assertEqual(len(local_archives), 2) + self.assertEqual(len(peer_archives), 2) + + self._checkpoint(node0, 4, range(0, 2)) + self._checkpoint(node1, 4, range(2, 4)) + relay0._publish_local() + relay1._publish_local() + relay0._pull_peer() + relay1._pull_peer() + + for relay in (relay0, relay1): + names = {path.name for path in relay.relay_dir.glob("*.tar")} + self.assertFalse(any("step2-" in name for name in names)) + self.assertTrue(any("step3-" in name for name in names)) + self.assertTrue(any("step4-" in name for name in names)) + + # A transiently absent or already-pruned output tree must not erase the + # relay's bounded recovery copies. + shutil.rmtree(node0 / "output") + shutil.rmtree(node1 / "output") + relay0._publish_local() + relay1._publish_local() + for relay in (relay0, relay1): + manifest = json.loads( + (relay.relay_dir / "manifest.json").read_text(encoding="utf-8") + ) + self.assertEqual( + [entry["step"] for entry in manifest["entries"]], [3, 4] + ) + + def test_rank_ranges_and_archive_retention_are_validated(self): + self.assertTrue(os.access(SCRIPT, os.X_OK)) + self.assertEqual(RELAY_MODULE._rank_range("8-15"), tuple(range(8, 16))) + with self.assertRaisesRegex(Exception, "rank range"): + RELAY_MODULE._rank_range("15-8") + self.assertEqual(RELAY_MODULE._positive_int("3"), 3) + with self.assertRaisesRegex(Exception, "at least 1"): + RELAY_MODULE._positive_int("0") + + def test_peer_archive_name_cannot_escape_or_disagree_with_step(self): + relay = self._relay( + self.root / "node0", + local_ranks=range(0, 2), + peer_ranks=range(2, 4), + ) + base_entry = { + "step": 3, + "sha256": "0" * 64, + "files": [ + "training_state_rank2.pt", + "training_state_rank3.pt", + ], + } + with self.assertRaisesRegex(ValueError, "unexpected peer archive"): + relay._install_peer_archive( + {**base_entry, "archive": "../outside-step3-ranks2-3.tar"} + ) + with self.assertRaisesRegex(ValueError, "unexpected peer archive"): + relay._install_peer_archive( + { + **base_entry, + "archive": f"{self.run_id}-step4-ranks2-3.tar", + } + ) + with self.assertRaisesRegex(ValueError, "unexpected peer archive name"): + relay._download("../outside.tar", "0" * 64) + + +if __name__ == "__main__": + unittest.main() From 27502ffc1e93ef04d4c98c8bfc9297686fbc7b05 Mon Sep 17 00:00:00 2001 From: maocheng Date: Sun, 2 Aug 2026 10:33:34 -0700 Subject: [PATCH 27/88] style: apply repository formatters --- .../disagg/sync_distributed_checkpoints.py | 8 ++--- specforge/runtime/data_plane/http_inbox.py | 4 +-- tests/test_runtime/test_http_inbox.py | 5 +-- tests/test_runtime/test_launch_plan.py | 32 +++++++++---------- tests/test_scripts/test_disagg_launchers.py | 28 ++++++---------- .../test_sync_distributed_checkpoints.py | 20 +++--------- 6 files changed, 34 insertions(+), 63 deletions(-) diff --git a/examples/disagg/sync_distributed_checkpoints.py b/examples/disagg/sync_distributed_checkpoints.py index 3437b4e17..88fbc0373 100755 --- a/examples/disagg/sync_distributed_checkpoints.py +++ b/examples/disagg/sync_distributed_checkpoints.py @@ -181,9 +181,7 @@ def _publish_local(self) -> None: checkpoint_dirs = self._checkpoint_dirs()[-self.max_archives :] for step, checkpoint_dir in checkpoint_dirs: sources = [checkpoint_dir / name for name in local_names] - if not all( - path.is_file() and path.stat().st_size > 0 for path in sources - ): + if not all(path.is_file() and path.stat().st_size > 0 for path in sources): continue archive = self.relay_dir / self._archive_name(step) sha_path = archive.with_name(archive.name + ".sha256") @@ -314,9 +312,7 @@ def _pull_peer(self) -> None: entries = sorted( manifest.get("entries", ()), key=lambda item: int(item["step"]) )[-self.max_archives :] - self._prune_peer_archives( - {str(entry["archive"]) for entry in entries} - ) + self._prune_peer_archives({str(entry["archive"]) for entry in entries}) for entry in entries: self._install_peer_archive(entry) diff --git a/specforge/runtime/data_plane/http_inbox.py b/specforge/runtime/data_plane/http_inbox.py index 4f24b1a00..b24ff6eee 100644 --- a/specforge/runtime/data_plane/http_inbox.py +++ b/specforge/runtime/data_plane/http_inbox.py @@ -313,9 +313,7 @@ def mark_consumed(self, n: int) -> None: time.sleep(0.05 * (attempt + 1)) continue if int(payload.get("consumed", -1)) != target: - raise RuntimeError( - "inbox HTTP server did not confirm consumed target" - ) + raise RuntimeError("inbox HTTP server did not confirm consumed target") self._consumed_target = target return raise RuntimeError( diff --git a/tests/test_runtime/test_http_inbox.py b/tests/test_runtime/test_http_inbox.py index 10e91b04a..c119fd2a8 100644 --- a/tests/test_runtime/test_http_inbox.py +++ b/tests/test_runtime/test_http_inbox.py @@ -11,10 +11,7 @@ from urllib.request import Request, urlopen from specforge.runtime.contracts import FeatureSpec, SampleRef -from specforge.runtime.data_plane.http_inbox import ( - InboxHTTPServer, - RemoteInboxChannel, -) +from specforge.runtime.data_plane.http_inbox import InboxHTTPServer, RemoteInboxChannel from specforge.runtime.data_plane.ref_distributor import RefDistributor from specforge.runtime.data_plane.streaming_ref_channel import StreamingRefChannel diff --git a/tests/test_runtime/test_launch_plan.py b/tests/test_runtime/test_launch_plan.py index 741732caf..bb07a85ac 100644 --- a/tests/test_runtime/test_launch_plan.py +++ b/tests/test_runtime/test_launch_plan.py @@ -26,9 +26,9 @@ ReadinessSpec, ServiceSpec, _http_ready, - run_commands, ) from specforge.launch_plan import build_launch_plan as _build_launch_plan +from specforge.launch_plan import run_commands from specforge.training.capture_contract import ServerCaptureContract ALGORITHM = builtin_algorithm_registry().resolve("dflash") @@ -75,9 +75,9 @@ def _offline_disaggregated_config(*, backend="mooncake", producer_segment_size=N if backend == "shared_dir": raw["deployment"]["disaggregated"]["store_root"] = "/shared/features" if producer_segment_size is not None: - raw["deployment"]["disaggregated"]["producer_segment_size"] = ( - producer_segment_size - ) + raw["deployment"]["disaggregated"][ + "producer_segment_size" + ] = producer_segment_size return Config.model_validate(raw) @@ -332,9 +332,9 @@ def test_disaggregated_auto_supervises_both_roles_on_one_node(self): def test_consumer_state_dir_keeps_refs_shared_and_state_node_local(self): raw = _config(mode="disaggregated", nproc=2).model_dump() - raw["deployment"]["disaggregated"]["consumer_state_dir"] = ( - "/local/attempt-state" - ) + raw["deployment"]["disaggregated"][ + "consumer_state_dir" + ] = "/local/attempt-state" plan = build_launch_plan( Config.model_validate(raw), config_path="run.yaml", @@ -374,9 +374,9 @@ def test_consumer_state_dir_rejects_unsupported_modes_and_whitespace(self): for name, (mutate, message) in invalid_cases.items(): with self.subTest(case=name): raw = cfg.model_dump() - raw["deployment"]["disaggregated"]["consumer_state_dir"] = ( - "/local/attempt-state" - ) + raw["deployment"]["disaggregated"][ + "consumer_state_dir" + ] = "/local/attempt-state" mutate(raw) with self.assertRaisesRegex(ValidationError, message): Config.model_validate(raw) @@ -392,9 +392,9 @@ def test_multi_node_requires_node_local_consumer_state(self): def test_multi_node_keeps_wal_local_and_inboxes_shared(self): raw = _config(mode="disaggregated", nproc=2, nnodes=2).model_dump() - raw["deployment"]["disaggregated"]["inbox_server_url"] = ( - "http://trainer-0:35900" - ) + raw["deployment"]["disaggregated"][ + "inbox_server_url" + ] = "http://trainer-0:35900" plan = build_launch_plan( Config.model_validate(raw), config_path="run.yaml", @@ -427,9 +427,9 @@ def test_inbox_server_url_is_typed_and_online_multinode_only(self): Config.model_validate(raw) raw = _config(mode="disaggregated", nproc=2, nnodes=1).model_dump() - raw["deployment"]["disaggregated"]["inbox_server_url"] = ( - "http://trainer-0:35900" - ) + raw["deployment"]["disaggregated"][ + "inbox_server_url" + ] = "http://trainer-0:35900" with self.assertRaisesRegex(ValidationError, "multi-node trainer"): Config.model_validate(raw) diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index 12b9ade20..28f197bc5 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -13,9 +13,7 @@ OFFLINE = ROOT / "examples" / "disagg" / "run_offline.sh" OFFLINE_TWO_NODE = ROOT / "examples" / "disagg" / "run_offline_2node.sh" TWO_NODE = ROOT / "examples" / "disagg" / "run_qwen3_8b_dflash_disagg_2node.sh" -KIMI_K3_CAPTURE = ( - ROOT / "examples" / "disagg" / "run_kimi_k3_dspark_capture_server.sh" -) +KIMI_K3_CAPTURE = ROOT / "examples" / "disagg" / "run_kimi_k3_dspark_capture_server.sh" KIMI_K3_CAPTURE_PATCH = ( ROOT / "patches" / "sglang" / "kimi-k3-f8493a4" / "spec-capture.patch" ) @@ -132,15 +130,13 @@ def test_help_describes_auto_and_explicit_roles(self): def test_kimi_k3_capture_launcher_keeps_prefill_on_the_fast_path(self): source = KIMI_K3_CAPTURE.read_text(encoding="utf-8") self.assertIn('--max-running-requests "$MAX_RUNNING_REQUESTS"', source) - self.assertIn('MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16}', source) - self.assertIn('MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728}', source) - self.assertIn('MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960}', source) - self.assertIn('MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80}', source) + self.assertIn("MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16}", source) + self.assertIn("MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728}", source) + self.assertIn("MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960}", source) + self.assertIn("MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80}", source) self.assertIn('--max-prefill-tokens "$MAX_PREFILL_TOKENS"', source) self.assertIn('--max-mamba-cache-size "$MAX_MAMBA_CACHE_SIZE"', source) - self.assertIn( - "SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2", source - ) + self.assertIn("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2", source) self.assertIn("--disable-cuda-graph", source) self.assertNotIn("--enable-symm-mem", source) @@ -148,24 +144,18 @@ def test_kimi_k3_capture_patch_only_copies_features_on_the_writer_rank(self): source = KIMI_K3_CAPTURE_PATCH.read_text(encoding="utf-8") self.assertIn("self.output_streamer.ps.attn_tp_rank != 0", source) self.assertNotIn(".cpu().clone()", source) - self.assertIn( - 'getattr(logits_output, "_spec_capture_aux_cpu", None)', source - ) + self.assertIn('getattr(logits_output, "_spec_capture_aux_cpu", None)', source) self.assertIn("logits_output.hidden_states.cpu()", source) self.assertIn('"aux" in features', source) self.assertIn('"last_hidden" in features', source) self.assertIn("_should_copy_hidden_states_to_cpu", source) self.assertIn("self.ps.attn_tp_rank == 0", source) - self.assertIn( - "self.logits_output.last_hidden_states = _async_d2h(", source - ) + self.assertIn("self.logits_output.last_hidden_states = _async_d2h(", source) self.assertIn("len(chunks) == 1", source) self.assertIn("ThreadPoolExecutor(", source) self.assertIn('getattr(store, "batch_put_from", None)', source) self.assertIn("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES", source) - self.assertIn( - "req.finished() and req.spec_capture_result is None", source - ) + self.assertIn("req.finished() and req.spec_capture_result is None", source) def test_two_node_wrapper_keeps_training_on_the_unified_cli(self): self.assertTrue(os.access(TWO_NODE, os.X_OK)) diff --git a/tests/test_scripts/test_sync_distributed_checkpoints.py b/tests/test_scripts/test_sync_distributed_checkpoints.py index 1b0104d79..b6a5b067f 100644 --- a/tests/test_scripts/test_sync_distributed_checkpoints.py +++ b/tests/test_scripts/test_sync_distributed_checkpoints.py @@ -75,12 +75,8 @@ def test_two_nodes_assemble_complete_checkpoints_and_bound_archives(self): self._checkpoint(node0, step, range(0, 2)) self._checkpoint(node1, step, range(2, 4)) - relay0 = self._relay( - node0, local_ranks=range(0, 2), peer_ranks=range(2, 4) - ) - relay1 = self._relay( - node1, local_ranks=range(2, 4), peer_ranks=range(0, 2) - ) + relay0 = self._relay(node0, local_ranks=range(0, 2), peer_ranks=range(2, 4)) + relay1 = self._relay(node1, local_ranks=range(2, 4), peer_ranks=range(0, 2)) relay0.peer_url = relay1.relay_dir.as_uri() relay1.peer_url = relay0.relay_dir.as_uri() @@ -93,9 +89,7 @@ def test_two_nodes_assemble_complete_checkpoints_and_bound_archives(self): for step in (2, 3): checkpoint = root / "output" / f"{self.run_id}-step{step}" expected = {"training_state.pt"} - expected.update( - f"training_state_rank{rank}.pt" for rank in range(4) - ) + expected.update(f"training_state_rank{rank}.pt" for rank in range(4)) self.assertTrue( expected.issubset(path.name for path in checkpoint.iterdir()) ) @@ -104,9 +98,7 @@ def test_two_nodes_assemble_complete_checkpoints_and_bound_archives(self): manifest = json.loads( (relay.relay_dir / "manifest.json").read_text(encoding="utf-8") ) - self.assertEqual( - [entry["step"] for entry in manifest["entries"]], [2, 3] - ) + self.assertEqual([entry["step"] for entry in manifest["entries"]], [2, 3]) local_archives = [ path for path in relay.relay_dir.glob("*.tar") @@ -139,9 +131,7 @@ def test_two_nodes_assemble_complete_checkpoints_and_bound_archives(self): manifest = json.loads( (relay.relay_dir / "manifest.json").read_text(encoding="utf-8") ) - self.assertEqual( - [entry["step"] for entry in manifest["entries"]], [3, 4] - ) + self.assertEqual([entry["step"] for entry in manifest["entries"]], [3, 4]) def test_rank_ranges_and_archive_retention_are_validated(self): self.assertTrue(os.access(SCRIPT, os.X_OK)) From 0c47c0a79c78f355b1d24042978704cd0978b910 Mon Sep 17 00:00:00 2001 From: canghua Date: Mon, 3 Aug 2026 10:40:54 +0800 Subject: [PATCH 28/88] fix lint and fix:generate rope_theta that matches old transformer version --- specforge/export/checkpoint_io.py | 2 ++ specforge/export/to_hf.py | 2 +- specforge/export/to_sglang.py | 2 +- tests/test_runtime/test_export.py | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/specforge/export/checkpoint_io.py b/specforge/export/checkpoint_io.py index f2fefe39d..3e2f3fddf 100644 --- a/specforge/export/checkpoint_io.py +++ b/specforge/export/checkpoint_io.py @@ -59,6 +59,8 @@ def rope_kind(payload): config["rope_scaling"] = { key: value for key, value in rope_parameters.items() if key != "rope_theta" } + if "rope_theta" in rope_parameters: + config["rope_theta"] = rope_parameters["rope_theta"] elif ( rope_scaling and not rope_parameters diff --git a/specforge/export/to_hf.py b/specforge/export/to_hf.py index 71be220d7..f97e98c60 100644 --- a/specforge/export/to_hf.py +++ b/specforge/export/to_hf.py @@ -27,8 +27,8 @@ from safetensors import safe_open from specforge.export.checkpoint_io import ( - materialize_draft, apply_legacy_rope_scaling, + materialize_draft, resolve_training_state, ) diff --git a/specforge/export/to_sglang.py b/specforge/export/to_sglang.py index 19c95eea1..b08b856ec 100644 --- a/specforge/export/to_sglang.py +++ b/specforge/export/to_sglang.py @@ -24,8 +24,8 @@ from typing import Dict, Optional from specforge.export.checkpoint_io import ( - materialize_draft, apply_legacy_rope_scaling, + materialize_draft, resolve_training_state, ) diff --git a/tests/test_runtime/test_export.py b/tests/test_runtime/test_export.py index 72b5f69e8..8303bc1fb 100644 --- a/tests/test_runtime/test_export.py +++ b/tests/test_runtime/test_export.py @@ -49,6 +49,7 @@ def test_modern_rope_parameters_are_mirrored_for_legacy_readers(self): config["rope_scaling"], {"rope_type": "yarn", "factor": 128.0}, ) + self.assertEqual(config["rope_theta"], 8_000_000) def test_legacy_rope_scaling_is_mirrored_for_modern_readers(self): from specforge.export.checkpoint_io import apply_legacy_rope_scaling From 40d88f832b2beda85e7c2e8381b217c205819f1f Mon Sep 17 00:00:00 2001 From: David Wang Date: Sun, 2 Aug 2026 23:45:31 -0400 Subject: [PATCH 29/88] fix test --- tests/test_scripts/test_expand_reasoning_conversations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scripts/test_expand_reasoning_conversations.py b/tests/test_scripts/test_expand_reasoning_conversations.py index 5a687f326..d5665658a 100644 --- a/tests/test_scripts/test_expand_reasoning_conversations.py +++ b/tests/test_scripts/test_expand_reasoning_conversations.py @@ -34,7 +34,7 @@ def _load_preprocessing_stack(): distributed_module.get_sp_ring_group = lambda: None sys.modules[f"{package_name}.distributed"] = distributed_module - for module_name in ("template", "parse", "preprocessing"): + for module_name in ("template", "parse", "loss_mask", "preprocessing"): full_name = f"{data_package_name}.{module_name}" module_path = repo_root / "specforge" / "data" / f"{module_name}.py" spec = importlib.util.spec_from_file_location(full_name, module_path) From 2dcfcdfd01a4b72764cf890aa197abb9b762806d Mon Sep 17 00:00:00 2001 From: canghua Date: Mon, 3 Aug 2026 14:57:03 +0800 Subject: [PATCH 30/88] fix lint and del overfit intro in examples/gates/README --- examples/disagg/README.md | 6 ------ tests/test_scripts/test_disagg_launchers.py | 4 +--- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/examples/disagg/README.md b/examples/disagg/README.md index 2f7900527..6473cdc71 100644 --- a/examples/disagg/README.md +++ b/examples/disagg/README.md @@ -159,12 +159,6 @@ That opt-in profile starts, health-checks, and cleans up the owned local services. It does not change the default external-service boundary or attempt to schedule services on remote hosts. -The strict e2e gate at -`scripts/gates/run_disaggregated_overfit_gate.sh` retains full local test-stack -automation: it starts and health-checks Mooncake and SGLang, runs the unified -producer/consumer entry, verifies training and serving, and cleans up owned -processes. That test harness is not the production service supervisor. - Online configs use Mooncake. Offline configs may use either a typed `shared_dir` store or Mooncake. `deployment.disaggregated.control_dir` is the one attempt root from which the launcher derives the reference channel or diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index 7b4751696..5dd6de70e 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -13,9 +13,7 @@ OFFLINE = ROOT / "examples" / "disagg" / "run_offline.sh" OFFLINE_TWO_NODE = ROOT / "examples" / "disagg" / "run_offline_2node.sh" TWO_NODE = ROOT / "examples" / "disagg" / "run_qwen3_8b_dflash_disagg_2node.sh" -INKLING_TWO_NODE = ( - ROOT / "examples" / "disagg" / "run_inkling_dspark_disagg_2node.sh" -) +INKLING_TWO_NODE = ROOT / "examples" / "disagg" / "run_inkling_dspark_disagg_2node.sh" class DisaggregatedWrapperTest(unittest.TestCase): From edc450a84af77ea34b772e770546b1254ebb9b82 Mon Sep 17 00:00:00 2001 From: David Wang Date: Mon, 3 Aug 2026 03:05:43 -0400 Subject: [PATCH 31/88] more fixes --- specforge/training/controller.py | 14 ++++++++--- tests/test_runtime/test_trainer.py | 40 ++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/specforge/training/controller.py b/specforge/training/controller.py index 95e9c4c63..38b87a0b9 100644 --- a/specforge/training/controller.py +++ b/specforge/training/controller.py @@ -139,10 +139,16 @@ def _reduce_ratio_metrics( import torch.distributed as dist if dist.is_available() and dist.is_initialized(): - if process_group is None: - dist.all_reduce(packed) - else: - dist.all_reduce(packed, group=process_group) + world = ( + dist.get_world_size() + if process_group is None + else dist.get_world_size(group=process_group) + ) + if world > 1: + if process_group is None: + dist.all_reduce(packed) + else: + dist.all_reduce(packed, group=process_group) output: Dict[str, float] = {} cursor = 0 diff --git a/tests/test_runtime/test_trainer.py b/tests/test_runtime/test_trainer.py index d82b536ae..12c661d61 100644 --- a/tests/test_runtime/test_trainer.py +++ b/tests/test_runtime/test_trainer.py @@ -286,35 +286,59 @@ def test_ratio_metrics_override_mean_of_means_accuracy(self): self.assertEqual(result.metrics["ce_position_0"], 0.5) self.assertEqual(result.metrics["ce_position_1"], 0.5) + _RATIO_INPUTS = { + "acc": (torch.tensor(2.0), torch.tensor(4.0)), + "ce_position": ( + torch.tensor([1.0, 3.0]), + torch.tensor([2.0, 6.0]), + ), + } + def test_ratio_metrics_sum_numerators_and_denominators_before_dividing(self): remote = torch.tensor([8.0, 6.0, 3.0, 1.0, 2.0, 2.0]) + reduced_groups = [] def all_reduce(packed, *, group): - self.assertEqual(group, "dp") + reduced_groups.append(group) packed.add_(remote) with ( mock.patch("torch.distributed.is_available", return_value=True), mock.patch("torch.distributed.is_initialized", return_value=True), + mock.patch("torch.distributed.get_world_size", return_value=2), mock.patch("torch.distributed.all_reduce", side_effect=all_reduce), ): metrics = _reduce_ratio_metrics( - { - "acc": (torch.tensor(2.0), torch.tensor(4.0)), - "ce_position": ( - torch.tensor([1.0, 3.0]), - torch.tensor([2.0, 6.0]), - ), - }, + self._RATIO_INPUTS, device=torch.device("cpu"), process_group="dp", reduce=True, ) + self.assertEqual(reduced_groups, ["dp"]) self.assertEqual(metrics["acc"], 1.0) self.assertEqual(metrics["ce_position_0"], 1.0) self.assertEqual(metrics["ce_position_1"], 0.5) + def test_ratio_metrics_skip_all_reduce_when_world_size_is_one(self): + with ( + mock.patch("torch.distributed.is_available", return_value=True), + mock.patch("torch.distributed.is_initialized", return_value=True), + mock.patch("torch.distributed.get_world_size", return_value=1), + mock.patch("torch.distributed.all_reduce") as all_reduce_mock, + ): + metrics = _reduce_ratio_metrics( + self._RATIO_INPUTS, + device=torch.device("cpu"), + process_group="dp", + reduce=True, + ) + + all_reduce_mock.assert_not_called() + self.assertEqual(metrics["acc"], 0.5) + self.assertEqual(metrics["ce_position_0"], 0.5) + self.assertEqual(metrics["ce_position_1"], 0.5) + @staticmethod def _eagle_output( *, From 5a8d926c6f3c555d76663e3336ee38955e59a87f Mon Sep 17 00:00:00 2001 From: canghua Date: Mon, 3 Aug 2026 16:49:26 +0800 Subject: [PATCH 32/88] fix lint and merge main --- specforge/data/prompt_builder.py | 16 +++++++++++++--- tests/test_scripts/test_disagg_launchers.py | 1 - 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/specforge/data/prompt_builder.py b/specforge/data/prompt_builder.py index 9b0d63b4d..aa5104f0e 100644 --- a/specforge/data/prompt_builder.py +++ b/specforge/data/prompt_builder.py @@ -144,17 +144,25 @@ def _prepare_raw_prompts( max_length=max_length, min_loss_tokens=min_loss_tokens, limit=limit, - loss_mask_filter=None, + loss_mask_filter=loss_mask_filter, ) class _ProcessedPromptSequence(Sequence[PromptTaskDict]): """Normalize memory-mapped processed rows only when the producer ingests them.""" - def __init__(self, dataset, *, max_length: int, min_loss_tokens: int) -> None: + def __init__( + self, + dataset, + *, + max_length: int, + min_loss_tokens: int, + loss_mask_filter: Callable[[Sequence[int]], bool] | None, + ) -> None: self._dataset = dataset self._max_length = max_length self._min_loss_tokens = min_loss_tokens + self._loss_mask_filter = loss_mask_filter def __len__(self) -> int: return len(self._dataset) @@ -199,7 +207,9 @@ def _materialize_prompt_tasks( ) if prompt is None: continue - if loss_mask_filter is not None and not loss_mask_filter(loss_mask): + if loss_mask_filter is not None and not loss_mask_filter( + prompt["payload"]["loss_mask"] + ): continue prompts.append(prompt) if limit is not None and len(prompts) >= limit: diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index 4f1dca793..7d854925c 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -20,7 +20,6 @@ ) - class DisaggregatedWrapperTest(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory(prefix="disagg_wrapper_") From ed06ab2d0cdc3f0b4edad4b9d437279cab21d858 Mon Sep 17 00:00:00 2001 From: canghua Date: Mon, 3 Aug 2026 18:00:10 +0800 Subject: [PATCH 33/88] restore code according to PR 731 and fix unittest --- specforge/data/prompt_builder.py | 3 +- .../test_kimi_k3_dspark_architectures.py | 675 +++++++++--------- 2 files changed, 350 insertions(+), 328 deletions(-) diff --git a/specforge/data/prompt_builder.py b/specforge/data/prompt_builder.py index aa5104f0e..a3461d6c2 100644 --- a/specforge/data/prompt_builder.py +++ b/specforge/data/prompt_builder.py @@ -94,7 +94,7 @@ def prepare_prompt_tasks( num_proc=num_proc, min_loss_tokens=min_loss_tokens, limit=limit, - loss_mask_filter=loss_mask_filter, + loss_mask_filter=None, ) @@ -143,7 +143,6 @@ def _prepare_raw_prompts( processed_dataset, max_length=max_length, min_loss_tokens=min_loss_tokens, - limit=limit, loss_mask_filter=loss_mask_filter, ) diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py index c9b008ebd..e7307ee95 100644 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ b/tests/test_modeling/test_kimi_k3_dspark_architectures.py @@ -1,7 +1,8 @@ import json +import unittest from pathlib import Path +from unittest.mock import patch -import pytest import torch from transformers.models.qwen3.modeling_qwen3 import Qwen3Config @@ -63,337 +64,359 @@ def _tiny_config(architecture: str, *, layers: int = 5) -> Qwen3Config: return config -@pytest.mark.parametrize( - ("filename", "architecture"), - [ - ("kimi-k3-dspark-5mla.json", "KimiK3DSpark5MLADraftModel"), - ( - "kimi-k3-dspark-4kda-1mla.json", - "KimiK3DSpark4KDA1MLADraftModel", - ), - ], -) -def test_production_configs_match_k3_target(filename, architecture): - config = json.loads((ROOT / "configs" / filename).read_text()) - assert config["architectures"] == [architecture] - assert config["block_size"] == 7 - assert config["num_hidden_layers"] == 5 - assert config["dflash_config"]["target_layer_ids"] == [11, 23, 47, 71, 83] - assert config["mla_use_output_gate"] is True - assert { - "num_attention_heads": config["num_attention_heads"], - "q_lora_rank": config["q_lora_rank"], - "kv_lora_rank": config["kv_lora_rank"], - "qk_nope_head_dim": config["qk_nope_head_dim"], - "qk_rope_head_dim": config["qk_rope_head_dim"], - "v_head_dim": config["v_head_dim"], - } == { - "num_attention_heads": 96, - "q_lora_rank": 1536, - "kv_lora_rank": 512, - "qk_nope_head_dim": 128, - "qk_rope_head_dim": 64, - "v_head_dim": 128, - } - - -def test_reference_full_attention_config_is_exact_old_architecture(): - config = json.loads( - (ROOT / "configs" / "kimi-k3-dspark-fullattn-gqa16.json").read_text() - ) - assert config["architectures"] == ["DSparkDraftModel"] - assert config["block_size"] == 7 - assert config["num_hidden_layers"] == 5 - assert config["layer_types"] == ["full_attention"] * 5 - assert config["num_attention_heads"] == 64 - assert config["num_key_value_heads"] == 16 - assert config["dflash_config"]["target_layer_ids"] == [7, 23, 51, 67, 83] - assert config["rope_scaling"] is None - - -def test_reference_full_attention_recipe_preserves_exact_old_contract(): - config = Config.from_file( - str( - ROOT - / "examples" - / "configs" - / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" +class TestKimiK3DSparkArchitectures(unittest.TestCase): + def test_production_configs_match_k3_target(self): + cases = [ + ("kimi-k3-dspark-5mla.json", "KimiK3DSpark5MLADraftModel"), + ( + "kimi-k3-dspark-4kda-1mla.json", + "KimiK3DSpark4KDA1MLADraftModel", + ), + ] + for filename, architecture in cases: + with self.subTest(filename=filename, architecture=architecture): + self._assert_production_config(filename, architecture) + + def _assert_production_config(self, filename, architecture): + config = json.loads((ROOT / "configs" / filename).read_text()) + assert config["architectures"] == [architecture] + assert config["block_size"] == 7 + assert config["num_hidden_layers"] == 5 + assert config["dflash_config"]["target_layer_ids"] == [11, 23, 47, 71, 83] + assert config["mla_use_output_gate"] is True + assert { + "num_attention_heads": config["num_attention_heads"], + "q_lora_rank": config["q_lora_rank"], + "kv_lora_rank": config["kv_lora_rank"], + "qk_nope_head_dim": config["qk_nope_head_dim"], + "qk_rope_head_dim": config["qk_rope_head_dim"], + "v_head_dim": config["v_head_dim"], + } == { + "num_attention_heads": 96, + "q_lora_rank": 1536, + "kv_lora_rank": 512, + "qk_nope_head_dim": 128, + "qk_rope_head_dim": 64, + "v_head_dim": 128, + } + + def test_reference_full_attention_config_is_exact_old_architecture(self): + config = json.loads( + (ROOT / "configs" / "kimi-k3-dspark-fullattn-gqa16.json").read_text() ) - ) - assert config.model.target_model_path == "/workspace/models/Kimi-K3" - assert config.data.max_length == 4096 - assert config.data.dataloader_num_workers == 4 - assert config.training.batch_size == 1 - assert config.training.accumulation_steps == 32 - assert config.deployment.trainer.nnodes == 2 - assert config.deployment.trainer.nproc_per_node == 8 - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - == 16 - ) - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - == 512 - ) - assert config.training.num_epochs == 10 - assert config.training.total_steps == 9173 - assert config.training.learning_rate == pytest.approx(6e-4) - assert config.training.lr_scheduler == "cosine" - assert config.training.warmup_ratio == pytest.approx(0.04) - assert config.training.num_anchors == 512 - assert config.training.max_checkpoints == 3 - assert config.runtime.producer_lease == 16 - assert len(config.deployment.disaggregated.server_urls) == 2 - assert ( - config.deployment.disaggregated.inbox_server_url - == "http://trainer-node-0:35900" - ) - assert "openperfectblend-regen-9caaf705" in config.data.train_data_path - assert config.tracking.report_to == "wandb" - - -@pytest.mark.parametrize( - "filename", - [ - "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", - "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", - ], -) -def test_training_recipes_preserve_reference_run_contract(filename): - config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) - assert config.data.max_length == 4096 - assert config.training.batch_size == 2 - assert config.training.accumulation_steps == 32 - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - == 16 - ) - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - == 512 - ) - assert config.training.num_epochs == 10 - assert config.training.learning_rate == pytest.approx(6e-4) - assert config.training.warmup_ratio == pytest.approx(0.04) - assert config.training.num_anchors == 512 - assert config.training.save_interval == 250 - assert config.training.log_interval == 10 - optimizer_quantum = ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - ) - max_capture_overshoot = ( - len(config.deployment.disaggregated.server_urls) - * config.runtime.producer_concurrency - * config.runtime.producer_lease - ) - # Two windows are required for capture and training to overlap. Once one - # window is acknowledged, hysteresis must resume capture even at the - # maximum number of concurrently leased refs. - assert config.runtime.in_flight_high_watermark >= 2 * optimizer_quantum - assert ( - config.runtime.in_flight_low_watermark - >= config.runtime.in_flight_high_watermark - + max_capture_overshoot - - optimizer_quantum - ) - # Each capture HTTP request uses the source job's complete 16-sample - # optimizer microbatch to amortize auxiliary-state aggregation. This is a - # producer request size, independent of the DP8 consumer's per-rank batch. - assert config.runtime.producer_lease == 16 - assert config.runtime.producer_concurrency == 2 - assert config.model.sglang_enable_symm_mem is False - assert config.model.sglang_max_running_requests == 16 - # K3 consumes five linear-attention cache entries per live request; 40 - # silently caps SGLang at eight even when max_running_requests is 16. - assert config.model.sglang_max_mamba_cache_size == 80 - assert len(config.deployment.disaggregated.server_urls) >= 2 - # Two worst-case 4,096-token optimizer windows carry about 336 GiB of - # captured features; byte throttling must not serialize the pipeline. - assert config.runtime.resident_high_watermark_bytes >= 360777252864 - assert config.runtime.resident_low_watermark_bytes >= 180388626432 - assert ( - config.runtime.feature_store_max_resident_bytes - >= config.runtime.resident_high_watermark_bytes - ) - assert config.tracking.report_to == "wandb" - assert config.tracking.wandb_offline is False - assert config.data.chat_template in TEMPLATE_REGISTRY.get_all_template_names() - assert "kimi-k3-openperfectblend-regen-439c2fdc" in (config.data.train_data_path) - - -def test_kimi_k3_template_matches_target_xtml_contract(): - template = TEMPLATE_REGISTRY.get("kimi-k3-thinking") - assert template.assistant_header == ( - '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' - ) - assert template.user_header == '<|open|>message role="user"<|sep|>' - assert template.end_of_turn_token == "<|end_of_msg|>" - assert template.parser_type == "thinking" - assert template.enable_thinking is False - assert template.ignore_token == ["<|end_of_msg|>"] - - -def _forward_backward(model): - batch, context_len, query_len = 1, 5, 6 - config = model.config - target_hidden = torch.randn( - batch, - context_len, - len(config.dflash_config["target_layer_ids"]) * config.hidden_size, - ) - noise_embedding = torch.randn( - batch, query_len, config.hidden_size, requires_grad=True - ) - position_ids = torch.arange(context_len + query_len).expand(batch, -1) - output = model( - position_ids=position_ids, - target_hidden=target_hidden, - noise_embedding=noise_embedding, - ) - assert output.shape == (batch, query_len, config.hidden_size) - assert torch.isfinite(output).all() - output.square().mean().backward() - assert noise_embedding.grad is not None - assert torch.isfinite(noise_embedding.grad).all() - - -def test_tiny_5mla_forward_and_backward(): - model = KimiK3DSpark5MLADraftModel(_tiny_config("KimiK3DSpark5MLADraftModel")) - _forward_backward(model) - assert all(layer.self_attn.use_output_gate for layer in model.layers) - - -def test_mla_absorbed_and_expanded_attention_are_algebraically_equivalent(): - torch.manual_seed(7) - batch, queries, keys, heads = 2, 3, 5, 4 - nope_dim, latent_dim, value_dim = 6, 8, 7 - q_nope = torch.randn(batch, queries, heads, nope_dim, dtype=torch.float64) - kv_latent = torch.randn(batch, keys, latent_dim, dtype=torch.float64) - w_kc = torch.randn(heads, nope_dim, latent_dim, dtype=torch.float64) - w_vc = torch.randn(heads, value_dim, latent_dim, dtype=torch.float64) - - q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) - absorbed_scores = torch.einsum("bqhk,bsk->bhqs", q_absorbed, kv_latent) - k_expanded = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc) - expanded_scores = torch.einsum("bqhd,bshd->bhqs", q_nope, k_expanded) - torch.testing.assert_close(absorbed_scores, expanded_scores) - - probabilities = absorbed_scores.softmax(dim=-1) - latent_output = torch.einsum("bhqs,bsk->bhqk", probabilities, kv_latent) - absorbed_output = torch.einsum("bhqk,hvk->bqhv", latent_output, w_vc) - v_expanded = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc) - expanded_output = torch.einsum("bhqs,bshv->bqhv", probabilities, v_expanded) - torch.testing.assert_close(absorbed_output, expanded_output) - - -def test_tiny_4kda_1mla_forward_and_backward(): - model = KimiK3DSpark4KDA1MLADraftModel( - _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - ) - _forward_backward(model) - assert [type(layer.self_attn).__name__ for layer in model.layers] == [ - "KimiK3DraftKDAAttention", - "KimiK3DraftKDAAttention", - "KimiK3DraftMLAAttention", - "KimiK3DraftKDAAttention", - "KimiK3DraftKDAAttention", - ] - first_kda = model.layers[0].self_attn - assert "q_conv1d.weight" in first_kda.state_dict() - assert "q_conv1d.bias" not in first_kda.state_dict() - - -def test_fla_kda_splits_independent_blocks_below_cuda_grid_z_limit(monkeypatch): - calls = [] - - def fake_chunk_kda(**kwargs): - q = kwargs["q"] - calls.append(int(q.shape[0])) - assert q.shape[0] * q.shape[2] <= 8 - assert kwargs["output_final_state"] is False - assert kwargs["use_qk_l2norm_in_kernel"] is True - assert kwargs["use_gate_in_kernel"] is True - assert kwargs["use_beta_sigmoid_in_kernel"] is True - output = ( - q - + kwargs["k"] - + kwargs["v"] - + kwargs["g"] - + kwargs["beta"].unsqueeze(-1) - + kwargs["A_log"].view(1, 1, -1, 1) - + kwargs["dt_bias"].view(1, 1, q.shape[2], q.shape[3]) + assert config["architectures"] == ["DSparkDraftModel"] + assert config["block_size"] == 7 + assert config["num_hidden_layers"] == 5 + assert config["layer_types"] == ["full_attention"] * 5 + assert config["num_attention_heads"] == 64 + assert config["num_key_value_heads"] == 16 + assert config["dflash_config"]["target_layer_ids"] == [7, 23, 51, 67, 83] + assert config["rope_scaling"] is None + + def test_reference_full_attention_recipe_preserves_exact_old_contract(self): + config = Config.from_file( + str( + ROOT + / "examples" + / "configs" + / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" + ) + ) + assert config.model.target_model_path == "/workspace/models/Kimi-K3" + assert config.data.max_length == 4096 + assert config.data.dataloader_num_workers == 4 + assert config.training.batch_size == 1 + assert config.training.accumulation_steps == 32 + assert config.deployment.trainer.nnodes == 2 + assert config.deployment.trainer.nproc_per_node == 8 + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + == 16 + ) + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + == 512 + ) + assert config.training.num_epochs == 10 + assert config.training.total_steps == 9173 + self.assertAlmostEqual( + config.training.learning_rate, + 6e-4, + delta=max(1e-12, 1e-6 * abs(6e-4)), + ) + assert config.training.lr_scheduler == "cosine" + self.assertAlmostEqual( + config.training.warmup_ratio, + 0.04, + delta=max(1e-12, 1e-6 * abs(0.04)), + ) + assert config.training.num_anchors == 512 + assert config.training.max_checkpoints == 3 + assert config.runtime.producer_lease == 16 + assert len(config.deployment.disaggregated.server_urls) == 2 + assert ( + config.deployment.disaggregated.inbox_server_url + == "http://trainer-node-0:35900" + ) + assert "openperfectblend-regen-9caaf705" in config.data.train_data_path + assert config.tracking.report_to == "wandb" + + def test_training_recipes_preserve_reference_run_contract(self): + for filename in [ + "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", + "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", + ]: + with self.subTest(filename=filename): + self._assert_training_recipe(filename) + + def _assert_training_recipe(self, filename): + config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) + assert config.data.max_length == 4096 + assert config.training.batch_size == 2 + assert config.training.accumulation_steps == 32 + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + == 16 + ) + assert ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + == 512 + ) + assert config.training.num_epochs == 10 + self.assertAlmostEqual( + config.training.learning_rate, + 6e-4, + delta=max(1e-12, 1e-6 * abs(6e-4)), + ) + self.assertAlmostEqual( + config.training.warmup_ratio, + 0.04, + delta=max(1e-12, 1e-6 * abs(0.04)), + ) + assert config.training.num_anchors == 512 + assert config.training.save_interval == 250 + assert config.training.log_interval == 10 + optimizer_quantum = ( + config.deployment.trainer.nnodes + * config.deployment.trainer.nproc_per_node + * config.training.batch_size + * config.training.accumulation_steps + ) + max_capture_overshoot = ( + len(config.deployment.disaggregated.server_urls) + * config.runtime.producer_concurrency + * config.runtime.producer_lease + ) + # Two windows are required for capture and training to overlap. Once one + # window is acknowledged, hysteresis must resume capture even at the + # maximum number of concurrently leased refs. + assert config.runtime.in_flight_high_watermark >= 2 * optimizer_quantum + assert ( + config.runtime.in_flight_low_watermark + >= config.runtime.in_flight_high_watermark + + max_capture_overshoot + - optimizer_quantum + ) + # Each capture HTTP request uses the source job's complete 16-sample + # optimizer microbatch to amortize auxiliary-state aggregation. This is a + # producer request size, independent of the DP8 consumer's per-rank batch. + assert config.runtime.producer_lease == 16 + assert config.runtime.producer_concurrency == 2 + assert config.model.sglang_enable_symm_mem is False + assert config.model.sglang_max_running_requests == 16 + # K3 consumes five linear-attention cache entries per live request; 40 + # silently caps SGLang at eight even when max_running_requests is 16. + assert config.model.sglang_max_mamba_cache_size == 80 + assert len(config.deployment.disaggregated.server_urls) >= 2 + # Two worst-case 4,096-token optimizer windows carry about 336 GiB of + # captured features; byte throttling must not serialize the pipeline. + assert config.runtime.resident_high_watermark_bytes >= 360777252864 + assert config.runtime.resident_low_watermark_bytes >= 180388626432 + assert ( + config.runtime.feature_store_max_resident_bytes + >= config.runtime.resident_high_watermark_bytes + ) + assert config.tracking.report_to == "wandb" + assert config.tracking.wandb_offline is False + assert config.data.chat_template in TEMPLATE_REGISTRY.get_all_template_names() + assert "kimi-k3-openperfectblend-regen-439c2fdc" in ( + config.data.train_data_path ) - return output, None - - monkeypatch.setattr(kimi_k3_dspark, "_CUDA_MAX_GRID_DIM_Z", 8) - monkeypatch.setattr(kimi_k3_dspark, "_load_fla_chunk_kda", lambda: fake_chunk_kda) - - shape = (5, 2, 3, 4) - q, k, v, gate = (torch.randn(shape, requires_grad=True) for _ in range(4)) - beta = torch.randn(shape[:-1], requires_grad=True) - A_log = torch.randn(shape[2], requires_grad=True) - dt_bias = torch.randn(shape[2] * shape[3], requires_grad=True) - output = kimi_k3_dspark._fla_kda( - q, - k, - v, - gate, - beta, - A_log, - dt_bias, - lower_bound=-5.0, - ) - - assert calls == [2, 2, 1] - assert output.shape == shape - output.sum().backward() - for tensor in (q, k, v, gate, beta, A_log, dt_bias): - assert tensor.grad is not None - assert torch.isfinite(tensor.grad).all() - -def test_kda_resets_state_between_proposal_blocks(): - config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - attention = KimiK3DraftKDAAttention(config, layer_idx=0) - first = torch.randn(1, config.block_size, config.hidden_size) - second = torch.randn_like(first) - baseline = attention(torch.cat((first, second), dim=1))[0] - changed = attention(torch.cat((first, second + 20.0), dim=1))[0] - torch.testing.assert_close( - baseline[:, : config.block_size], - changed[:, : config.block_size], - ) + def test_kimi_k3_template_matches_target_xtml_contract(self): + template = TEMPLATE_REGISTRY.get("kimi-k3-thinking") + assert template.assistant_header == ( + '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' + ) + assert template.user_header == '<|open|>message role="user"<|sep|>' + assert template.end_of_turn_token == "<|end_of_msg|>" + assert template.parser_type == "thinking" + assert template.enable_thinking is False + assert template.ignore_token == ["<|end_of_msg|>"] + + def _forward_backward(self, model): + batch, context_len, query_len = 1, 5, 6 + config = model.config + target_hidden = torch.randn( + batch, + context_len, + len(config.dflash_config["target_layer_ids"]) * config.hidden_size, + ) + noise_embedding = torch.randn( + batch, query_len, config.hidden_size, requires_grad=True + ) + position_ids = torch.arange(context_len + query_len).expand(batch, -1) + output = model( + position_ids=position_ids, + target_hidden=target_hidden, + noise_embedding=noise_embedding, + ) + assert output.shape == (batch, query_len, config.hidden_size) + assert torch.isfinite(output).all() + output.square().mean().backward() + assert noise_embedding.grad is not None + assert torch.isfinite(noise_embedding.grad).all() + + def test_tiny_5mla_forward_and_backward(self): + model = KimiK3DSpark5MLADraftModel(_tiny_config("KimiK3DSpark5MLADraftModel")) + self._forward_backward(model) + assert all(layer.self_attn.use_output_gate for layer in model.layers) + + def test_mla_absorbed_and_expanded_attention_are_algebraically_equivalent(self): + torch.manual_seed(7) + batch, queries, keys, heads = 2, 3, 5, 4 + nope_dim, latent_dim, value_dim = 6, 8, 7 + q_nope = torch.randn(batch, queries, heads, nope_dim, dtype=torch.float64) + kv_latent = torch.randn(batch, keys, latent_dim, dtype=torch.float64) + w_kc = torch.randn(heads, nope_dim, latent_dim, dtype=torch.float64) + w_vc = torch.randn(heads, value_dim, latent_dim, dtype=torch.float64) + + q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) + absorbed_scores = torch.einsum("bqhk,bsk->bhqs", q_absorbed, kv_latent) + k_expanded = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc) + expanded_scores = torch.einsum("bqhd,bshd->bhqs", q_nope, k_expanded) + torch.testing.assert_close(absorbed_scores, expanded_scores) + + probabilities = absorbed_scores.softmax(dim=-1) + latent_output = torch.einsum("bhqs,bsk->bhqk", probabilities, kv_latent) + absorbed_output = torch.einsum("bhqk,hvk->bqhv", latent_output, w_vc) + v_expanded = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc) + expanded_output = torch.einsum("bhqs,bshv->bqhv", probabilities, v_expanded) + torch.testing.assert_close(absorbed_output, expanded_output) + + def test_tiny_4kda_1mla_forward_and_backward(self): + model = KimiK3DSpark4KDA1MLADraftModel( + _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + ) + self._forward_backward(model) + assert [type(layer.self_attn).__name__ for layer in model.layers] == [ + "KimiK3DraftKDAAttention", + "KimiK3DraftKDAAttention", + "KimiK3DraftMLAAttention", + "KimiK3DraftKDAAttention", + "KimiK3DraftKDAAttention", + ] + first_kda = model.layers[0].self_attn + assert "q_conv1d.weight" in first_kda.state_dict() + assert "q_conv1d.bias" not in first_kda.state_dict() + + def test_fla_kda_splits_independent_blocks_below_cuda_grid_z_limit(self): + calls = [] + + def fake_chunk_kda(**kwargs): + q = kwargs["q"] + calls.append(int(q.shape[0])) + assert q.shape[0] * q.shape[2] <= 8 + assert kwargs["output_final_state"] is False + assert kwargs["use_qk_l2norm_in_kernel"] is True + assert kwargs["use_gate_in_kernel"] is True + assert kwargs["use_beta_sigmoid_in_kernel"] is True + output = ( + q + + kwargs["k"] + + kwargs["v"] + + kwargs["g"] + + kwargs["beta"].unsqueeze(-1) + + kwargs["A_log"].view(1, 1, -1, 1) + + kwargs["dt_bias"].view(1, 1, q.shape[2], q.shape[3]) + ) + return output, None + + shape = (5, 2, 3, 4) + q, k, v, gate = (torch.randn(shape, requires_grad=True) for _ in range(4)) + beta = torch.randn(shape[:-1], requires_grad=True) + A_log = torch.randn(shape[2], requires_grad=True) + dt_bias = torch.randn(shape[2] * shape[3], requires_grad=True) + with ( + patch.object(kimi_k3_dspark, "_CUDA_MAX_GRID_DIM_Z", 8), + patch.object( + kimi_k3_dspark, + "_load_fla_chunk_kda", + return_value=fake_chunk_kda, + ), + ): + output = kimi_k3_dspark._fla_kda( + q, + k, + v, + gate, + beta, + A_log, + dt_bias, + lower_bound=-5.0, + ) + + assert calls == [2, 2, 1] + assert output.shape == shape + output.sum().backward() + for tensor in (q, k, v, gate, beta, A_log, dt_bias): + assert tensor.grad is not None + assert torch.isfinite(tensor.grad).all() + + def test_kda_resets_state_between_proposal_blocks(self): + config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + attention = KimiK3DraftKDAAttention(config, layer_idx=0) + first = torch.randn(1, config.block_size, config.hidden_size) + second = torch.randn_like(first) + baseline = attention(torch.cat((first, second), dim=1))[0] + changed = attention(torch.cat((first, second + 20.0), dim=1))[0] + torch.testing.assert_close( + baseline[:, : config.block_size], + changed[:, : config.block_size], + ) + def test_hybrid_rejects_noncanonical_layer_order(self): + config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") + config.draft_layer_types = ["mla", "kda", "kda", "kda", "kda"] + with self.assertRaisesRegex(ValueError, "layer pattern"): + KimiK3DSpark4KDA1MLADraftModel(config) -def test_hybrid_rejects_noncanonical_layer_order(): - config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - config.draft_layer_types = ["mla", "kda", "kda", "kda", "kda"] - with pytest.raises(ValueError, match="layer pattern"): - KimiK3DSpark4KDA1MLADraftModel(config) + def test_5mla_rejects_noncanonical_layer_count(self): + config = _tiny_config("KimiK3DSpark5MLADraftModel", layers=4) + with self.assertRaisesRegex(ValueError, "exactly 5 layers"): + KimiK3DSpark5MLADraftModel(config) + def test_mla_requires_k3_attention_features(self): + for field in ("mla_use_nope", "mla_use_output_gate"): + with self.subTest(field=field): + self._assert_mla_requires_k3_attention_feature(field) -def test_5mla_rejects_noncanonical_layer_count(): - config = _tiny_config("KimiK3DSpark5MLADraftModel", layers=4) - with pytest.raises(ValueError, match="exactly 5 layers"): - KimiK3DSpark5MLADraftModel(config) + def _assert_mla_requires_k3_attention_feature(self, field): + config = _tiny_config("KimiK3DSpark5MLADraftModel") + setattr(config, field, False) + with self.assertRaisesRegex(ValueError, field): + KimiK3DSpark5MLADraftModel(config) -@pytest.mark.parametrize("field", ["mla_use_nope", "mla_use_output_gate"]) -def test_mla_requires_k3_attention_features(field): - config = _tiny_config("KimiK3DSpark5MLADraftModel") - setattr(config, field, False) - with pytest.raises(ValueError, match=field): - KimiK3DSpark5MLADraftModel(config) +if __name__ == "__main__": + unittest.main() From 469da28f357f8af622c7a4c46df6026a7a0ddb60 Mon Sep 17 00:00:00 2001 From: Yi Sun Date: Mon, 3 Aug 2026 13:42:30 +0000 Subject: [PATCH 34/88] feat: Kimi-K3 DSpark support -- template, draft config, example run --- configs/kimi-k3-dspark.json | 50 +++++++++++++++++++ .../configs/kimi-k3-dspark-disaggregated.yaml | 42 ++++++++++++++++ specforge/data/template.py | 15 ++++++ tests/test_data/test_kimi_k3_template.py | 27 ++++++++++ 4 files changed, 134 insertions(+) create mode 100644 configs/kimi-k3-dspark.json create mode 100644 examples/configs/kimi-k3-dspark-disaggregated.yaml create mode 100644 tests/test_data/test_kimi_k3_template.py diff --git a/configs/kimi-k3-dspark.json b/configs/kimi-k3-dspark.json new file mode 100644 index 000000000..5b4dec061 --- /dev/null +++ b/configs/kimi-k3-dspark.json @@ -0,0 +1,50 @@ +{ + "architectures": ["DSparkDraftModel"], + "attention_bias": false, + "attention_dropout": 0.0, + "auto_map": {"AutoModel": "dspark.DSparkDraftModel"}, + "block_size": 7, + "bos_token_id": 163584, + "dflash_config": { + "attention_mode": "gqa", + "confidence_head_alpha": 1.0, + "confidence_head_with_markov": true, + "enable_confidence_head": true, + "markov_head_type": "vanilla", + "markov_rank": 256, + "mask_token_id": 163824, + "projector_type": "dspark", + "target_layer_ids": [7, 23, 51, 67, 83] + }, + "dtype": "bfloat16", + "eos_token_id": 163586, + "head_dim": 64, + "hidden_act": "silu", + "hidden_size": 7168, + "initializer_range": 0.02, + "intermediate_size": 14336, + "layer_types": [ + "full_attention", "full_attention", "full_attention", + "full_attention", "full_attention" + ], + "max_position_embeddings": 1048576, + "max_window_layers": 5, + "model_type": "qwen3", + "num_attention_heads": 64, + "num_hidden_layers": 5, + "num_key_value_heads": 16, + "num_target_layers": 93, + "pad_token_id": 163839, + "rms_norm_eps": 1e-05, + "rope_parameters": { + "factor": 16.0, + "original_max_position_embeddings": 65536, + "rope_theta": 10000.0, + "rope_type": "yarn" + }, + "sliding_window": null, + "tie_word_embeddings": false, + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 163840 +} diff --git a/examples/configs/kimi-k3-dspark-disaggregated.yaml b/examples/configs/kimi-k3-dspark-disaggregated.yaml new file mode 100644 index 000000000..5b87504b9 --- /dev/null +++ b/examples/configs/kimi-k3-dspark-disaggregated.yaml @@ -0,0 +1,42 @@ +model: + target_model_path: moonshotai/Kimi-K3 + draft_model_config: configs/kimi-k3-dspark.json + target_backend: sglang + embedding_key: language_model.model.embed_tokens.weight + lm_head_key: language_model.lm_head.weight +data: + train_data_path: ./cache/dataset/kimi_k3_dspark_train.jsonl + max_length: 4096 + chat_template: kimi-k3-thinking + cache_dir: cache + build_dataset_num_proc: 64 +training: + strategy: dspark + num_epochs: 10 + # Portable one-rank equivalent of the source recipe's global batch 512. + batch_size: 1 + accumulation_steps: 512 + learning_rate: 0.0006 + warmup_ratio: 0.04 + max_grad_norm: 1.0 + num_anchors: 512 + loss_decay_gamma: 4.0 + objective_chunk_blocks: 128 + # Optimizer-step equivalent of about 250 source microsteps at microbatch 16. + save_interval: 8 + dist_timeout: 30 + seed: 42 +run_id: kimi-k3-dspark-disaggregated +output_dir: outputs/kimi-k3-dspark-disaggregated +deployment: + mode: disaggregated + trainer: + nnodes: 1 + nproc_per_node: 1 + disaggregated: + control_dir: outputs/kimi-k3-dspark-disaggregated/control + backend: mooncake + server_urls: [http://127.0.0.1:30000] + mooncake_metadata_server: http://127.0.0.1:35880/metadata + mooncake_master_server_addr: 127.0.0.1:35551 + mooncake_protocol: tcp diff --git a/specforge/data/template.py b/specforge/data/template.py index cfb409829..da1c85490 100644 --- a/specforge/data/template.py +++ b/specforge/data/template.py @@ -249,6 +249,21 @@ def get_all_template_names(self) -> List[str]: ), ) +TEMPLATE_REGISTRY.register( + name="kimi-k3-thinking", + template=ChatTemplate( + assistant_header=( + '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' + ), + user_header='<|open|>message role="user"<|sep|>', + system_prompt=None, + end_of_turn_token="<|end_of_msg|>", + parser_type="thinking", + enable_thinking=False, + ignore_token=["<|end_of_msg|>"], + ), +) + TEMPLATE_REGISTRY.register( name="deepseek-v3", template=ChatTemplate( diff --git a/tests/test_data/test_kimi_k3_template.py b/tests/test_data/test_kimi_k3_template.py new file mode 100644 index 000000000..60496523f --- /dev/null +++ b/tests/test_data/test_kimi_k3_template.py @@ -0,0 +1,27 @@ +"""Kimi-K3 template registration and draft-config contract.""" + +import json +import unittest +from pathlib import Path + +from specforge.data.template import TEMPLATE_REGISTRY + + +class TestKimiK3Template(unittest.TestCase): + def test_registered_with_thinking_contract(self): + t = TEMPLATE_REGISTRY.get("kimi-k3-thinking") + self.assertIsNotNone(t) + self.assertEqual(t.parser_type, "thinking") + # Reasoning is stored inline in assistant content (deepspec-style + # regenerations), so the split-field thinking path stays off. + self.assertFalse(t.enable_thinking) + # The assistant header must end inside the think block: the chat + # template emits the opening think tag as part of the generation + # prompt, so it is never model output. + self.assertTrue(t.assistant_header.endswith("<|open|>think<|sep|>")) + self.assertEqual(t.end_of_turn_token, "<|end_of_msg|>") + self.assertIn("<|end_of_msg|>", t.ignore_token) + + +if __name__ == "__main__": + unittest.main() From 3e71683bc9ad498ad87fbab443f2e76f9b5b33a9 Mon Sep 17 00:00:00 2001 From: ddlearn <1220585470@qq.com> Date: Mon, 3 Aug 2026 22:24:42 +0800 Subject: [PATCH 35/88] perf: make reference channel polling linear --- .../runtime/data_plane/streaming_ref_channel.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/specforge/runtime/data_plane/streaming_ref_channel.py b/specforge/runtime/data_plane/streaming_ref_channel.py index f1008b165..927962cd7 100644 --- a/specforge/runtime/data_plane/streaming_ref_channel.py +++ b/specforge/runtime/data_plane/streaming_ref_channel.py @@ -41,6 +41,7 @@ import os import threading import time +from collections import deque from dataclasses import dataclass from typing import Iterator, List, Optional, Sequence @@ -114,7 +115,8 @@ def __init__(self, path: str) -> None: self._published = 0 # consumer-side self._read_offset = 0 - self._buf = "" + self._partial_line = "" + self._complete_lines = deque[str]() self._consumed = 0 # ``mark_consumed`` is called from both the trainer thread (batch acks) # and the prefetch worker (failure settlement), so the increment and @@ -327,15 +329,16 @@ def poll(self, max_n: Optional[int] = None) -> List[SampleRef]: except FileNotFoundError: chunk = "" if chunk: - self._buf += chunk - # parse the buffer even when no new bytes arrived -- a previous max_n call - # may have left complete lines buffered. + lines = (self._partial_line + chunk).split("\n") + self._partial_line = lines.pop() + self._complete_lines.extend(lines) + # Parse queued lines even when no new bytes arrived -- a previous max_n + # call may have left complete lines buffered. out: List[SampleRef] = [] - while "\n" in self._buf: + while self._complete_lines: if max_n is not None and len(out) >= max_n: break - line, self._buf = self._buf.split("\n", 1) - line = line.strip() + line = self._complete_lines.popleft().strip() if line: out.append(ref_from_dict(json.loads(line))) return out From 2768619ee19c8265d161e1f8c1aa4fb09ef238c7 Mon Sep 17 00:00:00 2001 From: wjp666666 <1969554248@qq.com> Date: Tue, 4 Aug 2026 01:23:38 +0800 Subject: [PATCH 36/88] update news about specforge v0.3.0 fix grammar fix grammar --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d34a68055..61f0c63dd 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,13 @@ SpecBundle is a collection of production-grade speculative decoding models that ## 🎉 News - +- [2026-08] 🎉 Released SpecBundle (phase 2) and SpecForge v0.3.0. Check out our blog at [LMSYS.org](https://www.lmsys.org/blog) +- [2026-07] 🔥 Supported full disaggregation of training and inference in online training. +- [2026-07] 🔥 Added DSpark online training for DFlash draft models. - [2026-06] 🔥 Added D-PACE as an optional loss for DFlash training. - [2026-06] 🔥 Added Domino online training for DFlash draft models. - [2026-01] 🔥 Added DFlash block-parallel online training with SGLang serving support. -- [2025-12] 🎉 Released SpecBundle (phase 1) and SpecForge v0.2. Check out our blog at [LMSYS.org](https://lmsys.org/blog/2025-12-23-spec-bundle-phase-1/) +- [2025-12] 🎉 Released SpecBundle (phase 1) and SpecForge v0.2.0. Check out our blog at [LMSYS.org](https://lmsys.org/blog/2025-12-23-spec-bundle-phase-1/) - [2025-08] 🔔 SpecForge is listed as a [flagship project](https://lmsys.org/about/) in LMSYS. Congratulations to the SpecForge team! - [2025-08] 🔥 SpecForge powered the Eagle3 draft model for GPT-OSS. Check out the blog at [LMSYS.org](https://lmsys.org/blog/2025-08-27-gpt-oss/) - [2025-07] 🔥 SpecForge is released together with Llama4-Eagle3 checkpoints. Check out our blog at [LMSYS.org](https://lmsys.org/blog/2025-07-25-spec-forge/) From 0d505d6351bde623a3dad2172bbec49903f7d702 Mon Sep 17 00:00:00 2001 From: wjp666666 <1969554248@qq.com> Date: Tue, 4 Aug 2026 01:41:03 +0800 Subject: [PATCH 37/88] add inkling and kimi-k3 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 61f0c63dd..88d354822 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ SpecBundle is a collection of production-grade speculative decoding models that ## 🎉 News - [2026-08] 🎉 Released SpecBundle (phase 2) and SpecForge v0.3.0. Check out our blog at [LMSYS.org](https://www.lmsys.org/blog) +- [2026-07] 🚀 Day0 supported two flagship dspark draft model, [Inklink](https://huggingface.co/RadixArk/Inkling-DSpark-Preview) and [Kimi-K3](https://huggingface.co/RadixArk/Kimi-K3-DSpark). - [2026-07] 🔥 Supported full disaggregation of training and inference in online training. - [2026-07] 🔥 Added DSpark online training for DFlash draft models. - [2026-06] 🔥 Added D-PACE as an optional loss for DFlash training. From f19d389b7e0f8f1cbfaf708373370ff858b8d37e Mon Sep 17 00:00:00 2001 From: Yi Sun Date: Tue, 4 Aug 2026 00:16:55 +0000 Subject: [PATCH 38/88] Remove in-house Kimi-K3 MLA/KDA experiments and publish a single renamed DSpark recipe. Drop the experimental draft backbones, private dataset prep, and v1c codename so only the public plain-DSpark Kimi-K3 disaggregated runbook remains. --- configs/kimi-k3-dspark-4kda-1mla.json | 67 -- configs/kimi-k3-dspark-5mla.json | 58 -- configs/kimi-k3-dspark-fullattn-gqa16.json | 49 -- ...k3-dspark-v1c.json => kimi-k3-dspark.json} | 0 ...ted.md => kimi-k3-dspark-disaggregated.md} | 16 +- .../kimi-k3-dspark-mla-kda-disaggregated.md | 206 ------ examples/README.md | 3 +- examples/configs/README.md | 6 +- ...a-1mla-openperfectblend-disaggregated.yaml | 93 --- ...k-5mla-openperfectblend-disaggregated.yaml | 100 --- ...yaml => kimi-k3-dspark-disaggregated.yaml} | 20 +- ...llattn-openperfectblend-disaggregated.yaml | 107 --- examples/disagg/README.md | 5 +- .../run_kimi_k3_dspark_capture_server.sh | 69 -- pyproject.toml | 1 - scripts/prepare_kimi_k3_openperfectblend.sh | 62 -- specforge/algorithms/dspark/providers.py | 8 +- specforge/modeling/draft/__init__.py | 3 - specforge/modeling/draft/kimi_k3_dspark.py | 700 ------------------ tests/test_config/test_launch_topology.py | 76 +- tests/test_data/test_template_registry.py | 15 + .../test_kimi_k3_dspark_architectures.py | 422 ----------- tests/test_runtime/test_model_loading.py | 20 - .../test_runtime/test_package_architecture.py | 3 +- tests/test_scripts/test_disagg_launchers.py | 16 +- 25 files changed, 45 insertions(+), 2080 deletions(-) delete mode 100644 configs/kimi-k3-dspark-4kda-1mla.json delete mode 100644 configs/kimi-k3-dspark-5mla.json delete mode 100644 configs/kimi-k3-dspark-fullattn-gqa16.json rename configs/{kimi-k3-dspark-v1c.json => kimi-k3-dspark.json} (100%) rename docs/recipes/{kimi-k3-dspark-v1c-disaggregated.md => kimi-k3-dspark-disaggregated.md} (90%) delete mode 100644 docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md delete mode 100644 examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml delete mode 100644 examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml rename examples/configs/{kimi-k3-dspark-v1c-disaggregated.yaml => kimi-k3-dspark-disaggregated.yaml} (75%) delete mode 100644 examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml delete mode 100755 examples/disagg/run_kimi_k3_dspark_capture_server.sh delete mode 100755 scripts/prepare_kimi_k3_openperfectblend.sh delete mode 100644 specforge/modeling/draft/kimi_k3_dspark.py delete mode 100644 tests/test_modeling/test_kimi_k3_dspark_architectures.py diff --git a/configs/kimi-k3-dspark-4kda-1mla.json b/configs/kimi-k3-dspark-4kda-1mla.json deleted file mode 100644 index cc3695452..000000000 --- a/configs/kimi-k3-dspark-4kda-1mla.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "architectures": ["KimiK3DSpark4KDA1MLADraftModel"], - "attention_bias": false, - "attention_dropout": 0.0, - "auto_map": { - "AutoModel": "kimi_k3_dspark.KimiK3DSpark4KDA1MLADraftModel" - }, - "block_size": 7, - "bos_token_id": 163584, - "dflash_config": { - "confidence_head_alpha": 1.0, - "confidence_head_with_markov": true, - "enable_confidence_head": true, - "markov_head_type": "vanilla", - "markov_rank": 256, - "mask_token_id": 163824, - "projector_type": "dspark", - "target_layer_ids": [11, 23, 47, 71, 83] - }, - "draft_layer_types": ["kda", "kda", "mla", "kda", "kda"], - "dtype": "bfloat16", - "eos_token_id": 163586, - "head_dim": 192, - "hidden_act": "silu", - "hidden_size": 7168, - "initializer_range": 0.02, - "intermediate_size": 14336, - "kv_lora_rank": 512, - "layer_types": [ - "full_attention", - "full_attention", - "full_attention", - "full_attention", - "full_attention" - ], - "linear_attn_config": { - "backend": "fla", - "gate_lower_bound": -5.0, - "head_dim": 128, - "num_heads": 96, - "short_conv_kernel_size": 4, - "use_full_rank_gate": true - }, - "max_position_embeddings": 1048576, - "max_window_layers": 5, - "mla_use_nope": true, - "mla_use_output_gate": true, - "model_type": "qwen3", - "num_attention_heads": 96, - "num_hidden_layers": 5, - "num_key_value_heads": 1, - "num_target_layers": 93, - "pad_token_id": 163839, - "q_lora_rank": 1536, - "qk_nope_head_dim": 128, - "qk_rope_head_dim": 64, - "rms_norm_eps": 0.00001, - "rope_interleave": true, - "rope_scaling": null, - "rope_theta": 10000.0, - "sliding_window": null, - "tie_word_embeddings": false, - "use_cache": true, - "use_sliding_window": false, - "v_head_dim": 128, - "vocab_size": 163840 -} diff --git a/configs/kimi-k3-dspark-5mla.json b/configs/kimi-k3-dspark-5mla.json deleted file mode 100644 index 2b87ddc0b..000000000 --- a/configs/kimi-k3-dspark-5mla.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "architectures": ["KimiK3DSpark5MLADraftModel"], - "attention_bias": false, - "attention_dropout": 0.0, - "auto_map": { - "AutoModel": "kimi_k3_dspark.KimiK3DSpark5MLADraftModel" - }, - "block_size": 7, - "bos_token_id": 163584, - "dflash_config": { - "confidence_head_alpha": 1.0, - "confidence_head_with_markov": true, - "enable_confidence_head": true, - "markov_head_type": "vanilla", - "markov_rank": 256, - "mask_token_id": 163824, - "projector_type": "dspark", - "target_layer_ids": [11, 23, 47, 71, 83] - }, - "dtype": "bfloat16", - "eos_token_id": 163586, - "head_dim": 192, - "hidden_act": "silu", - "hidden_size": 7168, - "initializer_range": 0.02, - "intermediate_size": 14336, - "kv_lora_rank": 512, - "layer_types": [ - "full_attention", - "full_attention", - "full_attention", - "full_attention", - "full_attention" - ], - "max_position_embeddings": 1048576, - "max_window_layers": 5, - "mla_use_nope": true, - "mla_use_output_gate": true, - "model_type": "qwen3", - "num_attention_heads": 96, - "num_hidden_layers": 5, - "num_key_value_heads": 1, - "num_target_layers": 93, - "pad_token_id": 163839, - "q_lora_rank": 1536, - "qk_nope_head_dim": 128, - "qk_rope_head_dim": 64, - "rms_norm_eps": 0.00001, - "rope_interleave": true, - "rope_scaling": null, - "rope_theta": 10000.0, - "sliding_window": null, - "tie_word_embeddings": false, - "use_cache": true, - "use_sliding_window": false, - "v_head_dim": 128, - "vocab_size": 163840 -} diff --git a/configs/kimi-k3-dspark-fullattn-gqa16.json b/configs/kimi-k3-dspark-fullattn-gqa16.json deleted file mode 100644 index 32527337b..000000000 --- a/configs/kimi-k3-dspark-fullattn-gqa16.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "architectures": ["DSparkDraftModel"], - "attention_bias": false, - "attention_dropout": 0.0, - "auto_map": {"AutoModel": "dspark.DSparkDraftModel"}, - "block_size": 7, - "bos_token_id": 163584, - "dflash_config": { - "attention_mode": "gqa", - "confidence_head_alpha": 1.0, - "confidence_head_with_markov": true, - "enable_confidence_head": true, - "markov_head_type": "vanilla", - "markov_rank": 256, - "mask_token_id": 163824, - "projector_type": "dspark", - "target_layer_ids": [7, 23, 51, 67, 83] - }, - "dtype": "bfloat16", - "eos_token_id": 163586, - "head_dim": 64, - "hidden_act": "silu", - "hidden_size": 7168, - "initializer_range": 0.02, - "intermediate_size": 14336, - "layer_types": [ - "full_attention", - "full_attention", - "full_attention", - "full_attention", - "full_attention" - ], - "max_position_embeddings": 1048576, - "max_window_layers": 5, - "model_type": "qwen3", - "num_attention_heads": 64, - "num_hidden_layers": 5, - "num_key_value_heads": 16, - "num_target_layers": 93, - "pad_token_id": 163839, - "rms_norm_eps": 0.00001, - "rope_scaling": null, - "rope_theta": 10000.0, - "sliding_window": null, - "tie_word_embeddings": false, - "use_cache": true, - "use_sliding_window": false, - "vocab_size": 163840 -} diff --git a/configs/kimi-k3-dspark-v1c.json b/configs/kimi-k3-dspark.json similarity index 100% rename from configs/kimi-k3-dspark-v1c.json rename to configs/kimi-k3-dspark.json diff --git a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md b/docs/recipes/kimi-k3-dspark-disaggregated.md similarity index 90% rename from docs/recipes/kimi-k3-dspark-v1c-disaggregated.md rename to docs/recipes/kimi-k3-dspark-disaggregated.md index 8f885a0c1..bb63c6d60 100644 --- a/docs/recipes/kimi-k3-dspark-v1c-disaggregated.md +++ b/docs/recipes/kimi-k3-dspark-disaggregated.md @@ -1,6 +1,6 @@ -# Kimi K3 V1C DSpark disaggregated reproduction +# Kimi K3 DSpark disaggregated reproduction -This recipe migrates the prior four-node colocated Kimi K3 V1C continual run +This recipe migrates the prior four-node colocated Kimi K3 continual run to one TP8 capture node and one four-rank trainer node. It preserves the draft architecture, weights-only warm start, regenerated agentic prompt order, effective global batch, constant learning rate, and DSpark loss weights. @@ -27,11 +27,11 @@ and the Marlin grid.y fallback above 65,535 tokens. ## Artifacts -The checked-in recipe uses the paths already provisioned on the K3 RunPod -pool. Other deployments should override them without editing the recipe: +The checked-in recipe uses paths already provisioned on the deployment. Other +deployments should override them without editing the recipe: - target revision `cdd2e49a2c1cf8d4713b513955e415ed75405a72`; -- the weights-only V1C `epoch_0_step_0` draft checkpoint; +- the weights-only `epoch_0_step_0` draft checkpoint; - the 462-row regenerated dataset whose SHA-256 is `6d50e6bb9ee59095eed91bfba035081efef9fea43bece9ad5dd01c6648a8ef24`. @@ -105,7 +105,7 @@ export WANDB_API_KEY="$(< /protected/path/wandb-api-key)" export WANDB_ENTITY=your-entity unset RANK LOCAL_RANK WORLD_SIZE MASTER_ADDR MASTER_PORT NODE_RANK CUDA_VISIBLE_DEVICES=0,1,2,3 specforge train \ - -c examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml \ + -c examples/configs/kimi-k3-dspark-disaggregated.yaml \ --role both \ "deployment.disaggregated.server_urls=[\"http://$CAPTURE_IP:30000\"]" \ "deployment.disaggregated.mooncake_metadata_server=http://$CAPTURE_IP:35880/metadata" \ @@ -127,10 +127,10 @@ fixture and shrink the optimizer quantum: ```bash specforge train \ - -c examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml \ + -c examples/configs/kimi-k3-dspark-disaggregated.yaml \ --role both \ data.train_data_path= \ - data.prompts_path=/workspace/k3_dspark/k3_specforge/cache/kimi-k3-agentic-regen-9a6ea2c7-v1c-full139264/longest-smoke-pretokenized-4rows-65536.jsonl \ + data.prompts_path=/workspace/k3_dspark/data/longest-smoke-pretokenized-4rows-65536.jsonl \ training.num_epochs=1 \ training.max_steps=1 \ training.accumulation_steps=1 \ diff --git a/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md b/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md deleted file mode 100644 index f2ef630f9..000000000 --- a/docs/recipes/kimi-k3-dspark-mla-kda-disaggregated.md +++ /dev/null @@ -1,206 +0,0 @@ -# Kimi-K3 DSpark 5×MLA and 4×KDA+1×MLA - -These experimental draft backbones train against Kimi-K3 target features with -the disaggregated SpecForge data plane: - -- `KimiK3DSpark5MLADraftModel`: five K3-style MLA layers; -- `KimiK3DSpark4KDA1MLADraftModel`: `KDA → KDA → MLA → KDA → KDA`. - -Both retain the normal DSpark target projector, Markov head, confidence head, -loss, and seven-token proposal block. They use captured target layers -`[11, 23, 47, 71, 83]` (zero based). - -## Architecture contract - -MLA uses the K3 target dimensions: 96 heads, Q LoRA rank 1536, KV LoRA rank -512, 128 non-RoPE QK channels, 64 RoPE channels, 128 value channels, and the -K3 output gate. - -KDA uses 96 heads of width 128, a bias-free four-token causal depthwise -convolution, a full-rank output gate, and the bounded forget gate with lower -bound `-5`. GPU training uses `fla-core==0.5.1`. - -In the hybrid, the central MLA layer is the only layer that reads the captured -target context. Each KDA layer operates on one proposal block at a time and -resets its state between anchors. This preserves DFlash's anchor isolation; -linear recurrence cannot connect two independently sampled proposal blocks. - -## Pinned data - -The recipes use only this Kimi-K3 Open Perfect Blend regeneration: - -| Field | Value | -| --- | --- | -| HF dataset | `skx618/Kimi-K3-OpenPerfectBlend-Regen` | -| Revision | `439c2fdc9fd2ae92e194bde468d26867b36dd660` | -| File | `data.jsonl` | -| Rows | `698316` | -| SHA-256 | `5418f09d1af8ec2e08e8385799f1eeb3c062c669b28407870f7737007bc3eeb9` | - -Store the Hugging Face token outside the repository: - -```bash -install -d -m 700 /workspace/k3_dspark/secrets -install -m 600 /dev/stdin /workspace/k3_dspark/secrets/hf_token -scripts/prepare_kimi_k3_openperfectblend.sh -``` - -The downloader verifies both the row count and SHA-256. SpecForge dynamically -renders the conversation schema and right-truncates to 4096 tokens. - -## Reference training settings - -The exact full-attention recipe preserves the K3 reference run's training -settings and trainer shape: - -- 4096 token examples, per-rank batch size 1, accumulation 32, DP16; -- four-batch feature prefetch to overlap Mooncake TCP transfer with trainer - compute; -- 10 epochs, learning rate `6e-4`, warmup ratio `0.04`; -- 9,173 optimizer steps (`469,695 * 10 // 512`), explicitly pinned so all - roles share one schedule horizon at startup; -- 512 anchors, block size 7, Markov rank 256; -- CE/L1/confidence weights `0.1/0.9/1.0`; -- log every 10 steps and save every 250 steps; -- retain the latest three assembled checkpoints on each trainer node; -- online W&B project `specforge-dspark`. - -The supplied exact topology is two TP8 K3 capture replicas and a separate -two-node, 16-rank trainer. The source job's `BATCH_SIZE=8` was per TP8 target -replica, not per FSDP rank: its two replicas formed a 16-sample optimizer -microbatch. One sample on each of 16 trainer GPUs with accumulation 32 restores -the source global batch of 512 and its per-GPU draft compute. The experimental -MLA/KDA recipes remain portable one-node DP8 configurations with two samples -per rank and the same global batch. Two optimizer windows stay in Mooncake so -target capture for the next update overlaps draft training; record any topology -override in the W&B run config. - -## Smoke then full training - -Start Mooncake as described in -[the K3 V1C runbook](kimi-k3-dspark-v1c-disaggregated.md), then apply the K3 -patch to revision `ee560a2b2df5dafe18fd835d2e546eff019ca5ba`. Launch -`run_kimi_k3_dspark_capture_server.sh` on each of the two capture nodes. The -capture layer ids are part of this recipe's contract and intentionally differ -from the V1C reproduction: - -```bash -export MODEL_PATH=/workspace/models/Kimi-K3-cdd2e49a -export MOONCAKE_MASTER_IP=10.65.0.2 -export SGLANG_ROOT=/workspace/sglang-kimi-k3-ee560a2-spec-capture -export CAPTURE_IP=10.65.0.5 # use the current node's routable address -examples/disagg/run_kimi_k3_dspark_capture_server.sh -``` - -Use capture IP/ports that match the selected YAML. - -For the exact five-layer full-attention reference architecture, use capture -layers `[7, 23, 51, 67, 83]` and the checked-in full-attention recipe. Run the -producer beside trainer rank 0 so both can use the rank-0 local `control_dir`. -The rank-0 inbox HTTP relay serves only tensor-free `SampleRef` metadata to the -second trainer node; feature tensors still move directly through Mooncake. -Override the placeholder host names with private, trusted-network addresses: - -```bash -export AUX_LAYER_IDS="7 23 51 67 83" -specforge train \ - --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ - --role producer \ - deployment.trainer.master_addr=10.65.0.3 \ - deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 - -# Trainer node 0 -WANDB_MODE=online MOONCAKE_LOCAL_HOSTNAME=10.65.0.3 specforge train \ - --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ - --role consumer --node-rank 0 \ - deployment.trainer.master_addr=10.65.0.3 \ - deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 - -# Trainer node 1 -WANDB_MODE=online MOONCAKE_LOCAL_HOSTNAME=10.65.0.4 specforge train \ - --config examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml \ - --role consumer --node-rank 1 \ - deployment.trainer.master_addr=10.65.0.3 \ - deployment.disaggregated.inbox_server_url=http://10.65.0.3:35900 -``` - -When the two trainer nodes do not share `output_dir`, start -`examples/disagg/sync_distributed_checkpoints.py` on both nodes with local rank -ranges `0-7` and `8-15`, respectively. The -[disaggregated examples guide](../../examples/disagg/README.md#checkpoints-without-shared-storage) -contains the complete commands and private-network boundary. - -## Capture throughput contract - -The source run used two independent TP8 target replicas. A normal TP8 prefill -and a capture-enabled TP8 prefill should have comparable model-compute speed, -but one replica still provides only half of the source job's aggregate sample -rate. The capture patch therefore: - -- performs D2H only on the output TP rank and reuses contiguous per-request - views instead of concatenating every single-chunk tensor; -- publishes the scheduler batch through Mooncake `batch_put_from` rather than - one RPC per feature object; -- gives each target endpoint two producer request slots, overlapping one - bounded background publish with the next TP8 prefill; and -- holds the HTTP completion response until every feature key is durable. - -The exact 4,096-token validation captured 128 unique samples (211,715 tokens, -18,214,264,880 feature bytes) in 25.010 seconds after the first request began: -5.118 samples/s per TP8 replica. Two replicas project to 10.236 samples/s, or -71.97 optimizer steps/hour at global batch 512. The source W&B run measured -71.50 steps/hour. Steady target prefill remained approximately 10.9–11.2k -tokens/s, so the remaining scale factor is replica count rather than an SGLang -compute regression. - -A DP16 end-to-end smoke completed with finite losses and gradient norms across -all 16 ranks. Both trainer nodes assembled and opened the portable checkpoint: -one shared training-state file plus rank files 0--15. Before prefetch, its warm -step took 56.44 seconds: 35--39 seconds of trainer compute plus up to 21 seconds -waiting for Mooncake TCP feature materialization. - -With `data.dataloader_num_workers: 4`, four subsequent warm steps took 49.61, -47.70, 48.52, and 49.25 seconds. Their mean was 48.77 seconds, or 73.82 -steps/hour and 10.50 samples/s at global batch 512. This is 3.1% faster than -the source W&B run's 50.35 seconds/step (71.50 steps/hour). The prefetch smoke -is recorded in W&B as run `npia5q21`. Keep prefetch enabled when Mooncake uses -TCP; it overlaps feature transfer with the current optimizer step without -changing capture outputs or the training objective. - -The resulting 9,173-step full run is recorded in W&B as run `fth4aze4`. Steps -20 through 70 reported finite losses and gradient norms. Step time fell from -45.53 to approximately 40.3 seconds as feature prefetch warmed; steps 60 and -70 sustained 89.28 and 88.85 steps/hour (about 12.7 samples/s), roughly 24% -above the source run. These measurements are early-run validation; use the W&B -performance panels and assembled checkpoints to monitor the remaining run. - -Keep the W&B API key in a protected file and export it only in the trainer -shell; do not put it in YAML or a command transcript. - -Run a one-step smoke with the same architecture and objective before removing -the overrides: - -```bash -WANDB_MODE=online specforge train \ - --config examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml \ - --role both \ - training.max_steps=1 \ - training.batch_size=1 \ - training.accumulation_steps=1 \ - data.train_data_path=/workspace/k3_dspark/data/kimi-k3-openperfectblend-smoke.jsonl -``` - -After the smoke produces finite loss, gradients, a checkpoint, and online W&B -telemetry, launch the full run: - -```bash -WANDB_MODE=online specforge train \ - --config examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml \ - --role both -``` - -Use the corresponding -`kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` config for the -hybrid, or the full-attention config above for the exact old architecture. The -full jobs need separate output/control directories and should not share one -trainer allocation concurrently. diff --git a/examples/README.md b/examples/README.md index 68116cf58..434b9061a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,8 +31,7 @@ NPU, offline, and managed/external-service variants, is in | `examples/configs/qwen3-8b-domino-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | Domino | | `examples/configs/qwen3-8b-peagle-disaggregated.yaml` | Disaggregated SGLang server capture | P-EAGLE | | `examples/configs/qwen3-4b-dspark-disaggregated.yaml` | Disaggregated server capture | DSpark | -| `examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml` | Two Kimi-K3 TP8 capture replicas + eight-rank pipelined trainer | DSpark 5×MLA | -| `examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml` | Two Kimi-K3 TP8 capture replicas + eight-rank pipelined trainer | DSpark 4×KDA+1×MLA | +| `examples/configs/kimi-k3-dspark-disaggregated.yaml` | External-service TP8 capture + four-rank trainer | DSpark | | `examples/configs/qwen3-4b-dspark-offline.yaml` | Precomputed features | DSpark | | `examples/configs/qwen3.6-27b-dflash-multiserver-disaggregated.yaml` | Managed local Mooncake + two capture servers | DFlash | | `examples/configs/qwen3.6-27b-dflash-1server-dp2-disaggregated.yaml` | Managed local one capture server + DP2 | DFlash | diff --git a/examples/configs/README.md b/examples/configs/README.md index 9ce7c9815..936efa226 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -50,9 +50,9 @@ two patched SGLang capture servers, and the trainer GPU allocation; the same Disaggregated recipes without `managed_local` keep Mooncake and SGLang external for scheduler- or service-managed deployments. -The `kimi-k3-dspark-v1c-disaggregated.yaml` recipe is the external-service -two-node migration of the 64K Kimi K3 V1C continual run. Its dedicated -[runbook](../../docs/recipes/kimi-k3-dspark-v1c-disaggregated.md) pins the K3 +The `kimi-k3-dspark-disaggregated.yaml` recipe is the external-service +two-node migration of the 64K Kimi K3 continual run. Its dedicated +[runbook](../../docs/recipes/kimi-k3-dspark-disaggregated.md) pins the K3 SGLang revision and patch target, preserves the old effective global batch and prompt order, and documents the TP8 capture plus four-rank trainer topology. diff --git a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml deleted file mode 100644 index a501683fe..000000000 --- a/examples/configs/kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml +++ /dev/null @@ -1,93 +0,0 @@ -model: - target_model_path: /workspace/models/Kimi-K3-cdd2e49a - draft_model_config: configs/kimi-k3-dspark-4kda-1mla.json - target_backend: sglang - trust_remote_code: true - embedding_key: language_model.model.embed_tokens.weight - lm_head_key: language_model.lm_head.weight - mask_token_id: 163824 - torch_dtype: bfloat16 - sglang_attention_backend: flashinfer - sglang_mem_fraction_static: 0.76 - sglang_context_length: 4608 - sglang_max_running_requests: 16 - sglang_max_total_tokens: 73728 - sglang_moe_runner_backend: marlin - # Latest K3 uses its faster fused CustomAllReduceV2 path only when the - # generic symmetric-memory allocator is disabled. - sglang_enable_symm_mem: false - sglang_mamba_radix_cache_strategy: extra_buffer - sglang_max_mamba_cache_size: 80 - -data: - train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl - max_length: 4096 - chat_template: kimi-k3-thinking - cache_dir: /workspace/k3_dspark/cache - build_dataset_num_proc: 64 - dataloader_num_workers: 0 - -training: - strategy: dspark - num_epochs: 10 - # Source TP8x2 capture produced 16 samples per optimizer microbatch. DP8 - # consumes the same shape as 2 samples per rank; 8 * 2 * 32 = 512. - batch_size: 2 - accumulation_steps: 32 - learning_rate: 0.0006 - warmup_ratio: 0.04 - max_grad_norm: 1 - attention_backend: flex_attention - num_anchors: 512 - loss_decay_gamma: 4.0 - objective_chunk_blocks: 128 - dspark_ce_loss_alpha: 0.1 - dspark_l1_loss_alpha: 0.9 - dspark_confidence_head_alpha: 1.0 - save_interval: 250 - log_interval: 10 - dist_timeout: 30 - seed: 42 - prompt_seed: 1 - -tracking: - report_to: wandb - wandb_project: specforge-dspark - wandb_name: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated - wandb_offline: false - wandb_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/wandb - -runtime: - producer_lease: 16 - # Per endpoint: publish batch N while TP8 prefills batch N+1. - producer_concurrency: 2 - # Two optimizer windows remove the capture/train bubble. The low watermark - # lets both batched capture workers resume after one 512-ref durable ack. - in_flight_high_watermark: 1088 - in_flight_low_watermark: 640 - resident_high_watermark_bytes: 377957122048 - resident_low_watermark_bytes: 206158430208 - feature_store_max_resident_bytes: 412316860416 - -run_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated -output_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/output - -deployment: - mode: disaggregated - trainer: - nnodes: 1 - nproc_per_node: 8 - disaggregated: - control_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/control - consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-4kda-1mla-openperfectblend/consumer-state - backend: mooncake - store_id: kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated - server_urls: - - http://capture-node-0:30000 - - http://capture-node-1:30000 - mooncake_metadata_server: http://10.65.0.2:35880/metadata - mooncake_master_server_addr: 10.65.0.2:35551 - mooncake_protocol: tcp - client_buffer_size: 1073741824 - idle_timeout_s: 7200 - peer_wait_timeout_s: 7200 diff --git a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml deleted file mode 100644 index d55ff6457..000000000 --- a/examples/configs/kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml +++ /dev/null @@ -1,100 +0,0 @@ -model: - target_model_path: /workspace/models/Kimi-K3-cdd2e49a - draft_model_config: configs/kimi-k3-dspark-5mla.json - target_backend: sglang - trust_remote_code: true - embedding_key: language_model.model.embed_tokens.weight - lm_head_key: language_model.lm_head.weight - mask_token_id: 163824 - torch_dtype: bfloat16 - sglang_attention_backend: flashinfer - sglang_mem_fraction_static: 0.76 - sglang_context_length: 4608 - sglang_max_running_requests: 16 - sglang_max_total_tokens: 73728 - sglang_moe_runner_backend: marlin - # Latest K3 uses its faster fused CustomAllReduceV2 path only when the - # generic symmetric-memory allocator is disabled. - sglang_enable_symm_mem: false - sglang_mamba_radix_cache_strategy: extra_buffer - sglang_max_mamba_cache_size: 80 - -data: - train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl - max_length: 4096 - chat_template: kimi-k3-thinking - cache_dir: /workspace/k3_dspark/cache - build_dataset_num_proc: 64 - dataloader_num_workers: 0 - -training: - strategy: dspark - num_epochs: 10 - # Source TP8x2 capture produced 16 samples per optimizer microbatch. DP8 - # consumes the same shape as 2 samples per rank; 8 * 2 * 32 = 512. - batch_size: 2 - accumulation_steps: 32 - learning_rate: 0.0006 - warmup_ratio: 0.04 - max_grad_norm: 1 - attention_backend: flex_attention - num_anchors: 512 - loss_decay_gamma: 4.0 - objective_chunk_blocks: 128 - dspark_ce_loss_alpha: 0.1 - dspark_l1_loss_alpha: 0.9 - dspark_confidence_head_alpha: 1.0 - save_interval: 250 - log_interval: 10 - dist_timeout: 30 - seed: 42 - prompt_seed: 1 - -tracking: - report_to: wandb - wandb_project: specforge-dspark - wandb_name: kimi-k3-dspark-5mla-openperfectblend-disaggregated - wandb_offline: false - wandb_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/wandb - -runtime: - # Amortize K3's fixed auxiliary-capture and HTTP overhead across 16 prompts. - # The 40K prefill cap still splits unusually token-heavy batches safely. - producer_lease: 16 - # Per endpoint: publish batch N while TP8 prefills batch N+1. - producer_concurrency: 2 - # Keep two complete global-batch windows resident. A single-window buffer - # makes capture and training alternate because source refs are acknowledged - # only after the optimizer step is durable. The 64-ref margin covers both - # TP8 capture workers' outstanding leases; low=640 resumes capture after one - # 512-ref optimizer acknowledgement while another window stays available. - in_flight_high_watermark: 1088 - in_flight_low_watermark: 640 - # Two worst-case 4K windows are about 336 GiB. Pause at 352 GiB, resume - # after one window at 192 GiB, and leave 32 GiB for leased-request overshoot. - resident_high_watermark_bytes: 377957122048 - resident_low_watermark_bytes: 206158430208 - feature_store_max_resident_bytes: 412316860416 - -run_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated -output_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/output - -deployment: - mode: disaggregated - trainer: - nnodes: 1 - nproc_per_node: 8 - disaggregated: - control_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/control - consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-5mla-openperfectblend/consumer-state - backend: mooncake - store_id: kimi-k3-dspark-5mla-openperfectblend-disaggregated - server_urls: - - http://capture-node-0:30000 - - http://capture-node-1:30000 - mooncake_metadata_server: http://10.65.0.2:35880/metadata - mooncake_master_server_addr: 10.65.0.2:35551 - mooncake_protocol: tcp - client_buffer_size: 1073741824 - idle_timeout_s: 7200 - peer_wait_timeout_s: 7200 diff --git a/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml b/examples/configs/kimi-k3-dspark-disaggregated.yaml similarity index 75% rename from examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml rename to examples/configs/kimi-k3-dspark-disaggregated.yaml index 18527b084..23b57fab4 100644 --- a/examples/configs/kimi-k3-dspark-v1c-disaggregated.yaml +++ b/examples/configs/kimi-k3-dspark-disaggregated.yaml @@ -1,9 +1,9 @@ model: target_model_path: /workspace/models/Kimi-K3-cdd2e49a - draft_model_config: configs/kimi-k3-dspark-v1c.json + draft_model_config: configs/kimi-k3-dspark.json # Weights-only initialization: optimizer, scheduler, counters, and RNG start # fresh for the regenerated agentic data. - draft_checkpoint_path: /workspace/k3_dspark/k3_specforge-replay/outputs/kimi-k3-dspark-v1c-cdd2e49a-agentic-65536-a512-b7-constantlr-20260729/epoch_0_step_0 + draft_checkpoint_path: /workspace/k3_dspark/checkpoints/kimi-k3-dspark/epoch_0_step_0 target_backend: sglang trust_remote_code: true embedding_key: language_model.model.embed_tokens.weight @@ -20,7 +20,7 @@ model: sglang_max_mamba_cache_size: 5 data: - train_data_path: /workspace/k3_dspark/k3_specforge/cache/kimi-k3-agentic-regen-9a6ea2c7-v1c-full139264/kimi-k3-agentic-regen-v1c-full139264.jsonl + train_data_path: /workspace/k3_dspark/data/kimi-k3-agentic-regen.jsonl max_length: 65536 chat_template: kimi-k3-thinking cache_dir: /workspace/k3_dspark/cache @@ -54,8 +54,8 @@ training: tracking: report_to: wandb wandb_project: specforge-dspark - wandb_name: kimi-k3-dspark-v1c-specforge-disaggregated - wandb_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/wandb + wandb_name: kimi-k3-dspark-specforge-disaggregated + wandb_dir: /workspace/k3_dspark/runs/kimi-k3-specforge/wandb runtime: producer_lease: 1 @@ -71,8 +71,8 @@ runtime: resident_low_watermark_bytes: 697932185600 feature_store_max_resident_bytes: 966367641600 -run_id: kimi-k3-dspark-v1c-specforge-disaggregated -output_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/output +run_id: kimi-k3-dspark-specforge-disaggregated +output_dir: /workspace/k3_dspark/runs/kimi-k3-specforge/output deployment: mode: disaggregated @@ -80,10 +80,10 @@ deployment: nnodes: 1 nproc_per_node: 4 disaggregated: - control_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/control - consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-v1c-specforge/consumer-state + control_dir: /workspace/k3_dspark/runs/kimi-k3-specforge/control + consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-specforge/consumer-state backend: mooncake - store_id: kimi-k3-dspark-v1c-specforge-disaggregated + store_id: kimi-k3-dspark-specforge-disaggregated server_urls: - http://capture-node:30000 mooncake_metadata_server: http://capture-node:35880/metadata diff --git a/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml b/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml deleted file mode 100644 index dc952b221..000000000 --- a/examples/configs/kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml +++ /dev/null @@ -1,107 +0,0 @@ -model: - # Exact target revision used by the reference run njyq006k. - target_model_path: /workspace/models/Kimi-K3 - draft_model_config: configs/kimi-k3-dspark-fullattn-gqa16.json - target_backend: sglang - trust_remote_code: true - embedding_key: language_model.model.embed_tokens.weight - lm_head_key: language_model.lm_head.weight - mask_token_id: 163824 - torch_dtype: bfloat16 - sglang_attention_backend: flashinfer - sglang_mem_fraction_static: 0.76 - sglang_context_length: 4608 - sglang_max_running_requests: 16 - sglang_max_total_tokens: 73728 - sglang_moe_runner_backend: marlin - sglang_enable_symm_mem: false - sglang_mamba_radix_cache_strategy: extra_buffer - sglang_max_mamba_cache_size: 80 - -data: - # Prepared from skx618/Kimi-K3-OpenPerfectBlend-Regen@9caaf705... - # after the reference structural gate: 499,591 source rows -> 469,695 - # trainable rows after the dataset builder's 14-token supervision gate. - train_data_path: /workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-9caaf705/prepared-4096.jsonl - max_length: 4096 - chat_template: kimi-k3-thinking - cache_dir: /workspace/k3_dspark/cache - build_dataset_num_proc: 64 - # Materialize the next optimizer window while the current one trains. This - # hides Mooncake TCP transfer latency on the two-node DP16 topology. - dataloader_num_workers: 4 - -training: - strategy: dspark - num_epochs: 10 - # floor(469,695 prompts * 10 epochs / global batch 512) - total_steps: 9173 - # Preserve the source FSDP16 trainer shape: one sample on each of 16 ranks, - # accumulated 32 times. Keeping a DP8 consumer would preserve the global - # batch mathematically but halve trainer compute and miss the source rate. - batch_size: 1 - accumulation_steps: 32 - learning_rate: 0.0006 - lr_scheduler: cosine - warmup_ratio: 0.04 - max_grad_norm: 1 - attention_backend: flex_attention - num_anchors: 512 - loss_decay_gamma: 4.0 - objective_chunk_blocks: 128 - dspark_ce_loss_alpha: 0.1 - dspark_l1_loss_alpha: 0.9 - dspark_confidence_head_alpha: 1.0 - save_interval: 250 - # Keep the latest three portable DP16 checkpoints; relay archives remain as - # an additional recovery source without duplicating every checkpoint tree. - max_checkpoints: 3 - log_interval: 10 - dist_timeout: 30 - seed: 42 - prompt_seed: 1 - -tracking: - report_to: wandb - wandb_project: specforge-dspark - wandb_name: kimi-k3-dspark-5layer-fullattn-reference-disaggregated - wandb_offline: false - wandb_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/wandb - -runtime: - producer_lease: 16 - # Per endpoint: publish batch N while TP8 prefills batch N+1. - producer_concurrency: 2 - in_flight_high_watermark: 1088 - in_flight_low_watermark: 640 - resident_high_watermark_bytes: 377957122048 - resident_low_watermark_bytes: 206158430208 - feature_store_max_resident_bytes: 412316860416 - -run_id: kimi-k3-dspark-5layer-fullattn-reference-disaggregated -output_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/output - -deployment: - mode: disaggregated - trainer: - nnodes: 2 - nproc_per_node: 8 - master_addr: trainer-node-0 - master_port: 29500 - disaggregated: - control_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/control - consumer_state_dir: /workspace/k3_dspark/runs/kimi-k3-fullattn-reference/consumer-state - # Rank 0 relays tensor-free inbox metadata to the second trainer node. The - # target tensors continue to move directly through Mooncake. - inbox_server_url: http://trainer-node-0:35900 - backend: mooncake - store_id: kimi-k3-dspark-5layer-fullattn-reference-disaggregated - server_urls: - - http://capture-node-0:30000 - - http://capture-node-1:30000 - mooncake_metadata_server: http://10.65.0.2:35880/metadata - mooncake_master_server_addr: 10.65.0.2:35551 - mooncake_protocol: tcp - client_buffer_size: 1073741824 - idle_timeout_s: 7200 - peer_wait_timeout_s: 7200 diff --git a/examples/disagg/README.md b/examples/disagg/README.md index eb3e62cf0..0b35e7f39 100644 --- a/examples/disagg/README.md +++ b/examples/disagg/README.md @@ -213,7 +213,4 @@ URL userinfo are redacted. See the [disaggregated training guide](../../docs/basic_usage/disaggregated_training.md) for service prerequisites, recovery rules, and the online/offline data-plane -contracts. Kimi-K3's two-replica production recipes use -`run_kimi_k3_dspark_capture_server.sh` on each TP8 capture node; set the -node-local model path and routable capture/Mooncake addresses through the -script's required environment variables. +contracts. diff --git a/examples/disagg/run_kimi_k3_dspark_capture_server.sh b/examples/disagg/run_kimi_k3_dspark_capture_server.sh deleted file mode 100755 index 04eb5a946..000000000 --- a/examples/disagg/run_kimi_k3_dspark_capture_server.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# Launch one Kimi-K3 TP8 DSpark capture replica for the pipelined recipes. - -set -euo pipefail - -MODEL_PATH=${MODEL_PATH:?set MODEL_PATH to the node-local Kimi-K3 snapshot} -CAPTURE_IP=${CAPTURE_IP:?set CAPTURE_IP to this node routable address} -MOONCAKE_MASTER_IP=${MOONCAKE_MASTER_IP:?set MOONCAKE_MASTER_IP} -SGLANG_ROOT=${SGLANG_ROOT:?set SGLANG_ROOT to the patched SGLang checkout} -SERVER_PORT=${SERVER_PORT:-30000} -AUX_LAYER_IDS=${AUX_LAYER_IDS:-"11 23 47 71 83"} -MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16} -MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728} -MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960} -MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80} - -[[ -f "$MODEL_PATH/config.json" ]] || { - printf 'missing target config: %s/config.json\n' "$MODEL_PATH" >&2 - exit 1 -} -[[ -f "$SGLANG_ROOT/python/sglang/srt/spec_capture_sink.py" ]] || { - printf 'SGLang checkout is not patched for spec capture: %s\n' "$SGLANG_ROOT" >&2 - exit 1 -} - -export PYTHONPATH="$SGLANG_ROOT/python${PYTHONPATH:+:$PYTHONPATH}" -export MOONCAKE_MASTER_SERVER_ADDR="$MOONCAKE_MASTER_IP:35551" -export MOONCAKE_METADATA_SERVER="http://$MOONCAKE_MASTER_IP:35880/metadata" -export MOONCAKE_LOCAL_HOSTNAME="$CAPTURE_IP" -export MC_TCP_BIND_ADDRESS="$CAPTURE_IP" -export MC_TRANSFER_TIMEOUT=${MC_TRANSFER_TIMEOUT:-300} -export MOONCAKE_PROTOCOL=${MOONCAKE_PROTOCOL:-tcp} -export MOONCAKE_GLOBAL_SEGMENT_SIZE=${MOONCAKE_GLOBAL_SEGMENT_SIZE:-1099511627776} -export MOONCAKE_LOCAL_BUFFER_SIZE=${MOONCAKE_LOCAL_BUFFER_SIZE:-1073741824} -export CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7} -# Retain at most two host-side capture batches: Mooncake publishes batch N on -# its background writer while TP8 computes prefill N+1. Larger queues retain -# tens of GiB per batch without increasing a single writer's throughput. -export SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES=${SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2} - -# AUX_LAYER_IDS is a trusted operator-provided whitespace-separated integer -# list and intentionally expands to separate CLI values. Capture workers spend -# almost all of their time in full-prompt prefill, so skip the long K3 decode -# CUDA-graph compile that cannot accelerate the one token completing /generate. -# Leave SGLang symmetric memory off: the latest K3 branch can then auto-enable -# its fused CustomAllReduceV2 path; symmetric memory disables that faster path. -# shellcheck disable=SC2086 -exec python3 -m sglang.launch_server \ - --host 0.0.0.0 \ - --port "$SERVER_PORT" \ - --model-path "$MODEL_PATH" \ - --trust-remote-code \ - --skip-tokenizer-init \ - --tp-size 8 \ - --mem-fraction-static 0.76 \ - --context-length 4608 \ - --max-running-requests "$MAX_RUNNING_REQUESTS" \ - --max-total-tokens "$MAX_TOTAL_TOKENS" \ - --max-prefill-tokens "$MAX_PREFILL_TOKENS" \ - --prefill-attention-backend flashinfer \ - --decode-attention-backend trtllm_mla \ - --moe-runner-backend marlin \ - --mamba-radix-cache-strategy extra_buffer \ - --max-mamba-cache-size "$MAX_MAMBA_CACHE_SIZE" \ - --disable-cuda-graph \ - --chunked-prefill-size -1 \ - --enable-spec-capture \ - --spec-capture-method dspark \ - --spec-capture-aux-layer-ids $AUX_LAYER_IDS diff --git a/pyproject.toml b/pyproject.toml index 278cde7e8..9d8097322 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ dev = [ ] fa = ["flash-attn", "ninja", "packaging"] liger = ["liger-kernel"] -kda = ["fla-core==0.5.1"] [tool.setuptools.dynamic] version = {file = "version.txt"} diff --git a/scripts/prepare_kimi_k3_openperfectblend.sh b/scripts/prepare_kimi_k3_openperfectblend.sh deleted file mode 100755 index 2079cfd6e..000000000 --- a/scripts/prepare_kimi_k3_openperfectblend.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Download the exact Kimi-K3 Open Perfect Blend regeneration used by the K3 -# draft recipes. The token is read from a protected file and is never printed. - -REPO_ID=${REPO_ID:-skx618/Kimi-K3-OpenPerfectBlend-Regen} -REVISION=${REVISION:-439c2fdc9fd2ae92e194bde468d26867b36dd660} -FILENAME=${FILENAME:-data.jsonl} -EXPECTED_SHA256=${EXPECTED_SHA256:-5418f09d1af8ec2e08e8385799f1eeb3c062c669b28407870f7737007bc3eeb9} -EXPECTED_ROWS=${EXPECTED_ROWS:-698316} -OUTPUT=${OUTPUT:-/workspace/k3_dspark/data/kimi-k3-openperfectblend-regen-439c2fdc/data.jsonl} -SMOKE_OUTPUT=${SMOKE_OUTPUT:-/workspace/k3_dspark/data/kimi-k3-openperfectblend-smoke.jsonl} -HF_TOKEN_FILE=${HF_TOKEN_FILE:-/workspace/k3_dspark/secrets/hf_token} - -if [[ -z "${HF_TOKEN:-}" && -r "$HF_TOKEN_FILE" ]]; then - IFS= read -r HF_TOKEN < "$HF_TOKEN_FILE" || [[ -n "$HF_TOKEN" ]] -fi -if [[ -z "${HF_TOKEN:-}" ]]; then - printf 'ERROR: set HF_TOKEN or create mode-600 %s\n' "$HF_TOKEN_FILE" >&2 - exit 1 -fi - -mkdir -p "$(dirname "$OUTPUT")" -downloaded=$( - HF_TOKEN="$HF_TOKEN" python3 - "$REPO_ID" "$REVISION" "$FILENAME" <<'PY' -import os -import sys - -from huggingface_hub import hf_hub_download - -print( - hf_hub_download( - repo_id=sys.argv[1], - repo_type="dataset", - revision=sys.argv[2], - filename=sys.argv[3], - token=os.environ["HF_TOKEN"], - ) -) -PY -) -install -m 0644 "$downloaded" "$OUTPUT" - -actual_sha256=$(sha256sum "$OUTPUT" | awk '{print $1}') -actual_rows=$(wc -l < "$OUTPUT" | tr -d ' ') -if [[ "$actual_sha256" != "$EXPECTED_SHA256" ]]; then - printf 'ERROR: dataset SHA-256 mismatch: %s\n' "$actual_sha256" >&2 - exit 1 -fi -if [[ "$actual_rows" != "$EXPECTED_ROWS" ]]; then - printf 'ERROR: dataset row count mismatch: %s\n' "$actual_rows" >&2 - exit 1 -fi -sed -n '1,4p' "$OUTPUT" > "$SMOKE_OUTPUT" -[[ "$(wc -l < "$SMOKE_OUTPUT" | tr -d ' ')" == 4 ]] || { - printf 'ERROR: failed to create four-row smoke fixture\n' >&2 - exit 1 -} -printf 'validated Kimi-K3 Open Perfect Blend: rows=%s sha256=%s path=%s\n' \ - "$actual_rows" "$actual_sha256" "$OUTPUT" -printf 'created four-row smoke fixture: %s\n' "$SMOKE_OUTPUT" diff --git a/specforge/algorithms/dspark/providers.py b/specforge/algorithms/dspark/providers.py index a1eee9d1b..d3e37be3f 100644 --- a/specforge/algorithms/dspark/providers.py +++ b/specforge/algorithms/dspark/providers.py @@ -37,13 +37,7 @@ ALGORITHM_NAME = "dspark" DRAFT_ARCHITECTURE = "DSparkDraftModel" -COMPATIBLE_DRAFT_ARCHITECTURES = frozenset( - { - DRAFT_ARCHITECTURE, - "KimiK3DSpark5MLADraftModel", - "KimiK3DSpark4KDA1MLADraftModel", - } -) +COMPATIBLE_DRAFT_ARCHITECTURES = frozenset({DRAFT_ARCHITECTURE}) def build_step(wrapped_model, *, target_head=None, **_options): diff --git a/specforge/modeling/draft/__init__.py b/specforge/modeling/draft/__init__.py index 34cec6c4a..839869dd6 100644 --- a/specforge/modeling/draft/__init__.py +++ b/specforge/modeling/draft/__init__.py @@ -7,7 +7,6 @@ ) from .domino import DominoDraftModel from .dspark import DSparkDraftModel -from .kimi_k3_dspark import KimiK3DSpark4KDA1MLADraftModel, KimiK3DSpark5MLADraftModel from .llama3_eagle import LlamaForCausalLMEagle3 from .peagle import PEagleDraftModel from .registry import DRAFT_REGISTRY, available_drafts, register_draft, resolve_draft @@ -17,8 +16,6 @@ "DFlashDraftModel", "DominoDraftModel", "DSparkDraftModel", - "KimiK3DSpark4KDA1MLADraftModel", - "KimiK3DSpark5MLADraftModel", "LlamaForCausalLMEagle3", "PEagleDraftModel", "build_target_layer_ids", diff --git a/specforge/modeling/draft/kimi_k3_dspark.py b/specforge/modeling/draft/kimi_k3_dspark.py deleted file mode 100644 index 699cccfb5..000000000 --- a/specforge/modeling/draft/kimi_k3_dspark.py +++ /dev/null @@ -1,700 +0,0 @@ -"""Kimi-K3 MLA and KDA backbones for DSpark draft training. - -The target model alternates Kimi Delta Attention (KDA) with Multi-Latent -Attention (MLA). These draft variants keep DSpark's projector, Markov head, -confidence head, and DFlash objective while replacing only the five decoder -layers: - -* ``KimiK3DSpark5MLADraftModel`` uses five MLA layers. -* ``KimiK3DSpark4KDA1MLADraftModel`` uses KDA, KDA, MLA, KDA, KDA. - -The hybrid intentionally uses MLA as its only target-context injection point. -Each KDA layer resets at every proposal block, so anchors never share recurrent -state and the DFlash training mask cannot be bypassed by a linear-attention -scan. -""" - -from __future__ import annotations - -from typing import Callable, Optional - -import torch -import torch.nn.functional as F -from torch import nn -from torch.nn.attention.flex_attention import BlockMask, flex_attention -from transformers.cache_utils import Cache -from transformers.models.qwen3.modeling_qwen3 import ( - FlashAttentionKwargs, - GradientCheckpointingLayer, - Qwen3MLP, - Qwen3PreTrainedModel, - Qwen3RMSNorm, - Qwen3RotaryEmbedding, -) -from typing_extensions import Tuple, Unpack - -from .dflash import build_target_layer_ids, normalize_draft_head_checkpoint_keys -from .dspark import DSparkDraftModel -from .flex_attention import compile_friendly_flex_attention -from .registry import register_draft - -# CUDA limits gridDim.z to 65,535. FLA's KDA gate kernel maps one program to -# every (independent proposal block, head) pair on that dimension, so a full -# DSpark optimizer microbatch can exceed the launch limit even though each -# proposal block is only a few tokens long. -_CUDA_MAX_GRID_DIM_Z = 65_535 - - -def _rotate_half(x: torch.Tensor, *, interleaved: bool) -> torch.Tensor: - if interleaved: - paired = x.float().reshape(*x.shape[:-1], -1, 2) - first, second = paired.unbind(dim=-1) - return torch.stack((-second, first), dim=-1).flatten(-2).to(x.dtype) - first, second = x.chunk(2, dim=-1) - return torch.cat((-second, first), dim=-1) - - -def _apply_rope( - x: torch.Tensor, - positions: torch.Tensor, - inv_freq: torch.Tensor, - *, - interleaved: bool, -) -> torch.Tensor: - freqs = torch.einsum("bs,d->bsd", positions.float(), inv_freq.float()) - angles = ( - torch.repeat_interleave(freqs, 2, dim=-1) - if interleaved - else torch.cat((freqs, freqs), dim=-1) - ) - cos = angles.cos().to(dtype=x.dtype).unsqueeze(1) - sin = angles.sin().to(dtype=x.dtype).unsqueeze(1) - return x * cos + _rotate_half(x, interleaved=interleaved) * sin - - -class KimiK3DraftMLAAttention(nn.Module): - """K3 MLA in compressed-latent (absorbed) form.""" - - def __init__(self, config, layer_idx: int): - super().__init__() - del layer_idx - self.hidden_size = int(config.hidden_size) - self.num_heads = int(config.num_attention_heads) - self.q_lora_rank = int(config.q_lora_rank) - self.kv_lora_rank = int(config.kv_lora_rank) - self.qk_nope_head_dim = int(config.qk_nope_head_dim) - self.qk_rope_head_dim = int(config.qk_rope_head_dim) - self.v_head_dim = int(config.v_head_dim) - self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim - self.scaling = self.qk_head_dim**-0.5 - self.rope_interleave = bool(getattr(config, "rope_interleave", True)) - self.use_output_gate = bool(getattr(config, "mla_use_output_gate", False)) - - if self.qk_rope_head_dim % 2: - raise ValueError("qk_rope_head_dim must be even") - - bias = bool(getattr(config, "attention_bias", False)) - eps = float(config.rms_norm_eps) - self.q_a_proj = nn.Linear(self.hidden_size, self.q_lora_rank, bias=bias) - self.q_a_layernorm = Qwen3RMSNorm(self.q_lora_rank, eps=eps) - self.q_b_proj = nn.Linear( - self.q_lora_rank, - self.num_heads * self.qk_head_dim, - bias=bias, - ) - self.kv_a_proj_with_mqa = nn.Linear( - self.hidden_size, - self.kv_lora_rank + self.qk_rope_head_dim, - bias=bias, - ) - self.kv_a_layernorm = Qwen3RMSNorm(self.kv_lora_rank, eps=eps) - self.kv_b_proj = nn.Linear( - self.kv_lora_rank, - self.num_heads * (self.qk_nope_head_dim + self.v_head_dim), - bias=bias, - ) - if self.use_output_gate: - self.g_proj = nn.Linear( - self.hidden_size, - self.num_heads * self.v_head_dim, - bias=False, - ) - self.o_proj = nn.Linear( - self.num_heads * self.v_head_dim, - self.hidden_size, - bias=bias, - ) - - rope_parameters = getattr(config, "rope_parameters", None) or {} - rope_theta = float( - rope_parameters.get( - "rope_theta", - getattr(config, "rope_theta", 10000.0), - ) - ) - inv_freq = 1.0 / ( - rope_theta - ** ( - torch.arange(0, self.qk_rope_head_dim, 2, dtype=torch.float32) - / self.qk_rope_head_dim - ) - ) - self.register_buffer("inv_freq", inv_freq, persistent=False) - - def _project_kv(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: - latent = self.kv_a_proj_with_mqa(x) - kv_latent, k_rope = latent.split( - [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 - ) - return self.kv_a_layernorm(kv_latent), k_rope - - @torch.compiler.disable - def forward( - self, - hidden_states: torch.Tensor, - target_hidden: torch.Tensor, - position_ids: torch.LongTensor, - attention_mask: Optional[torch.Tensor], - past_key_values: Optional[Cache] = None, - **kwargs: Unpack[FlashAttentionKwargs], - ) -> tuple[torch.Tensor, Optional[torch.Tensor]]: - del kwargs - if past_key_values is not None: - raise NotImplementedError( - "Kimi-K3 draft MLA training does not use the HF cache path" - ) - - batch, query_len = hidden_states.shape[:2] - context_len = target_hidden.shape[1] - q_lora = self.q_a_layernorm(self.q_a_proj(hidden_states)) - q = self.q_b_proj(q_lora).view( - batch, query_len, self.num_heads, self.qk_head_dim - ) - q_nope, q_rope = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1) - - context_latent, context_rope = self._project_kv(target_hidden) - noise_latent, noise_rope = self._project_kv(hidden_states) - kv_latent = torch.cat((context_latent, noise_latent), dim=1) - k_rope = torch.cat((context_rope, noise_rope), dim=1) - - q_positions = position_ids[:, -query_len:] - q_rope = _apply_rope( - q_rope.transpose(1, 2), - q_positions, - self.inv_freq, - interleaved=self.rope_interleave, - ).transpose(1, 2) - k_rope = _apply_rope( - k_rope.unsqueeze(1), - position_ids[:, : context_len + query_len], - self.inv_freq, - interleaved=self.rope_interleave, - ) - - kv_b = self.kv_b_proj.weight.view( - self.num_heads, - self.qk_nope_head_dim + self.v_head_dim, - self.kv_lora_rank, - ) - w_kc, w_vc = kv_b.split([self.qk_nope_head_dim, self.v_head_dim], dim=1) - if isinstance(attention_mask, BlockMask) and query_len > 128: - # The absorbed MLA form has q/k head dim kv_lora_rank + rope_dim - # (576 for K3), which exceeds Triton's shared-memory budget for the - # fused FlexAttention kernel. Expand the two linear projections - # around attention instead. This is algebraically equivalent: - # (q W_k) @ c == q @ (W_k c) - # softmax(scores) c W_v == softmax(scores) (c W_v) - # and reduces the fused q/k head dim to K3's native 192 while - # retaining a 128-dim value. It allocates linear K/V projections, - # but never the quadratic score matrix used by eager FlexAttention. - q_attn = torch.cat((q_nope, q_rope), dim=-1).transpose(1, 2) - k_nope = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc).transpose(1, 2) - k_attn = torch.cat( - (k_nope, k_rope.expand(-1, self.num_heads, -1, -1)), - dim=-1, - ) - v_attn = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc).transpose(1, 2) - attn_out = compile_friendly_flex_attention( - q_attn, - k_attn, - v_attn, - block_mask=attention_mask, - scale=self.scaling, - ).transpose(1, 2) - else: - q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) - q_attn = torch.cat((q_absorbed, q_rope), dim=-1).transpose(1, 2) - k_attn = torch.cat((kv_latent.unsqueeze(1), k_rope), dim=-1) - v_attn = kv_latent.unsqueeze(1) - if isinstance(attention_mask, BlockMask): - latent_out = flex_attention( - q_attn, - k_attn, - v_attn, - block_mask=attention_mask, - scale=self.scaling, - enable_gqa=True, - ) - else: - latent_out = F.scaled_dot_product_attention( - q_attn, - k_attn, - v_attn, - attn_mask=attention_mask, - dropout_p=0.0, - scale=self.scaling, - enable_gqa=True, - ) - attn_out = torch.einsum("bhqk,hvk->bqhv", latent_out, w_vc) - - attn_out = attn_out.reshape(batch, query_len, -1) - if self.use_output_gate: - attn_out = attn_out * torch.sigmoid(self.g_proj(hidden_states)) - return self.o_proj(attn_out), None - - -class KimiK3DraftMLADecoderLayer(GradientCheckpointingLayer): - def __init__(self, config, layer_idx: int): - super().__init__() - hidden_size = int(config.hidden_size) - eps = float(config.rms_norm_eps) - self.self_attn = KimiK3DraftMLAAttention(config, layer_idx) - self.mlp = Qwen3MLP(config) - self.input_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) - self.post_attention_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) - - def forward( - self, - target_hidden: Optional[torch.Tensor] = None, - hidden_states: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_value: Optional[Cache] = None, - **kwargs: Unpack[FlashAttentionKwargs], - ) -> Tuple[torch.FloatTensor]: - residual = hidden_states - hidden_states = self.input_layernorm(hidden_states) - hidden_states = self.self_attn( - hidden_states=hidden_states, - target_hidden=target_hidden, - position_ids=position_ids, - attention_mask=attention_mask, - past_key_values=past_key_value, - **kwargs, - )[0] - hidden_states = residual + hidden_states - residual = hidden_states - hidden_states = self.post_attention_layernorm(hidden_states) - return (residual + self.mlp(hidden_states),) - - -class KimiK3ShortConvolution(nn.Module): - """Causal depthwise convolution with target-compatible parameter layout.""" - - def __init__(self, channels: int, kernel_size: int): - super().__init__() - self.kernel_size = int(kernel_size) - self.weight = nn.Parameter(torch.empty(channels, self.kernel_size)) - nn.init.normal_(self.weight, mean=0.0, std=0.02) - - def forward(self, x: torch.Tensor) -> torch.Tensor: - x = x.transpose(1, 2) - x = F.pad(x, (self.kernel_size - 1, 0)) - x = F.conv1d( - x, - self.weight.unsqueeze(1), - bias=None, - groups=self.weight.shape[0], - ) - return F.silu(x.transpose(1, 2)) - - -class KimiK3GatedRMSNorm(nn.Module): - def __init__(self, hidden_size: int, eps: float): - super().__init__() - self.weight = nn.Parameter(torch.ones(hidden_size)) - self.eps = float(eps) - - def forward(self, x: torch.Tensor, gate: torch.Tensor) -> torch.Tensor: - variance = x.float().pow(2).mean(dim=-1, keepdim=True) - normalized = x * torch.rsqrt(variance + self.eps).to(x.dtype) - return normalized * self.weight.to(x.dtype) * torch.sigmoid(gate) - - -def _reference_kda( - q: torch.Tensor, - k: torch.Tensor, - v: torch.Tensor, - raw_gate: torch.Tensor, - beta: torch.Tensor, - A_log: torch.Tensor, - dt_bias: torch.Tensor, - lower_bound: Optional[float], -) -> torch.Tensor: - """Small differentiable recurrence used by CPU tests, not production.""" - - q = F.normalize(q.float(), dim=-1).to(q.dtype) - k = F.normalize(k.float(), dim=-1).to(k.dtype) - beta = torch.sigmoid(beta.float()).to(q.dtype) - gate_input = raw_gate.float() + dt_bias.view(1, 1, *raw_gate.shape[-2:]) - scale = A_log.float().exp().view(1, 1, -1, 1) - if lower_bound is None: - log_decay = -scale * F.softplus(gate_input) - else: - log_decay = float(lower_bound) * torch.sigmoid(scale * gate_input) - log_decay = log_decay.to(q.dtype) - - state = q.new_zeros( - q.shape[0], q.shape[2], q.shape[3], v.shape[3], dtype=torch.float32 - ) - outputs = [] - score_scale = q.shape[-1] ** -0.5 - for step in range(q.shape[1]): - decay = log_decay[:, step].float().exp().unsqueeze(-1) - state = state * decay - key = k[:, step].float() - value = v[:, step].float() - prediction = torch.einsum("bhd,bhdv->bhv", key, state) - delta = (value - prediction) * beta[:, step].float().unsqueeze(-1) - state = state + torch.einsum("bhd,bhv->bhdv", key, delta) - outputs.append( - torch.einsum("bhd,bhdv->bhv", q[:, step].float(), state) - .mul(score_scale) - .to(q.dtype) - ) - return torch.stack(outputs, dim=1) - - -def _load_fla_chunk_kda() -> Callable[..., tuple[torch.Tensor, object]]: - try: - from fla.ops.kda import chunk_kda - except ImportError as exc: - raise ImportError( - "Kimi-K3 KDA training requires fla-core==0.5.1; install " - "SpecForge with the 'kda' extra" - ) from exc - return chunk_kda - - -def _fla_kda( - q: torch.Tensor, - k: torch.Tensor, - v: torch.Tensor, - raw_gate: torch.Tensor, - beta: torch.Tensor, - A_log: torch.Tensor, - dt_bias: torch.Tensor, - lower_bound: Optional[float], -) -> torch.Tensor: - chunk_kda = _load_fla_chunk_kda() - - # FLA's kda_gate_chunk_cumsum launch uses grid_z = batch * num_heads. - # DSpark flattens anchors into the batch because recurrent KDA state must - # reset for every proposal block. At the production shape this is - # 8 * 512 * 96 = 393,216, beyond CUDA's grid-z limit. Splitting only the - # independent block dimension is algebraically exact; concatenation also - # lets autograd sum the shared A_log and dt_bias gradients across slices. - num_heads = int(q.shape[2]) - max_blocks_per_launch = max(1, _CUDA_MAX_GRID_DIM_Z // num_heads) - outputs = [] - for start in range(0, int(q.shape[0]), max_blocks_per_launch): - end = start + max_blocks_per_launch - output, _ = chunk_kda( - q=q[start:end], - k=k[start:end], - v=v[start:end], - g=raw_gate[start:end], - beta=beta[start:end], - A_log=A_log, - dt_bias=dt_bias, - output_final_state=False, - use_qk_l2norm_in_kernel=True, - use_gate_in_kernel=True, - use_beta_sigmoid_in_kernel=True, - safe_gate=lower_bound is not None, - lower_bound=lower_bound, - ) - outputs.append(output) - return outputs[0] if len(outputs) == 1 else torch.cat(outputs, dim=0) - - -class KimiK3DraftKDAAttention(nn.Module): - """K3 KDA applied independently to every DSpark proposal block.""" - - def __init__(self, config, layer_idx: int): - super().__init__() - del layer_idx - linear_config = dict(getattr(config, "linear_attn_config", None) or {}) - self.hidden_size = int(config.hidden_size) - self.head_dim = int(linear_config["head_dim"]) - self.num_heads = int(linear_config["num_heads"]) - self.block_size = int(config.block_size) - self.conv_size = int(linear_config["short_conv_kernel_size"]) - self.use_full_rank_gate = bool(linear_config.get("use_full_rank_gate", False)) - self.lower_bound = linear_config.get("gate_lower_bound") - self.backend = str(linear_config.get("backend", "fla")).lower() - if self.backend not in {"fla", "reference"}: - raise ValueError( - "linear_attn_config.backend must be 'fla' or 'reference', " - f"got {self.backend!r}" - ) - - projection_size = self.num_heads * self.head_dim - self.q_proj = nn.Linear(self.hidden_size, projection_size, bias=False) - self.k_proj = nn.Linear(self.hidden_size, projection_size, bias=False) - self.v_proj = nn.Linear(self.hidden_size, projection_size, bias=False) - self.q_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) - self.k_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) - self.v_conv1d = KimiK3ShortConvolution(projection_size, self.conv_size) - - self.A_log = nn.Parameter( - torch.log(torch.empty(self.num_heads, dtype=torch.float32).uniform_(1, 16)) - ) - self.f_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) - self.f_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) - self.dt_bias = nn.Parameter(torch.zeros(projection_size, dtype=torch.float32)) - self.b_proj = nn.Linear(self.hidden_size, self.num_heads, bias=False) - if self.use_full_rank_gate: - self.g_proj = nn.Linear(self.hidden_size, projection_size, bias=False) - else: - self.g_a_proj = nn.Linear(self.hidden_size, self.head_dim, bias=False) - self.g_b_proj = nn.Linear(self.head_dim, projection_size, bias=False) - self.o_norm = KimiK3GatedRMSNorm(self.head_dim, eps=float(config.rms_norm_eps)) - self.o_proj = nn.Linear(projection_size, self.hidden_size, bias=False) - - def _blocks(self, x: torch.Tensor) -> tuple[torch.Tensor, int, int]: - batch, query_len, hidden = x.shape - if query_len % self.block_size: - raise ValueError( - "KDA draft query length must be divisible by block_size; " - f"got {query_len} and {self.block_size}" - ) - num_blocks = query_len // self.block_size - return ( - x.reshape(batch * num_blocks, self.block_size, hidden), - batch, - query_len, - ) - - @torch.compiler.disable - def forward( - self, - hidden_states: torch.Tensor, - target_hidden: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - past_key_values: Optional[Cache] = None, - **kwargs, - ) -> tuple[torch.Tensor, None]: - del target_hidden, attention_mask, kwargs - if past_key_values is not None: - raise NotImplementedError( - "Kimi-K3 draft KDA training resets state at each proposal block" - ) - - blocks, batch, query_len = self._blocks(hidden_states) - q = self.q_conv1d(self.q_proj(blocks)) - k = self.k_conv1d(self.k_proj(blocks)) - v = self.v_conv1d(self.v_proj(blocks)) - shape = (*q.shape[:2], self.num_heads, self.head_dim) - q, k, v = (tensor.view(shape) for tensor in (q, k, v)) - raw_gate = self.f_b_proj(self.f_a_proj(blocks)).view(shape) - beta = self.b_proj(blocks).float() - - kernel: Callable[..., torch.Tensor] - kernel = _reference_kda if self.backend == "reference" else _fla_kda - output = kernel( - q, - k, - v, - raw_gate, - beta, - self.A_log, - self.dt_bias, - self.lower_bound, - ) - if self.use_full_rank_gate: - output_gate = self.g_proj(blocks).view(shape) - else: - output_gate = self.g_b_proj(self.g_a_proj(blocks)).view(shape) - output = self.o_norm(output, output_gate) - output = output.reshape(*blocks.shape[:2], -1) - output = self.o_proj(output).reshape(batch, query_len, self.hidden_size) - return output, None - - -class KimiK3DraftKDADecoderLayer(GradientCheckpointingLayer): - def __init__(self, config, layer_idx: int): - super().__init__() - hidden_size = int(config.hidden_size) - eps = float(config.rms_norm_eps) - self.self_attn = KimiK3DraftKDAAttention(config, layer_idx) - self.mlp = Qwen3MLP(config) - self.input_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) - self.post_attention_layernorm = Qwen3RMSNorm(hidden_size, eps=eps) - - def forward( - self, - target_hidden: Optional[torch.Tensor] = None, - hidden_states: Optional[torch.Tensor] = None, - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_value: Optional[Cache] = None, - **kwargs, - ) -> Tuple[torch.FloatTensor]: - del position_ids - residual = hidden_states - hidden_states = self.input_layernorm(hidden_states) - hidden_states = self.self_attn( - hidden_states=hidden_states, - target_hidden=target_hidden, - attention_mask=attention_mask, - past_key_values=past_key_value, - **kwargs, - )[0] - hidden_states = residual + hidden_states - residual = hidden_states - hidden_states = self.post_attention_layernorm(hidden_states) - return (residual + self.mlp(hidden_states),) - - -class _KimiK3DSparkDraftBase(DSparkDraftModel): - expected_projector_type = "dspark" - - def _initialize_kimi_backbone(self, config, layers: nn.ModuleList) -> None: - dflash_config = dict(getattr(config, "dflash_config", None) or {}) - projector_type = dflash_config.get("projector_type") - if projector_type is None: - dflash_config["projector_type"] = self.expected_projector_type - elif projector_type != self.expected_projector_type: - raise ValueError( - "Kimi-K3 DSpark drafts require dflash_config.projector_type='dspark'" - ) - config.dflash_config = dflash_config - - # Avoid constructing and immediately discarding the large GQA DSpark - # backbone. This is the shared DFlash initialization with custom layers. - Qwen3PreTrainedModel.__init__(self, config) - self.config = config - self.layers = layers - self.target_layer_ids = dflash_config.get( - "target_layer_ids", - build_target_layer_ids(config.num_target_layers, config.num_hidden_layers), - ) - self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) - self.rotary_emb = Qwen3RotaryEmbedding(config) - self.fc = nn.Linear( - len(self.target_layer_ids) * config.hidden_size, - config.hidden_size, - bias=False, - ) - self.hidden_norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps) - self.block_size = int(config.block_size) - self.mask_token_id = dflash_config.get("mask_token_id") - self.projector_type = dflash_config.get("projector_type") - self.pure_draft_prefix_len = dflash_config.get("pure_draft_prefix_len", 0) - self.shift_label = dflash_config.get("shift_label", False) - self._init_draft_head(config, dflash_config) - self.register_load_state_dict_pre_hook(normalize_draft_head_checkpoint_keys) - self.post_init() - - def forward( - self, - position_ids: torch.LongTensor, - attention_mask: Optional[torch.Tensor] = None, - noise_embedding: Optional[torch.Tensor] = None, - target_hidden: Optional[torch.Tensor] = None, - past_key_values: Optional[Cache] = None, - use_cache: bool = False, - **kwargs, - ) -> torch.Tensor: - if use_cache or past_key_values is not None: - raise NotImplementedError( - "Kimi-K3 DSpark training backbones do not use the HF cache path" - ) - hidden_states = noise_embedding - target_hidden = self.hidden_norm(self.fc(target_hidden)) - for layer in self.layers: - hidden_states = layer( - hidden_states=hidden_states, - target_hidden=target_hidden, - attention_mask=attention_mask, - position_ids=position_ids, - **kwargs, - )[0] - return self.norm(hidden_states) - - -def _validate_mla_config(config) -> None: - required = ( - "q_lora_rank", - "kv_lora_rank", - "qk_nope_head_dim", - "qk_rope_head_dim", - "v_head_dim", - ) - missing = [name for name in required if getattr(config, name, None) is None] - if missing: - raise ValueError(f"Kimi-K3 draft MLA config is missing: {missing}") - if not bool(getattr(config, "mla_use_nope", False)): - raise ValueError("Kimi-K3 draft MLA requires mla_use_nope=true") - if not bool(getattr(config, "mla_use_output_gate", False)): - raise ValueError("Kimi-K3 draft MLA requires mla_use_output_gate=true") - - -@register_draft -class KimiK3DSpark5MLADraftModel(_KimiK3DSparkDraftBase): - """Five-layer K3 MLA DSpark draft.""" - - _no_split_modules = ["KimiK3DraftMLADecoderLayer"] - - def __init__(self, config) -> None: - _validate_mla_config(config) - if int(config.num_hidden_layers) != 5: - raise ValueError("KimiK3DSpark5MLADraftModel requires exactly 5 layers") - layers = nn.ModuleList( - KimiK3DraftMLADecoderLayer(config, layer_idx) - for layer_idx in range(config.num_hidden_layers) - ) - self._initialize_kimi_backbone(config, layers) - - -@register_draft -class KimiK3DSpark4KDA1MLADraftModel(_KimiK3DSparkDraftBase): - """K3 draft with KDA, KDA, MLA, KDA, KDA layers.""" - - _no_split_modules = [ - "KimiK3DraftKDADecoderLayer", - "KimiK3DraftMLADecoderLayer", - ] - - def __init__(self, config) -> None: - _validate_mla_config(config) - if int(config.num_hidden_layers) != 5: - raise ValueError("KimiK3DSpark4KDA1MLADraftModel requires exactly 5 layers") - layer_pattern = list( - getattr(config, "draft_layer_types", None) - or ["kda", "kda", "mla", "kda", "kda"] - ) - expected = ["kda", "kda", "mla", "kda", "kda"] - if layer_pattern != expected: - raise ValueError( - "Kimi-K3 4KDA+1MLA layer pattern must be " - f"{expected}, got {layer_pattern}" - ) - factories = { - "kda": KimiK3DraftKDADecoderLayer, - "mla": KimiK3DraftMLADecoderLayer, - } - layers = nn.ModuleList( - factories[layer_type](config, layer_idx) - for layer_idx, layer_type in enumerate(layer_pattern) - ) - self._initialize_kimi_backbone(config, layers) - - -__all__ = [ - "KimiK3DSpark5MLADraftModel", - "KimiK3DSpark4KDA1MLADraftModel", - "KimiK3DraftMLAAttention", - "KimiK3DraftKDAAttention", -] diff --git a/tests/test_config/test_launch_topology.py b/tests/test_config/test_launch_topology.py index 2d7978986..0e77be1c8 100644 --- a/tests/test_config/test_launch_topology.py +++ b/tests/test_config/test_launch_topology.py @@ -20,10 +20,7 @@ "gpt-oss-20b-eagle3-online.yaml": 8, "lfm2.5-1.2b-instruct-dflash-online.yaml": 8, "inkling-dspark-disaggregated.yaml": 1, - "kimi-k3-dspark-v1c-disaggregated.yaml": 4, - "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml": 8, - "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml": 8, - "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml": 8, + "kimi-k3-dspark-disaggregated.yaml": 4, "ling-flash-2.0-eagle3-offline.yaml": 8, "ling-flash-2.0-eagle3-online.yaml": 8, "llama3.1-8b-eagle3-offline.yaml": 1, @@ -316,7 +313,7 @@ def _recipes() -> dict[str, Path]: class ExampleLaunchTopologyTest(unittest.TestCase): def test_every_recipe_has_the_explicit_golden_topology(self): recipes = _recipes() - self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 67) + self.assertEqual(len(EXPECTED_NPROC_PER_NODE), 64) self.assertEqual(set(recipes), set(EXPECTED_NPROC_PER_NODE)) for filename, nproc_per_node in EXPECTED_NPROC_PER_NODE.items(): @@ -341,16 +338,6 @@ def test_every_recipe_has_the_explicit_golden_topology(self): ) self.assertEqual(deployment["mode"], expected_mode) expected_trainer = {"nnodes": 1, "nproc_per_node": nproc_per_node} - if ( - filename - == "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" - ): - expected_trainer = { - "nnodes": 2, - "nproc_per_node": 8, - "master_addr": "trainer-node-0", - "master_port": 29500, - } self.assertEqual( deployment["trainer"], expected_trainer, @@ -379,13 +366,7 @@ def test_golden_topologies_validate_for_their_declared_world_size(self): config = Config.from_file(str(path)) topology = config.deployment.trainer expected_nproc = EXPECTED_NPROC_PER_NODE[filename] - expected_nnodes = ( - 2 - if filename - == "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" - else 1 - ) - self.assertEqual(topology.nnodes, expected_nnodes) + self.assertEqual(topology.nnodes, 1) self.assertEqual(topology.nproc_per_node, expected_nproc) config.validate_world_size(topology.nnodes * expected_nproc) @@ -438,7 +419,7 @@ def test_migrated_dspark_recipes_match_source_training_contract(self): self.assertEqual(qwen4b.training.objective_chunk_blocks, 128) kimi = Config.from_file( - str(EXAMPLE_CONFIG_DIR / "kimi-k3-dspark-v1c-disaggregated.yaml") + str(EXAMPLE_CONFIG_DIR / "kimi-k3-dspark-disaggregated.yaml") ) kimi_topology = kimi.deployment.trainer self.assertEqual( @@ -457,55 +438,6 @@ def test_migrated_dspark_recipes_match_source_training_contract(self): self.assertEqual(kimi.data.max_length, 65536) self.assertEqual(kimi.training.num_anchors, 512) - # The portable MLA/KDA recipes preserve the 16-sample optimizer - # microbatch on one DP8 trainer with two samples per rank. - for filename in ( - "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", - "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", - ): - with self.subTest(config=filename): - config = Config.from_file(str(EXAMPLE_CONFIG_DIR / filename)) - topology = config.deployment.trainer - world_size = topology.nnodes * topology.nproc_per_node - self.assertEqual(world_size, 8) - self.assertEqual(config.training.batch_size, 2) - self.assertEqual(config.training.accumulation_steps, 32) - self.assertEqual(world_size * config.training.batch_size, 16) - self.assertEqual( - world_size - * config.training.batch_size - * config.training.accumulation_steps, - 512, - ) - - # The exact reference recipe restores the source job's 16 trainer - # ranks. Keeping one sample per GPU avoids doubling per-rank draft - # compute, while the HTTP inbox relay removes any shared-filesystem - # requirement for the second trainer node. - reference = Config.from_file( - str( - EXAMPLE_CONFIG_DIR - / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" - ) - ) - topology = reference.deployment.trainer - world_size = topology.nnodes * topology.nproc_per_node - self.assertEqual(world_size, 16) - self.assertEqual(reference.training.batch_size, 1) - self.assertEqual(reference.training.accumulation_steps, 32) - self.assertEqual(world_size * reference.training.batch_size, 16) - self.assertEqual( - world_size - * reference.training.batch_size - * reference.training.accumulation_steps, - 512, - ) - self.assertEqual(topology.master_addr, "trainer-node-0") - self.assertEqual( - reference.deployment.disaggregated.inbox_server_url, - "http://trainer-node-0:35900", - ) - if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_data/test_template_registry.py b/tests/test_data/test_template_registry.py index 7609494ee..6355f5ffc 100644 --- a/tests/test_data/test_template_registry.py +++ b/tests/test_data/test_template_registry.py @@ -21,6 +21,21 @@ def test_deepseek_v2_uses_its_plain_text_tokenizer_headers(self): template.assistant_header, ) + def test_kimi_k3_template_matches_target_xtml_contract(self): + template = TEMPLATE_REGISTRY.get("kimi-k3-thinking") + self.assertEqual( + template.assistant_header, + '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>', + ) + self.assertEqual( + template.user_header, + '<|open|>message role="user"<|sep|>', + ) + self.assertEqual(template.end_of_turn_token, "<|end_of_msg|>") + self.assertEqual(template.parser_type, "thinking") + self.assertFalse(template.enable_thinking) + self.assertEqual(template.ignore_token, ["<|end_of_msg|>"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_modeling/test_kimi_k3_dspark_architectures.py b/tests/test_modeling/test_kimi_k3_dspark_architectures.py deleted file mode 100644 index e7307ee95..000000000 --- a/tests/test_modeling/test_kimi_k3_dspark_architectures.py +++ /dev/null @@ -1,422 +0,0 @@ -import json -import unittest -from pathlib import Path -from unittest.mock import patch - -import torch -from transformers.models.qwen3.modeling_qwen3 import Qwen3Config - -from specforge.config import Config -from specforge.data.template import TEMPLATE_REGISTRY -from specforge.modeling.draft import kimi_k3_dspark -from specforge.modeling.draft.kimi_k3_dspark import ( - KimiK3DraftKDAAttention, - KimiK3DSpark4KDA1MLADraftModel, - KimiK3DSpark5MLADraftModel, -) - -ROOT = Path(__file__).resolve().parents[2] - - -def _tiny_config(architecture: str, *, layers: int = 5) -> Qwen3Config: - config = Qwen3Config( - architectures=[architecture], - hidden_size=32, - intermediate_size=64, - num_hidden_layers=layers, - num_attention_heads=4, - num_key_value_heads=1, - head_dim=8, - q_lora_rank=8, - kv_lora_rank=8, - qk_nope_head_dim=4, - qk_rope_head_dim=4, - v_head_dim=4, - mla_use_nope=True, - mla_use_output_gate=True, - rope_interleave=True, - max_position_embeddings=128, - vocab_size=64, - block_size=3, - num_target_layers=8, - dflash_config={ - "projector_type": "dspark", - "target_layer_ids": [0, 1, 2, 3, 4], - "mask_token_id": 63, - "markov_rank": 4, - "enable_confidence_head": True, - "confidence_head_with_markov": True, - "confidence_head_alpha": 1.0, - }, - draft_layer_types=["kda", "kda", "mla", "kda", "kda"], - linear_attn_config={ - "backend": "reference", - "gate_lower_bound": -5.0, - "head_dim": 4, - "num_heads": 4, - "short_conv_kernel_size": 4, - "use_full_rank_gate": True, - }, - layer_types=["full_attention"] * layers, - attention_bias=False, - ) - config._attn_implementation = "sdpa" - return config - - -class TestKimiK3DSparkArchitectures(unittest.TestCase): - def test_production_configs_match_k3_target(self): - cases = [ - ("kimi-k3-dspark-5mla.json", "KimiK3DSpark5MLADraftModel"), - ( - "kimi-k3-dspark-4kda-1mla.json", - "KimiK3DSpark4KDA1MLADraftModel", - ), - ] - for filename, architecture in cases: - with self.subTest(filename=filename, architecture=architecture): - self._assert_production_config(filename, architecture) - - def _assert_production_config(self, filename, architecture): - config = json.loads((ROOT / "configs" / filename).read_text()) - assert config["architectures"] == [architecture] - assert config["block_size"] == 7 - assert config["num_hidden_layers"] == 5 - assert config["dflash_config"]["target_layer_ids"] == [11, 23, 47, 71, 83] - assert config["mla_use_output_gate"] is True - assert { - "num_attention_heads": config["num_attention_heads"], - "q_lora_rank": config["q_lora_rank"], - "kv_lora_rank": config["kv_lora_rank"], - "qk_nope_head_dim": config["qk_nope_head_dim"], - "qk_rope_head_dim": config["qk_rope_head_dim"], - "v_head_dim": config["v_head_dim"], - } == { - "num_attention_heads": 96, - "q_lora_rank": 1536, - "kv_lora_rank": 512, - "qk_nope_head_dim": 128, - "qk_rope_head_dim": 64, - "v_head_dim": 128, - } - - def test_reference_full_attention_config_is_exact_old_architecture(self): - config = json.loads( - (ROOT / "configs" / "kimi-k3-dspark-fullattn-gqa16.json").read_text() - ) - assert config["architectures"] == ["DSparkDraftModel"] - assert config["block_size"] == 7 - assert config["num_hidden_layers"] == 5 - assert config["layer_types"] == ["full_attention"] * 5 - assert config["num_attention_heads"] == 64 - assert config["num_key_value_heads"] == 16 - assert config["dflash_config"]["target_layer_ids"] == [7, 23, 51, 67, 83] - assert config["rope_scaling"] is None - - def test_reference_full_attention_recipe_preserves_exact_old_contract(self): - config = Config.from_file( - str( - ROOT - / "examples" - / "configs" - / "kimi-k3-dspark-fullattn-openperfectblend-disaggregated.yaml" - ) - ) - assert config.model.target_model_path == "/workspace/models/Kimi-K3" - assert config.data.max_length == 4096 - assert config.data.dataloader_num_workers == 4 - assert config.training.batch_size == 1 - assert config.training.accumulation_steps == 32 - assert config.deployment.trainer.nnodes == 2 - assert config.deployment.trainer.nproc_per_node == 8 - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - == 16 - ) - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - == 512 - ) - assert config.training.num_epochs == 10 - assert config.training.total_steps == 9173 - self.assertAlmostEqual( - config.training.learning_rate, - 6e-4, - delta=max(1e-12, 1e-6 * abs(6e-4)), - ) - assert config.training.lr_scheduler == "cosine" - self.assertAlmostEqual( - config.training.warmup_ratio, - 0.04, - delta=max(1e-12, 1e-6 * abs(0.04)), - ) - assert config.training.num_anchors == 512 - assert config.training.max_checkpoints == 3 - assert config.runtime.producer_lease == 16 - assert len(config.deployment.disaggregated.server_urls) == 2 - assert ( - config.deployment.disaggregated.inbox_server_url - == "http://trainer-node-0:35900" - ) - assert "openperfectblend-regen-9caaf705" in config.data.train_data_path - assert config.tracking.report_to == "wandb" - - def test_training_recipes_preserve_reference_run_contract(self): - for filename in [ - "kimi-k3-dspark-5mla-openperfectblend-disaggregated.yaml", - "kimi-k3-dspark-4kda-1mla-openperfectblend-disaggregated.yaml", - ]: - with self.subTest(filename=filename): - self._assert_training_recipe(filename) - - def _assert_training_recipe(self, filename): - config = Config.from_file(str(ROOT / "examples" / "configs" / filename)) - assert config.data.max_length == 4096 - assert config.training.batch_size == 2 - assert config.training.accumulation_steps == 32 - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - == 16 - ) - assert ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - == 512 - ) - assert config.training.num_epochs == 10 - self.assertAlmostEqual( - config.training.learning_rate, - 6e-4, - delta=max(1e-12, 1e-6 * abs(6e-4)), - ) - self.assertAlmostEqual( - config.training.warmup_ratio, - 0.04, - delta=max(1e-12, 1e-6 * abs(0.04)), - ) - assert config.training.num_anchors == 512 - assert config.training.save_interval == 250 - assert config.training.log_interval == 10 - optimizer_quantum = ( - config.deployment.trainer.nnodes - * config.deployment.trainer.nproc_per_node - * config.training.batch_size - * config.training.accumulation_steps - ) - max_capture_overshoot = ( - len(config.deployment.disaggregated.server_urls) - * config.runtime.producer_concurrency - * config.runtime.producer_lease - ) - # Two windows are required for capture and training to overlap. Once one - # window is acknowledged, hysteresis must resume capture even at the - # maximum number of concurrently leased refs. - assert config.runtime.in_flight_high_watermark >= 2 * optimizer_quantum - assert ( - config.runtime.in_flight_low_watermark - >= config.runtime.in_flight_high_watermark - + max_capture_overshoot - - optimizer_quantum - ) - # Each capture HTTP request uses the source job's complete 16-sample - # optimizer microbatch to amortize auxiliary-state aggregation. This is a - # producer request size, independent of the DP8 consumer's per-rank batch. - assert config.runtime.producer_lease == 16 - assert config.runtime.producer_concurrency == 2 - assert config.model.sglang_enable_symm_mem is False - assert config.model.sglang_max_running_requests == 16 - # K3 consumes five linear-attention cache entries per live request; 40 - # silently caps SGLang at eight even when max_running_requests is 16. - assert config.model.sglang_max_mamba_cache_size == 80 - assert len(config.deployment.disaggregated.server_urls) >= 2 - # Two worst-case 4,096-token optimizer windows carry about 336 GiB of - # captured features; byte throttling must not serialize the pipeline. - assert config.runtime.resident_high_watermark_bytes >= 360777252864 - assert config.runtime.resident_low_watermark_bytes >= 180388626432 - assert ( - config.runtime.feature_store_max_resident_bytes - >= config.runtime.resident_high_watermark_bytes - ) - assert config.tracking.report_to == "wandb" - assert config.tracking.wandb_offline is False - assert config.data.chat_template in TEMPLATE_REGISTRY.get_all_template_names() - assert "kimi-k3-openperfectblend-regen-439c2fdc" in ( - config.data.train_data_path - ) - - def test_kimi_k3_template_matches_target_xtml_contract(self): - template = TEMPLATE_REGISTRY.get("kimi-k3-thinking") - assert template.assistant_header == ( - '<|open|>message role="assistant"<|sep|><|open|>think<|sep|>' - ) - assert template.user_header == '<|open|>message role="user"<|sep|>' - assert template.end_of_turn_token == "<|end_of_msg|>" - assert template.parser_type == "thinking" - assert template.enable_thinking is False - assert template.ignore_token == ["<|end_of_msg|>"] - - def _forward_backward(self, model): - batch, context_len, query_len = 1, 5, 6 - config = model.config - target_hidden = torch.randn( - batch, - context_len, - len(config.dflash_config["target_layer_ids"]) * config.hidden_size, - ) - noise_embedding = torch.randn( - batch, query_len, config.hidden_size, requires_grad=True - ) - position_ids = torch.arange(context_len + query_len).expand(batch, -1) - output = model( - position_ids=position_ids, - target_hidden=target_hidden, - noise_embedding=noise_embedding, - ) - assert output.shape == (batch, query_len, config.hidden_size) - assert torch.isfinite(output).all() - output.square().mean().backward() - assert noise_embedding.grad is not None - assert torch.isfinite(noise_embedding.grad).all() - - def test_tiny_5mla_forward_and_backward(self): - model = KimiK3DSpark5MLADraftModel(_tiny_config("KimiK3DSpark5MLADraftModel")) - self._forward_backward(model) - assert all(layer.self_attn.use_output_gate for layer in model.layers) - - def test_mla_absorbed_and_expanded_attention_are_algebraically_equivalent(self): - torch.manual_seed(7) - batch, queries, keys, heads = 2, 3, 5, 4 - nope_dim, latent_dim, value_dim = 6, 8, 7 - q_nope = torch.randn(batch, queries, heads, nope_dim, dtype=torch.float64) - kv_latent = torch.randn(batch, keys, latent_dim, dtype=torch.float64) - w_kc = torch.randn(heads, nope_dim, latent_dim, dtype=torch.float64) - w_vc = torch.randn(heads, value_dim, latent_dim, dtype=torch.float64) - - q_absorbed = torch.einsum("bqhd,hdk->bqhk", q_nope, w_kc) - absorbed_scores = torch.einsum("bqhk,bsk->bhqs", q_absorbed, kv_latent) - k_expanded = torch.einsum("bsk,hdk->bshd", kv_latent, w_kc) - expanded_scores = torch.einsum("bqhd,bshd->bhqs", q_nope, k_expanded) - torch.testing.assert_close(absorbed_scores, expanded_scores) - - probabilities = absorbed_scores.softmax(dim=-1) - latent_output = torch.einsum("bhqs,bsk->bhqk", probabilities, kv_latent) - absorbed_output = torch.einsum("bhqk,hvk->bqhv", latent_output, w_vc) - v_expanded = torch.einsum("bsk,hvk->bshv", kv_latent, w_vc) - expanded_output = torch.einsum("bhqs,bshv->bqhv", probabilities, v_expanded) - torch.testing.assert_close(absorbed_output, expanded_output) - - def test_tiny_4kda_1mla_forward_and_backward(self): - model = KimiK3DSpark4KDA1MLADraftModel( - _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - ) - self._forward_backward(model) - assert [type(layer.self_attn).__name__ for layer in model.layers] == [ - "KimiK3DraftKDAAttention", - "KimiK3DraftKDAAttention", - "KimiK3DraftMLAAttention", - "KimiK3DraftKDAAttention", - "KimiK3DraftKDAAttention", - ] - first_kda = model.layers[0].self_attn - assert "q_conv1d.weight" in first_kda.state_dict() - assert "q_conv1d.bias" not in first_kda.state_dict() - - def test_fla_kda_splits_independent_blocks_below_cuda_grid_z_limit(self): - calls = [] - - def fake_chunk_kda(**kwargs): - q = kwargs["q"] - calls.append(int(q.shape[0])) - assert q.shape[0] * q.shape[2] <= 8 - assert kwargs["output_final_state"] is False - assert kwargs["use_qk_l2norm_in_kernel"] is True - assert kwargs["use_gate_in_kernel"] is True - assert kwargs["use_beta_sigmoid_in_kernel"] is True - output = ( - q - + kwargs["k"] - + kwargs["v"] - + kwargs["g"] - + kwargs["beta"].unsqueeze(-1) - + kwargs["A_log"].view(1, 1, -1, 1) - + kwargs["dt_bias"].view(1, 1, q.shape[2], q.shape[3]) - ) - return output, None - - shape = (5, 2, 3, 4) - q, k, v, gate = (torch.randn(shape, requires_grad=True) for _ in range(4)) - beta = torch.randn(shape[:-1], requires_grad=True) - A_log = torch.randn(shape[2], requires_grad=True) - dt_bias = torch.randn(shape[2] * shape[3], requires_grad=True) - with ( - patch.object(kimi_k3_dspark, "_CUDA_MAX_GRID_DIM_Z", 8), - patch.object( - kimi_k3_dspark, - "_load_fla_chunk_kda", - return_value=fake_chunk_kda, - ), - ): - output = kimi_k3_dspark._fla_kda( - q, - k, - v, - gate, - beta, - A_log, - dt_bias, - lower_bound=-5.0, - ) - - assert calls == [2, 2, 1] - assert output.shape == shape - output.sum().backward() - for tensor in (q, k, v, gate, beta, A_log, dt_bias): - assert tensor.grad is not None - assert torch.isfinite(tensor.grad).all() - - def test_kda_resets_state_between_proposal_blocks(self): - config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - attention = KimiK3DraftKDAAttention(config, layer_idx=0) - first = torch.randn(1, config.block_size, config.hidden_size) - second = torch.randn_like(first) - baseline = attention(torch.cat((first, second), dim=1))[0] - changed = attention(torch.cat((first, second + 20.0), dim=1))[0] - torch.testing.assert_close( - baseline[:, : config.block_size], - changed[:, : config.block_size], - ) - - def test_hybrid_rejects_noncanonical_layer_order(self): - config = _tiny_config("KimiK3DSpark4KDA1MLADraftModel") - config.draft_layer_types = ["mla", "kda", "kda", "kda", "kda"] - with self.assertRaisesRegex(ValueError, "layer pattern"): - KimiK3DSpark4KDA1MLADraftModel(config) - - def test_5mla_rejects_noncanonical_layer_count(self): - config = _tiny_config("KimiK3DSpark5MLADraftModel", layers=4) - with self.assertRaisesRegex(ValueError, "exactly 5 layers"): - KimiK3DSpark5MLADraftModel(config) - - def test_mla_requires_k3_attention_features(self): - for field in ("mla_use_nope", "mla_use_output_gate"): - with self.subTest(field=field): - self._assert_mla_requires_k3_attention_feature(field) - - def _assert_mla_requires_k3_attention_feature(self, field): - config = _tiny_config("KimiK3DSpark5MLADraftModel") - setattr(config, field, False) - with self.assertRaisesRegex(ValueError, field): - KimiK3DSpark5MLADraftModel(config) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_runtime/test_model_loading.py b/tests/test_runtime/test_model_loading.py index 096e500ac..38182bdab 100644 --- a/tests/test_runtime/test_model_loading.py +++ b/tests/test_runtime/test_model_loading.py @@ -106,26 +106,6 @@ def _draft_payload(architecture: str, *, layers: int = 1, block_size=None): class DraftConfigResolutionTest(unittest.TestCase): - def test_dspark_accepts_registered_kimi_k3_backbones(self): - provider = _draft_config_provider("dspark") - for architecture in ( - "KimiK3DSpark5MLADraftModel", - "KimiK3DSpark4KDA1MLADraftModel", - ): - with ( - self.subTest(architecture=architecture), - tempfile.TemporaryDirectory() as directory, - ): - path = os.path.join(directory, "draft.json") - payload = _draft_payload(architecture, layers=5, block_size=7) - with open(path, "w", encoding="utf-8") as stream: - json.dump(payload, stream) - resolved = resolve_draft_config( - _run_config("dspark", draft_model_config=path), - provider=provider, - ) - self.assertEqual(resolved.architectures, [architecture]) - def test_config_resolution_does_not_initialize_cuda_model_dependencies(self): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "draft.json") diff --git a/tests/test_runtime/test_package_architecture.py b/tests/test_runtime/test_package_architecture.py index 2a7e4bad4..60b6cc96b 100644 --- a/tests/test_runtime/test_package_architecture.py +++ b/tests/test_runtime/test_package_architecture.py @@ -713,8 +713,7 @@ def test_dspark_configs_are_qwen3_gqa_only(self): { "glm-5.2-dspark.json", "inkling-dspark.json", - "kimi-k3-dspark-fullattn-gqa16.json", - "kimi-k3-dspark-v1c.json", + "kimi-k3-dspark.json", "qwen3-4b-dspark.json", "qwen3-8b-dspark.json", "qwen3.6-27b-dspark.json", diff --git a/tests/test_scripts/test_disagg_launchers.py b/tests/test_scripts/test_disagg_launchers.py index 7d854925c..bdaaec6dd 100644 --- a/tests/test_scripts/test_disagg_launchers.py +++ b/tests/test_scripts/test_disagg_launchers.py @@ -14,7 +14,6 @@ OFFLINE_TWO_NODE = ROOT / "examples" / "disagg" / "run_offline_2node.sh" TWO_NODE = ROOT / "examples" / "disagg" / "run_qwen3_8b_dflash_disagg_2node.sh" INKLING_TWO_NODE = ROOT / "examples" / "disagg" / "run_inkling_dspark_disagg_2node.sh" -KIMI_K3_CAPTURE = ROOT / "examples" / "disagg" / "run_kimi_k3_dspark_capture_server.sh" KIMI_K3_CAPTURE_PATCH = ( ROOT / "patches" / "sglang" / "kimi-k3-f8493a4" / "spec-capture.patch" ) @@ -62,7 +61,7 @@ def _run(self, wrapper, *args, include_config=True): ) def test_wrappers_are_executable_and_syntax_valid(self): - for wrapper in (ONLINE, OFFLINE, KIMI_K3_CAPTURE): + for wrapper in (ONLINE, OFFLINE): with self.subTest(wrapper=wrapper.name): self.assertTrue(os.access(wrapper, os.X_OK)) result = subprocess.run( @@ -128,19 +127,6 @@ def test_help_describes_auto_and_explicit_roles(self): self.assertIn("--role", result.stdout) self.assertIn("producer and consumer", result.stdout) - def test_kimi_k3_capture_launcher_keeps_prefill_on_the_fast_path(self): - source = KIMI_K3_CAPTURE.read_text(encoding="utf-8") - self.assertIn('--max-running-requests "$MAX_RUNNING_REQUESTS"', source) - self.assertIn("MAX_RUNNING_REQUESTS=${MAX_RUNNING_REQUESTS:-16}", source) - self.assertIn("MAX_TOTAL_TOKENS=${MAX_TOTAL_TOKENS:-73728}", source) - self.assertIn("MAX_PREFILL_TOKENS=${MAX_PREFILL_TOKENS:-40960}", source) - self.assertIn("MAX_MAMBA_CACHE_SIZE=${MAX_MAMBA_CACHE_SIZE:-80}", source) - self.assertIn('--max-prefill-tokens "$MAX_PREFILL_TOKENS"', source) - self.assertIn('--max-mamba-cache-size "$MAX_MAMBA_CACHE_SIZE"', source) - self.assertIn("SGLANG_SPEC_CAPTURE_MAX_PENDING_BATCHES:-2", source) - self.assertIn("--disable-cuda-graph", source) - self.assertNotIn("--enable-symm-mem", source) - def test_kimi_k3_capture_patch_only_copies_features_on_the_writer_rank(self): source = KIMI_K3_CAPTURE_PATCH.read_text(encoding="utf-8") self.assertIn("self.output_streamer.ps.attn_tp_rank != 0", source) From a0ae7f4d62dddca94353ed5a8a143bfc8ec918a7 Mon Sep 17 00:00:00 2001 From: Yi Sun Date: Tue, 4 Aug 2026 00:43:09 +0000 Subject: [PATCH 39/88] =?UTF-8?q?Fix=20the=20leftover=20hardcoded=20recipe?= =?UTF-8?q?=20count=20in=20tests/test=5Fconfig/test=5Funified=5Ffeature=5F?= =?UTF-8?q?reachability.py=20(67=20=E2=86=92=2064).=20That=20test=20passes?= =?UTF-8?q?=20now.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_config/test_unified_feature_reachability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config/test_unified_feature_reachability.py b/tests/test_config/test_unified_feature_reachability.py index 4e07c23cb..2421071ce 100644 --- a/tests/test_config/test_unified_feature_reachability.py +++ b/tests/test_config/test_unified_feature_reachability.py @@ -146,7 +146,7 @@ def test_all_example_configs_validate_through_the_typed_entry(self): for path in EXAMPLE_CONFIG_DIR.glob("*.yaml") if not path.name.startswith(".") ) - self.assertEqual(len(paths), 67) + self.assertEqual(len(paths), 64) resolved_runs = { path.name: resolve_run(Config.from_file(str(path))) for path in paths From 211d83ace446caaa63ed887727933e61709d43dc Mon Sep 17 00:00:00 2001 From: canghua Date: Tue, 4 Aug 2026 11:00:09 +0800 Subject: [PATCH 40/88] fix --- specforge/export/checkpoint_io.py | 19 ++++++++++++++----- tests/test_runtime/test_export.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/specforge/export/checkpoint_io.py b/specforge/export/checkpoint_io.py index 3e2f3fddf..87aad6929 100644 --- a/specforge/export/checkpoint_io.py +++ b/specforge/export/checkpoint_io.py @@ -33,8 +33,9 @@ def apply_legacy_rope_scaling(output_dir: str) -> bool: """Keep modern and legacy RoPE scaling fields compatible in an export. Transformers 5 writes ``rope_parameters`` while older serving stacks read - only ``rope_scaling``. When exactly one non-default representation is - present, write the other. On by default; set + only ``rope_scaling`` and the top-level ``rope_theta``. Mirror non-default + scaling representations and preserve the modern ``rope_theta`` for legacy + readers. On by default; set ``SPECFORGE_DISABLE_LEGACY_ROPE_SCALING=1`` to skip. Returns whether the config was rewritten. """ @@ -51,6 +52,13 @@ def apply_legacy_rope_scaling(output_dir: str) -> bool: def rope_kind(payload): return (payload or {}).get("rope_type") or (payload or {}).get("type") + changed = False + if rope_parameters and "rope_theta" in rope_parameters: + rope_theta = rope_parameters["rope_theta"] + if config.get("rope_theta") != rope_theta: + config["rope_theta"] = rope_theta + changed = True + if ( rope_parameters and not rope_scaling @@ -59,8 +67,7 @@ def rope_kind(payload): config["rope_scaling"] = { key: value for key, value in rope_parameters.items() if key != "rope_theta" } - if "rope_theta" in rope_parameters: - config["rope_theta"] = rope_parameters["rope_theta"] + changed = True elif ( rope_scaling and not rope_parameters @@ -70,7 +77,9 @@ def rope_kind(payload): if "rope_theta" in config: mirrored.setdefault("rope_theta", config["rope_theta"]) config["rope_parameters"] = mirrored - else: + changed = True + + if not changed: return False temporary = f"{config_path}.{os.getpid()}.tmp" diff --git a/tests/test_runtime/test_export.py b/tests/test_runtime/test_export.py index 8303bc1fb..9a2dc0993 100644 --- a/tests/test_runtime/test_export.py +++ b/tests/test_runtime/test_export.py @@ -87,6 +87,26 @@ def test_default_rope_config_is_not_rewritten(self): self.assertEqual(after, before) + def test_default_rope_theta_is_mirrored_for_legacy_readers(self): + from specforge.export.checkpoint_io import apply_legacy_rope_scaling + + with tempfile.TemporaryDirectory() as directory: + path = self._write_config( + directory, + { + "rope_parameters": { + "rope_type": "default", + "rope_theta": 1_000_000, + } + }, + ) + self.assertTrue(apply_legacy_rope_scaling(directory)) + with open(path, encoding="utf-8") as handle: + config = json.load(handle) + + self.assertEqual(config["rope_theta"], 1_000_000) + self.assertNotIn("rope_scaling", config) + class TestLegacyVocabMappingCompatibility(unittest.TestCase): def setUp(self): From 3107f3c164ff0ffb61e7245c6e5c94a4374f083e Mon Sep 17 00:00:00 2001 From: Richard Zou Date: Mon, 20 Jul 2026 20:29:17 -0700 Subject: [PATCH 41/88] DFlash/Domino: Add support for the FlexAttention FLASH backend Adds an option to turn on FAv4 backend for FlexAttention. This is significantly faster on Blackwell than the default backend (which is triton). We unfortunately need to monkey-patch PyTorch (in 2.11, which is what SGLang is pinned to right now). Whenever SpecForge can upgrade to PyTorch >= 2.13 then we can get rid of the monkeypatches (and also the correctness test). This PR also cleans up the attention implementation a little. --- .../algorithms/common/dflash_family_model.py | 19 +- specforge/modeling/draft/dflash.py | 61 ++++-- .../modeling/draft/flex_attention_backend.py | 28 +++ specforge/torch_compat.py | 62 +++++++ .../test_flex_attention_backend.py | 173 ++++++++++++++++++ 5 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 specforge/modeling/draft/flex_attention_backend.py create mode 100644 specforge/torch_compat.py create mode 100644 tests/test_modeling/test_flex_attention_backend.py diff --git a/specforge/algorithms/common/dflash_family_model.py b/specforge/algorithms/common/dflash_family_model.py index 69cdd0f3a..db659395a 100644 --- a/specforge/algorithms/common/dflash_family_model.py +++ b/specforge/algorithms/common/dflash_family_model.py @@ -9,6 +9,7 @@ from specforge.core.chunking import checkpointed_chunk_reduce from specforge.modeling.draft.dflash import DFlashDraftModel +from specforge.modeling.draft.flex_attention_backend import flex_attention_backend try: from torch.nn.attention.flex_attention import BlockMask, create_block_mask @@ -94,6 +95,7 @@ def create_dflash_block_mask( S: int, block_size: int, device: torch.device, + flex_block_size=None, sliding_window: Optional[int] = None, ): """Construct a full or sliding Flex Attention mask for DFlash training.""" @@ -128,8 +130,17 @@ def dflash_mask_mod(b, h, q_idx, kv_idx): Q_LEN = N * block_size KV_LEN = S + N * block_size + kwargs = {} + if flex_block_size is not None: + kwargs["BLOCK_SIZE"] = flex_block_size return create_block_mask( - dflash_mask_mod, B=B, H=None, Q_LEN=Q_LEN, KV_LEN=KV_LEN, device=device + dflash_mask_mod, + B=B, + H=None, + Q_LEN=Q_LEN, + KV_LEN=KV_LEN, + device=device, + **kwargs, ) @@ -309,6 +320,12 @@ def _forward_draft_blocks( "block_size": self.block_size, "device": device, } + if ( + self.attention_backend == "flex_attention" + and flex_attention_backend() == "FLASH" + ): + # FLASH requires a minimum of this block size. + mask_args["flex_block_size"] = (256, 128) full_attn_mask = mask_builder(**mask_args) sliding_window = self.draft_model.sliding_window dflash_attn_mask = full_attn_mask diff --git a/specforge/modeling/draft/dflash.py b/specforge/modeling/draft/dflash.py index 4fea605bd..4acd30e11 100644 --- a/specforge/modeling/draft/dflash.py +++ b/specforge/modeling/draft/dflash.py @@ -4,6 +4,7 @@ from torch import nn from transformers import DynamicCache from transformers.cache_utils import Cache +from transformers.integrations.flex_attention import compile_friendly_flex_attention from transformers.modeling_outputs import CausalLMOutputWithPast from transformers.models.qwen3.modeling_qwen3 import ( ALL_ATTENTION_FUNCTIONS, @@ -18,6 +19,7 @@ from typing_extensions import Tuple, Unpack from .dflash_kernels import DEFAULT_DFLASH_KERNELS, DFlashKernels +from .flex_attention_backend import flex_attention_backend from .registry import register_draft FULL_ATTENTION = "full_attention" @@ -114,6 +116,10 @@ def __init__( ) self.scaling = self.head_dim**-0.5 self.attention_dropout = config.attention_dropout + if config._attn_implementation == "flex_attention": + assert ( + config.attention_dropout == 0.0 + ), "DFlash FlexAttention requires attention_dropout=0.0" self.is_causal = False self.q_proj = nn.Linear( config.hidden_size, @@ -176,27 +182,44 @@ def forward( cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position} k, v = past_key_values.update(k, v, self.layer_idx, cache_kwargs) valid_queries = None - attn_fn: Callable = eager_attention_forward - if self.config._attn_implementation == "eager": - attention_mask, valid_queries = _prepare_dflash_eager_mask( + if self.config._attn_implementation == "flex_attention": + kernel_options = dict(kwargs.pop("kernel_options", None) or {}) + backend = flex_attention_backend() + if backend is not None: + kernel_options["BACKEND"] = backend + + attn_output = compile_friendly_flex_attention( + q, + k, + v, + block_mask=attention_mask, + enable_gqa=True, + scale=self.scaling, + kernel_options=kernel_options or None, + ).transpose(1, 2) + attn_weights = None + else: + attn_fn: Callable = eager_attention_forward + if self.config._attn_implementation == "eager": + attention_mask, valid_queries = _prepare_dflash_eager_mask( + attention_mask, + q.dtype, + ) + else: + attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] + attn_output, attn_weights = attn_fn( + self, + q, + k, + v, attention_mask, - q.dtype, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=self.sliding_window, + **kwargs, ) - else: - attn_fn = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation] - attn_output, attn_weights = attn_fn( - self, - q, - k, - v, - attention_mask, - dropout=0.0 if not self.training else self.attention_dropout, - scaling=self.scaling, - sliding_window=self.sliding_window, - **kwargs, - ) - if valid_queries is not None and attn_weights is not None: - attn_weights = attn_weights.masked_fill(~valid_queries, 0) + if valid_queries is not None and attn_weights is not None: + attn_weights = attn_weights.masked_fill(~valid_queries, 0) attn_output = attn_output.reshape(bsz, q_len, -1) attn_output = self.o_proj(attn_output) if valid_queries is not None: diff --git a/specforge/modeling/draft/flex_attention_backend.py b/specforge/modeling/draft/flex_attention_backend.py new file mode 100644 index 000000000..80c127425 --- /dev/null +++ b/specforge/modeling/draft/flex_attention_backend.py @@ -0,0 +1,28 @@ +"""FlexAttention backend selection shared by DFlash training components.""" + +from __future__ import annotations + +import os +from typing import Optional + +from specforge.torch_compat import patch_inductor_cutedsl_lowerings + +_VALID_BACKENDS = {"AUTO", "TRITON", "FLASH", "TRITON_DECODE"} +_BACKEND_ENV = "SPECFORGE_FLEX_ATTENTION_BACKEND" + + +def flex_attention_backend() -> Optional[str]: + backend = os.environ.get(_BACKEND_ENV, "").upper() + if not backend: + return None + if backend not in _VALID_BACKENDS: + raise ValueError( + f"{_BACKEND_ENV} must be one of {sorted(_VALID_BACKENDS)}, " + f"got {backend!r}" + ) + if backend == "FLASH": + patch_inductor_cutedsl_lowerings() + return backend + + +__all__ = ["flex_attention_backend"] diff --git a/specforge/torch_compat.py b/specforge/torch_compat.py new file mode 100644 index 000000000..2bf868853 --- /dev/null +++ b/specforge/torch_compat.py @@ -0,0 +1,62 @@ +"""PyTorch compatibility shims used by optional fast paths.""" + +from __future__ import annotations + +import importlib + +import sympy +import torch +from packaging.version import InvalidVersion, Version + + +def patch_inductor_cutedsl_lowerings() -> bool: + """Backfill CuteDSL lowering needed by Torch 2.11 FLASH FlexAttention.""" + try: + torch_version = Version(torch.__version__.split("+", 1)[0]) + except InvalidVersion: + return False + if torch_version.major != 2 or torch_version.minor != 11: + return False + + try: + module = importlib.import_module( + "torch._inductor.codegen.cutedsl.cutedsl_op_overrides" + ) + except ImportError: + return False + from torch._inductor.utils import get_bounds_index_expr + from torch._inductor.virtualized import V + + overrides = module.CuteDSLOpOverrides + if getattr(overrides, "_specforge_cutedsl_patch", False): + return True + + def _minimum(a, b): + return overrides.where(overrides.lt(a, b), a, b) + + def _maximum(a, b): + return overrides.where(overrides.gt(a, b), a, b) + + def _index_expr(expr: sympy.Expr, dtype: torch.dtype): + if isinstance(expr, (int, sympy.Integer)): + return overrides.constant(int(expr), dtype) + + idx_str = V.kernel.kexpr(V.kernel.rename_indexing(expr)) + result = V.kernel.cse.generate( + V.kernel.body, + idx_str, + bounds=get_bounds_index_expr(expr), + dtype=dtype, + ) + result.is_scalar_expr = True + result.index_expr = V.graph.sizevars.simplify(expr) + return result + + overrides.minimum = staticmethod(_minimum) + overrides.maximum = staticmethod(_maximum) + overrides.index_expr = staticmethod(_index_expr) + overrides._specforge_cutedsl_patch = True + return True + + +__all__ = ["patch_inductor_cutedsl_lowerings"] diff --git a/tests/test_modeling/test_flex_attention_backend.py b/tests/test_modeling/test_flex_attention_backend.py new file mode 100644 index 000000000..13d77b8c9 --- /dev/null +++ b/tests/test_modeling/test_flex_attention_backend.py @@ -0,0 +1,173 @@ +import os +import unittest +from unittest import mock + +import torch +from torch.nn.attention.flex_attention import flex_attention +from transformers import Qwen3Config + +from specforge.algorithms.common.dflash_family_model import create_dflash_block_mask +from specforge.modeling.draft.dflash import Qwen3DFlashAttention +from specforge.modeling.draft.dflash_kernels import DEFAULT_DFLASH_KERNELS +from specforge.modeling.draft.flex_attention_backend import flex_attention_backend + + +class FlexAttentionBackendTest(unittest.TestCase): + # This correctness regression test can be deleted when we require + # torch>=2.13; it tests the Torch 2.11 Inductor monkeypatch for CuteDSL + # operations in patch_inductor_cutedsl_lowerings(). + @unittest.skipUnless( + torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 10, + "FLASH FlexAttention correctness requires a Blackwell CUDA device", + ) + def test_flash_matches_triton_forward_and_backward(self): + torch.manual_seed(0) + device = torch.device("cuda") + dtype = torch.bfloat16 + batch_size, num_query_heads, num_key_value_heads = 1, 3, 1 + context_len, head_dim = 256, 64 + num_blocks, draft_block_size = 4, 64 + query_len = num_blocks * draft_block_size + kv_len = context_len + query_len + anchors = torch.tensor([[64, 128, 192, 224]], device=device) + keep_blocks = torch.ones( + (batch_size, num_blocks), dtype=torch.bool, device=device + ) + + inputs = ( + torch.randn( + batch_size, + num_query_heads, + query_len, + head_dim, + device=device, + dtype=dtype, + ), + torch.randn( + batch_size, + num_key_value_heads, + kv_len, + head_dim, + device=device, + dtype=dtype, + ), + torch.randn( + batch_size, + num_key_value_heads, + kv_len, + head_dim, + device=device, + dtype=dtype, + ), + ) + + def run_backend(backend, flex_block_size=None): + block_mask = create_dflash_block_mask( + anchor_positions=anchors, + block_keep_mask=keep_blocks, + S=context_len, + block_size=draft_block_size, + device=device, + flex_block_size=flex_block_size, + ) + + compiled_attention = torch.compile( + lambda query, key, value, mask: flex_attention( + query, + key, + value, + block_mask=mask, + enable_gqa=True, + kernel_options={"BACKEND": backend}, + ), + fullgraph=True, + ) + + query, key, value = [ + tensor.detach().clone().requires_grad_(True) for tensor in inputs + ] + output = compiled_attention(query, key, value, block_mask) + output.float().square().mean().backward() + torch.cuda.synchronize() + return output.detach(), tuple( + tensor.grad.detach() for tensor in (query, key, value) + ) + + triton_output, triton_grads = run_backend("TRITON") + with mock.patch.dict(os.environ, {"SPECFORGE_FLEX_ATTENTION_BACKEND": "FLASH"}): + self.assertEqual(flex_attention_backend(), "FLASH") + flash_output, flash_grads = run_backend("FLASH", (256, 128)) + + self.assertTrue(torch.isfinite(flash_output).all()) + torch.testing.assert_close(flash_output, triton_output, atol=3e-3, rtol=2e-2) + for flash_grad, triton_grad in zip(flash_grads, triton_grads): + self.assertTrue(torch.isfinite(flash_grad).all()) + torch.testing.assert_close(flash_grad, triton_grad, atol=5e-6, rtol=2e-2) + + @unittest.skipUnless( + torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 10, + "FLASH FlexAttention correctness requires a Blackwell CUDA device", + ) + def test_dflash_flash_attention_forward_backward_smoke(self): + config = Qwen3Config( + hidden_size=256, + intermediate_size=512, + num_attention_heads=4, + num_key_value_heads=2, + num_hidden_layers=1, + head_dim=64, + layer_types=["full_attention"], + attention_dropout=0.0, + ) + config._attn_implementation = "flex_attention" + attention = Qwen3DFlashAttention( + config, + layer_idx=0, + kernels=DEFAULT_DFLASH_KERNELS, + ).to(device="cuda", dtype=torch.bfloat16) + hidden_states = torch.randn( + 1, + 256, + config.hidden_size, + device="cuda", + dtype=torch.bfloat16, + requires_grad=True, + ) + target_hidden = torch.randn( + 1, + 256, + config.hidden_size, + device="cuda", + dtype=torch.bfloat16, + requires_grad=True, + ) + block_mask = create_dflash_block_mask( + anchor_positions=torch.tensor([[64, 128, 192, 224]], device="cuda"), + block_keep_mask=torch.ones(1, 4, dtype=torch.bool, device="cuda"), + S=256, + block_size=64, + device=torch.device("cuda"), + flex_block_size=(256, 128), + ) + cos = torch.ones(1, 512, config.head_dim, device="cuda", dtype=torch.bfloat16) + sin = torch.zeros_like(cos) + + with mock.patch.dict(os.environ, {"SPECFORGE_FLEX_ATTENTION_BACKEND": "FLASH"}): + output, weights = attention( + hidden_states=hidden_states, + target_hidden=target_hidden, + position_embeddings=(cos, sin), + attention_mask=block_mask, + ) + output.float().square().mean().backward() + torch.cuda.synchronize() + + self.assertIsNone(weights) + self.assertIsNotNone(hidden_states.grad) + self.assertIsNotNone(target_hidden.grad) + self.assertTrue(torch.isfinite(hidden_states.grad).all()) + self.assertTrue(torch.isfinite(target_hidden.grad).all()) + + +if __name__ == "__main__": + unittest.main() From 1e014d1087f5a0fce0518eb5e1c787d6e5d7eb8e Mon Sep 17 00:00:00 2001 From: canghua Date: Fri, 31 Jul 2026 10:58:06 +0800 Subject: [PATCH 42/88] support sliding-window --- .../algorithms/common/dflash_family_model.py | 6 + .../test_flex_attention_backend.py | 147 +++++++++++++++++- tests/test_utils/test_dflash_mask.py | 81 ++++++++++ 3 files changed, 232 insertions(+), 2 deletions(-) diff --git a/specforge/algorithms/common/dflash_family_model.py b/specforge/algorithms/common/dflash_family_model.py index db659395a..a6dc37ac4 100644 --- a/specforge/algorithms/common/dflash_family_model.py +++ b/specforge/algorithms/common/dflash_family_model.py @@ -54,6 +54,9 @@ def create_dflash_sdpa_mask( sliding_window: Optional[int] = None, ): """Construct a full or sliding dense boolean DFlash mask.""" + + if sliding_window is not None and sliding_window <= 0: + raise ValueError("sliding_window must be > 0") B, N = anchor_positions.shape Q_LEN = N * block_size KV_LEN = S + N * block_size @@ -100,6 +103,9 @@ def create_dflash_block_mask( ): """Construct a full or sliding Flex Attention mask for DFlash training.""" + if sliding_window is not None and sliding_window <= 0: + raise ValueError("sliding_window must be > 0") + def dflash_mask_mod(b, h, q_idx, kv_idx): q_block_id = q_idx // block_size q_block_offset = q_idx % block_size diff --git a/tests/test_modeling/test_flex_attention_backend.py b/tests/test_modeling/test_flex_attention_backend.py index 13d77b8c9..e2cdff95b 100644 --- a/tests/test_modeling/test_flex_attention_backend.py +++ b/tests/test_modeling/test_flex_attention_backend.py @@ -6,13 +6,155 @@ from torch.nn.attention.flex_attention import flex_attention from transformers import Qwen3Config -from specforge.algorithms.common.dflash_family_model import create_dflash_block_mask -from specforge.modeling.draft.dflash import Qwen3DFlashAttention +from specforge.algorithms.common.dflash_family_model import ( + create_dflash_block_mask, + create_dflash_sdpa_mask, +) +from specforge.modeling.draft.dflash import DFlashDraftModel, Qwen3DFlashAttention from specforge.modeling.draft.dflash_kernels import DEFAULT_DFLASH_KERNELS from specforge.modeling.draft.flex_attention_backend import flex_attention_backend class FlexAttentionBackendTest(unittest.TestCase): + @unittest.skipUnless(torch.cuda.is_available(), "FlexAttention requires CUDA") + def test_sliding_block_mask_matches_sdpa(self): + torch.manual_seed(0) + device = torch.device("cuda") + dtype = torch.bfloat16 + context_len, draft_block_size = 16, 4 + anchors = torch.tensor([[8, 12]], device=device) + keep_blocks = torch.ones(1, 2, dtype=torch.bool, device=device) + query_len = anchors.shape[1] * draft_block_size + kv_len = context_len + query_len + query = torch.randn(1, 2, query_len, 64, device=device, dtype=dtype) + key = torch.randn(1, 2, kv_len, 64, device=device, dtype=dtype) + value = torch.randn(1, 2, kv_len, 64, device=device, dtype=dtype) + + block_mask = create_dflash_block_mask( + anchor_positions=anchors, + block_keep_mask=keep_blocks, + S=context_len, + block_size=draft_block_size, + device=device, + sliding_window=8, + ) + dense_mask = create_dflash_sdpa_mask( + anchor_positions=anchors, + block_keep_mask=keep_blocks, + S=context_len, + block_size=draft_block_size, + device=device, + sliding_window=8, + ) + compiled_attention = torch.compile( + lambda q, k, v, mask: flex_attention( + q, + k, + v, + block_mask=mask, + ), + fullgraph=True, + ) + + flex_output = compiled_attention(query, key, value, block_mask) + sdpa_output = torch.nn.functional.scaled_dot_product_attention( + query, + key, + value, + attn_mask=dense_mask, + ) + torch.testing.assert_close( + flex_output, + sdpa_output, + atol=3e-3, + rtol=2e-2, + ) + + def test_mixed_layer_types_select_their_own_masks(self): + config = Qwen3Config( + hidden_size=16, + intermediate_size=32, + num_attention_heads=2, + num_key_value_heads=1, + num_hidden_layers=2, + num_target_layers=4, + head_dim=8, + layer_types=["sliding_attention", "full_attention"], + use_sliding_window=True, + sliding_window=8, + attention_dropout=0.0, + block_size=2, + dflash_config={"target_layer_ids": [1, 2]}, + ) + config._attn_implementation = "eager" + model = DFlashDraftModel(config) + sliding_mask = torch.ones(1, 1, 2, 5, dtype=torch.bool) + full_mask = torch.ones(1, 1, 2, 5, dtype=torch.bool) + + for layer in model.layers: + layer.self_attn.forward = mock.Mock( + side_effect=lambda hidden_states, **kwargs: (hidden_states, None) + ) + + model( + position_ids=torch.arange(5).unsqueeze(0), + noise_embedding=torch.randn(1, 2, config.hidden_size), + target_hidden=torch.randn(1, 3, 2 * config.hidden_size), + attention_mask={ + "sliding_attention": sliding_mask, + "full_attention": full_mask, + }, + ) + + self.assertIs( + model.layers[0].self_attn.forward.call_args.kwargs["attention_mask"], + sliding_mask, + ) + self.assertIs( + model.layers[1].self_attn.forward.call_args.kwargs["attention_mask"], + full_mask, + ) + + def test_eager_converts_boolean_mask_to_additive_mask(self): + config = Qwen3Config( + hidden_size=8, + intermediate_size=16, + num_attention_heads=1, + num_key_value_heads=1, + num_hidden_layers=1, + head_dim=8, + layer_types=["sliding_attention"], + sliding_window=8, + attention_dropout=0.0, + ) + config._attn_implementation = "eager" + attention = Qwen3DFlashAttention( + config, + layer_idx=0, + kernels=DEFAULT_DFLASH_KERNELS, + ) + boolean_mask = torch.tensor([[[[True, False]]]]) + cos = torch.ones(1, 2, config.head_dim) + sin = torch.zeros_like(cos) + + with mock.patch( + "specforge.modeling.draft.dflash.eager_attention_forward", + return_value=(torch.zeros(1, 1, 1, config.head_dim), None), + ) as eager: + attention( + hidden_states=torch.randn(1, 1, config.hidden_size), + target_hidden=torch.randn(1, 1, config.hidden_size), + position_embeddings=(cos, sin), + attention_mask=boolean_mask, + ) + + additive_mask = eager.call_args.args[4] + self.assertEqual(additive_mask[0, 0, 0, 0].item(), 0.0) + self.assertEqual( + additive_mask[0, 0, 0, 1].item(), + torch.finfo(additive_mask.dtype).min, + ) + # This correctness regression test can be deleted when we require # torch>=2.13; it tests the Torch 2.11 Inductor monkeypatch for CuteDSL # operations in patch_inductor_cutedsl_lowerings(). @@ -69,6 +211,7 @@ def run_backend(backend, flex_block_size=None): block_size=draft_block_size, device=device, flex_block_size=flex_block_size, + sliding_window=128, ) compiled_attention = torch.compile( diff --git a/tests/test_utils/test_dflash_mask.py b/tests/test_utils/test_dflash_mask.py index bdcd13a69..ab46def04 100644 --- a/tests/test_utils/test_dflash_mask.py +++ b/tests/test_utils/test_dflash_mask.py @@ -1,8 +1,14 @@ import unittest +from types import SimpleNamespace +from unittest import mock import torch +from torch import nn from specforge.algorithms.common.dflash_family_model import ( + OnlineDFlashModel, + OnlineDominoModel, + OnlineDSparkModel, create_dflash_block_mask, create_dflash_sdpa_mask, ) @@ -28,6 +34,7 @@ def _reference_dflash_mask( for b in range(B): for q_idx in range(Q_LEN): q_block_id = q_idx // block_size + q_offset = q_idx % block_size anchor_pos = anchor_positions[b, q_block_id].item() is_valid = block_keep_mask[b, q_block_id].item() if not is_valid: @@ -53,6 +60,21 @@ def _reference_dflash_mask( return mask +class _RecordingDraftModel(nn.Module): + def __init__(self): + super().__init__() + self.config = SimpleNamespace( + layer_types=["sliding_attention", "full_attention"], + sliding_window=8, + ) + self.sliding_window = 8 + self.attention_mask = None + + def forward(self, noise_embedding, attention_mask, **kwargs): + self.attention_mask = attention_mask + return noise_embedding + + class TestDFlashMask(unittest.TestCase): def setUp(self): @@ -108,6 +130,7 @@ def _compare_block_mask_consistency( block_keep_mask, S, block_size, + sliding_window=None, ): """Verify create_dflash_block_mask block-level mask is consistent with reference.""" anchor_positions = anchor_positions.to(self.device) @@ -119,6 +142,7 @@ def _compare_block_mask_consistency( S=S, block_size=block_size, device=self.device, + sliding_window=sliding_window, ) ref_mask = _reference_dflash_mask( @@ -127,6 +151,7 @@ def _compare_block_mask_consistency( S=S, block_size=block_size, device=self.device, + sliding_window=sliding_window, ) dense_blocks = block_mask.to_dense() # (B, H, Q_blocks, KV_blocks) @@ -242,6 +267,62 @@ def test_sliding_window_one_has_no_context_and_causal_draft(self): sliding_window=1, ) + def test_sliding_window_block_mask_consistency(self): + anchor_positions = torch.tensor([[12, 24]]) + block_keep_mask = torch.tensor([[True, True]]) + self._compare_block_mask_consistency( + anchor_positions, + block_keep_mask, + S=32, + block_size=4, + sliding_window=8, + ) + + def test_invalid_sliding_window(self): + anchor_positions = torch.tensor([[12]], device=self.device) + block_keep_mask = torch.tensor([[True]], device=self.device) + for factory in (create_dflash_sdpa_mask, create_dflash_block_mask): + with self.subTest(factory=factory.__name__): + with self.assertRaisesRegex(ValueError, "sliding_window must be > 0"): + factory( + anchor_positions=anchor_positions, + block_keep_mask=block_keep_mask, + S=16, + block_size=4, + device=self.device, + sliding_window=0, + ) + + def test_all_dflash_families_build_mixed_layer_masks(self): + anchors = torch.tensor([[12]], device=self.device) + keep = torch.tensor([[True]], device=self.device) + for model_class in (OnlineDFlashModel, OnlineDominoModel, OnlineDSparkModel): + with self.subTest(model_class=model_class.__name__): + draft_model = _RecordingDraftModel().to(self.device) + model = model_class( + draft_model=draft_model, + target_lm_head=nn.Identity(), + target_embed_tokens=nn.Embedding(32, 8).to(self.device), + mask_token_id=31, + block_size=4, + attention_backend="sdpa", + ) + with mock.patch.object( + model, + "_sample_anchor_positions", + return_value=(anchors, keep), + ): + model._forward_draft_blocks( + input_ids=torch.arange(16, device=self.device).unsqueeze(0), + hidden_states=torch.randn(1, 16, 8, device=self.device), + loss_mask=torch.ones(1, 16, device=self.device), + ) + + masks = draft_model.attention_mask + self.assertEqual(set(masks), {"full_attention", "sliding_attention"}) + self.assertTrue(masks["full_attention"][0, 0, 0, 0].item()) + self.assertFalse(masks["sliding_attention"][0, 0, 0, 0].item()) + def test_mixed_validity_multi_batch(self): """Multi-batch with mixed block validity patterns.""" anchor_positions = torch.tensor([[10, 40, 70, 100], [20, 50, 80, 110]]) From 19836f807503de0ae43bbef099c3cc2a685271bb Mon Sep 17 00:00:00 2001 From: wjp666666 <1969554248@qq.com> Date: Tue, 4 Aug 2026 16:49:54 +0000 Subject: [PATCH 43/88] update lmsys blog link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 88d354822..e716825a9 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ SpecBundle is a collection of production-grade speculative decoding models that ## 🎉 News -- [2026-08] 🎉 Released SpecBundle (phase 2) and SpecForge v0.3.0. Check out our blog at [LMSYS.org](https://www.lmsys.org/blog) +- [2026-08] 🎉 Released SpecBundle (phase 2) and SpecForge v0.3.0. Check out our blog at [LMSYS.org](https://www.lmsys.org/blog/2026-08-04-specforge-v0-3) - [2026-07] 🚀 Day0 supported two flagship dspark draft model, [Inklink](https://huggingface.co/RadixArk/Inkling-DSpark-Preview) and [Kimi-K3](https://huggingface.co/RadixArk/Kimi-K3-DSpark). - [2026-07] 🔥 Supported full disaggregation of training and inference in online training. - [2026-07] 🔥 Added DSpark online training for DFlash draft models. From a8393c76cb7c469f22cc4577a7b087cb23a679be Mon Sep 17 00:00:00 2001 From: Curnane Date: Wed, 5 Aug 2026 11:15:06 +0800 Subject: [PATCH 44/88] fix(data): attach record images in regenerate_train_data for VLM prompts --- scripts/regenerate_train_data.py | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/scripts/regenerate_train_data.py b/scripts/regenerate_train_data.py index f2f737fd2..252c989b2 100644 --- a/scripts/regenerate_train_data.py +++ b/scripts/regenerate_train_data.py @@ -244,6 +244,29 @@ def build_query_kwargs(args, messages, max_tokens=None): return query_kwargs +def _extract_record_images(data: Dict[str, Any]) -> List[str]: + """Resolve image references of a record (``image``/``image_path`` string or + ``images`` list), in insertion order.""" + refs: List[str] = [] + single = data.get("image") or data.get("image_path") + if isinstance(single, str): + refs.append(single) + images = data.get("images") + if isinstance(images, list): + refs.extend(r for r in images if isinstance(r, str)) + return refs + + +def _image_url_part(path: str) -> Dict[str, Any]: + import base64 + import mimetypes + + mime = mimetypes.guess_type(path)[0] or "image/jpeg" + with open(path, "rb") as f: + b64 = base64.b64encode(f.read()).decode("ascii") + return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}} + + def call_sglang( args, server_address: str, @@ -260,6 +283,8 @@ def call_sglang( messages = data["conversations"] regenerated_messages = [] + record_images = _extract_record_images(data) + image_attached = False # ignore data which starts with an assistant message if messages[0]["role"] == "assistant": @@ -273,6 +298,26 @@ def call_sglang( elif message["role"] == "assistant": continue elif message["role"] == "user": + # Multimodal records: attach the record's images to the first user + # turn that carries the placeholder (OpenAI content parts). + content = message.get("content") + if ( + record_images + and not image_attached + and isinstance(content, str) + and "" in content + ): + try: + parts = [_image_url_part(p) for p in record_images] + except OSError as exc: + data["status"] = "error" + data["error"] = f"unreadable image file: {exc}" + return data + text = content.replace("\n", "").replace("", "") + parts.append({"type": "text", "text": text}) + message = dict(message) + message["content"] = parts + image_attached = True regenerated_messages.append(message) query_kwargs = build_query_kwargs(args, regenerated_messages, max_tokens) From eaaf37ffd29be3e71ddda91b9fb3a237bf9139f4 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 45/88] docs: add ROCm known-issues notes to installation guide Document three ROCm-specific gotchas under the existing AMD ROCm install section: the torchvision VideoReader import error from newer datasets, the Hugging Face token requirement for gated target models, and the harmless torch._dynamo / FSDP warnings seen during training. Also note the --no-deps install flow so a ROCm torch wheel is not replaced by the CUDA torch pinned in pyproject.toml. yunchang and the distributed layer need no ROCm changes: main already lazy-imports yunchang inside init_distributed, and yunchang imports cleanly on ROCm without a CUDA flash-attn build, so the single-GPU / data-parallel path works as-is. --- docs/get_started/installation.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/get_started/installation.md b/docs/get_started/installation.md index f920b1a00..e558bf094 100644 --- a/docs/get_started/installation.md +++ b/docs/get_started/installation.md @@ -49,6 +49,23 @@ ROCm-compatible SGLang capture service; offline feature consumers can start without target inference. PyTorch exposes ROCm accelerators through its `torch.cuda` API and uses NCCL for distributed runs. +If your ROCm `torch` build is pulled from a separate index, install SpecForge +without dependencies (`pip install --no-deps -e .`) so the ROCm wheel is not +silently replaced by the CUDA `torch` pinned in `pyproject.toml`. + +Known issues on ROCm: + +- `ImportError: cannot import name 'VideoReader' from 'torchvision.io'` — newer + `datasets` tensorizes through torchvision video support that some ROCm + torchvision builds omit. Install a torchvision build with video support or pin + an older `datasets`. +- Gated target models (e.g. `meta-llama/*`) require a Hugging Face token; + otherwise embedding loading fails with `403 Forbidden`. Log in with + `huggingface-cli login`, or use an already-cached model with `HF_HUB_OFFLINE=1`. +- Harmless `torch._dynamo` "compilation metrics ... not JSON serializable" + warnings and FSDP deprecation `FutureWarning`s may appear during training; they + do not affect the run. + ### Ascend NPU Install the vendor-matched PyTorch and `torch_npu` packages first, then install From ac075b0636e2fff53426fc0235187194ba3258ad Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 46/88] fix: update Python version requirement in pyproject.toml Change the minimum required Python version from 3.11 to 3.10 in the pyproject.toml file to ensure compatibility with a broader range of environments. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9d8097322..46980d255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "specforge" dynamic = ["version"] readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.10" description = "SpecForge: Speculative Decoding Training Framework" authors = [{name = "SGLang Team"}] urls = {Homepage = "https://github.com/sgl-project/SpecForge"} From b78865737694d9d48a785441aada776e2d3077b9 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 47/88] fix: update exception handling in Trainer class Replace deprecated sys.exception() with sys.exc_info()[1] to correctly capture the primary exception during cleanup in the Trainer class. This change ensures proper error handling and improves the robustness of the training process. --- specforge/training/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specforge/training/trainer.py b/specforge/training/trainer.py index b1279a708..e2753914d 100644 --- a/specforge/training/trainer.py +++ b/specforge/training/trainer.py @@ -539,7 +539,7 @@ def close_loader() -> None: self._on_fit_failure(exc) raise finally: - primary_exception = sys.exception() + primary_exception = sys.exc_info()[1] cleanup_errors: list[tuple[str, BaseException]] = [] def capture_cleanup(label: str, action) -> None: From a8c09931494616060949eff22b6ba00c65cee521 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 48/88] feat: enhance spec_capture_sink with dynamic pinning configuration Add a new file for the spec_capture_sink and implement logic to dynamically set the pinning configuration based on the available ROCm build. This ensures compatibility with both newer and older builds by checking for the presence of `with_hard_pin` or falling back to `with_soft_pin` as needed. --- patches/sglang/v0.5.14/spec-capture.patch | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/patches/sglang/v0.5.14/spec-capture.patch b/patches/sglang/v0.5.14/spec-capture.patch index a1081ba3d..ac839120e 100644 --- a/patches/sglang/v0.5.14/spec-capture.patch +++ b/patches/sglang/v0.5.14/spec-capture.patch @@ -363,7 +363,7 @@ new file mode 100644 index 000000000..d317b2801 --- /dev/null +++ b/python/sglang/srt/spec_capture_sink.py -@@ -0,0 +1,243 @@ +@@ -0,0 +1,249 @@ +# Copyright 2024 SGLang Team +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. @@ -471,7 +471,13 @@ index 000000000..d317b2801 + # before the trainer consumes it. + cfg = ReplicateConfig() + cfg.replica_num = 1 -+ cfg.with_hard_pin = True ++ # `with_hard_pin` exists only on newer Mooncake builds; older ROCm ++ # sglang images expose only `with_soft_pin`. Map the hard-pin intent ++ # onto whatever the installed build supports. ++ if hasattr(cfg, "with_hard_pin"): ++ cfg.with_hard_pin = True ++ elif hasattr(cfg, "with_soft_pin"): ++ cfg.with_soft_pin = True + self._put_config = cfg + self._store = store + logger.info("spec-capture mooncake sink connected") From 2b43be93935c44b08fb7f01eec892e6a6bbaf507 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 49/88] docs: rewrite ROCm installation for container-based sglang v0.5.14 flow --- docs/get_started/installation.md | 92 +++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/docs/get_started/installation.md b/docs/get_started/installation.md index e558bf094..ff8baa5e2 100644 --- a/docs/get_started/installation.md +++ b/docs/get_started/installation.md @@ -35,36 +35,76 @@ the same `specforge train` entry. ### AMD ROCm -For the pinned ROCm environment, install the checked-in requirements before the -package: +On ROCm, install SpecForge into an environment that already provides a ROCm +PyTorch and a ROCm SGLang, and install the package **without dependencies** so +pip does not pull CUDA wheels over the working ROCm stack. + +The recommended base is an official SGLang ROCm release container. These ship a +ROCm PyTorch and an editable ROCm SGLang build, so SpecForge only needs to be +cloned and installed on top. + +#### Step 1: Pull the image for your accelerator + +The accelerator is baked into the tag, so use the image that matches your +hardware: + +```bash +# AMD Instinct MI300X (gfx942) +docker pull lmsysorg/sglang:v0.5.14-rocm720-mi30x + +# AMD Instinct MI355X (gfx950) +docker pull lmsysorg/sglang:v0.5.14-rocm700-mi35x +``` + +#### Step 2: Start the container + +Expose the ROCm device nodes (swap in the tag for your accelerator). Use `--name` +and omit `--rm` so the checkout survives across sessions: + +```bash +docker run -it --name specforge \ + --device=/dev/kfd --device=/dev/dri \ + --group-add video --cap-add SYS_PTRACE --security-opt seccomp=unconfined \ + --ipc=host --shm-size=16g \ + lmsysorg/sglang:v0.5.14-rocm720-mi30x \ + bash +``` + +Re-enter the running container later with `docker exec -it specforge bash`. + +#### Step 3: Clone and install SpecForge + +Inside the container, clone SpecForge into `/workspace/SpecForge` and register it +in editable mode without touching the image's torch/sglang: + +```bash +git clone https://github.com/sgl-project/SpecForge.git /workspace/SpecForge +cd /workspace/SpecForge +python -m pip install -e . --no-deps +``` + +#### Step 4: Apply the capture patch (online runs only) + +These images pin SGLang to exactly `0.5.14` (editable at `/sgl-workspace/sglang`), +so the online capture patch applies with a plain `git apply`. Skip this step for +offline training, which reads features from disk and needs no capture service: ```bash -python -m pip install -r requirements-rocm.txt -python -m pip install -e . +cd /sgl-workspace/sglang +git apply /workspace/SpecForge/patches/sglang/v0.5.14/spec-capture.patch ``` -The file pins a ROCm 7.2 PyTorch stack. Use a wheel index and driver combination -compatible with the host if your ROCm version differs. Online runs require a -ROCm-compatible SGLang capture service; offline feature consumers can start -without target inference. PyTorch exposes ROCm accelerators through its -`torch.cuda` API and uses NCCL for distributed runs. - -If your ROCm `torch` build is pulled from a separate index, install SpecForge -without dependencies (`pip install --no-deps -e .`) so the ROCm wheel is not -silently replaced by the CUDA `torch` pinned in `pyproject.toml`. - -Known issues on ROCm: - -- `ImportError: cannot import name 'VideoReader' from 'torchvision.io'` — newer - `datasets` tensorizes through torchvision video support that some ROCm - torchvision builds omit. Install a torchvision build with video support or pin - an older `datasets`. -- Gated target models (e.g. `meta-llama/*`) require a Hugging Face token; - otherwise embedding loading fails with `403 Forbidden`. Log in with - `huggingface-cli login`, or use an already-cached model with `HF_HUB_OFFLINE=1`. -- Harmless `torch._dynamo` "compilation metrics ... not JSON serializable" - warnings and FSDP deprecation `FutureWarning`s may appear during training; they - do not affect the run. +#### Step 5: Run training + +Use the `sdpa` or `flex_attention` attention backends on ROCm. The `fa` +(flash-attn) and `usp` backends, and `yunchang`-based Ulysses/Ring sequence +parallel (`sp_ulysses_size` / `sp_ring_size` > 1), depend on a CUDA flash-attn +build; the single-GPU / data-parallel path never loads `yunchang`, and selecting +those backends raises a clear error. The checked-in +[`qwen3-8b-eagle3-offline.yaml`](../../examples/configs/qwen3-8b-eagle3-offline.yaml) +recipe already uses `flex_attention`, so it runs on ROCm unchanged as a +single-GPU offline EAGLE3 example; launch it with +`specforge train --config examples/configs/qwen3-8b-eagle3-offline.yaml`. ### Ascend NPU From 33b5baa6fa7a998958366d036a863534b0fcdff2 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 50/88] rocm: map hard-pin onto with_soft_pin on older Mooncake builds Older ROCm Mooncake images expose only ReplicateConfig.with_soft_pin; newer builds add with_hard_pin. Guard the assignment with hasattr so the online feature store attaches on both instead of raising AttributeError on the first put(). --- specforge/runtime/data_plane/mooncake_store.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specforge/runtime/data_plane/mooncake_store.py b/specforge/runtime/data_plane/mooncake_store.py index ac3e9cdcc..c50c71ba6 100644 --- a/specforge/runtime/data_plane/mooncake_store.py +++ b/specforge/runtime/data_plane/mooncake_store.py @@ -209,7 +209,13 @@ def __init__( _require_store_api(store) self._store = store put_config.replica_num = replica_num - put_config.with_hard_pin = hard_pin + # `with_hard_pin` exists only on newer Mooncake builds; older ROCm + # images expose only `with_soft_pin`. Map the hard-pin intent onto + # whichever pin the installed build supports. + if hasattr(put_config, "with_hard_pin"): + put_config.with_hard_pin = hard_pin + elif hasattr(put_config, "with_soft_pin"): + put_config.with_soft_pin = hard_pin self._put_config = put_config self.max_resident_bytes = max_resident_bytes self.max_hold_age_s = max_hold_age_s From 8a3bcdc3ea4e395e6162c0515acf56dacfa665f2 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 51/88] managed_local: expose Mooncake default_kv_lease_ttl to fix teardown drain managed_local auto-launches mooncake_master without --default_kv_lease_ttl, so Mooncake's stock 5000ms key lease outlives the consumer's ~1.75s teardown drain window and shutdown fails with "could not drain N pending removal(s)". Add a default_kv_lease_ttl_ms knob (default 500ms; null inherits Mooncake's default) forwarded to the owned master command. --- examples/configs/README.md | 1 + specforge/config/schema.py | 6 ++++++ specforge/launch_plan.py | 5 +++++ tests/test_runtime/test_launch_plan.py | 1 + 4 files changed, 13 insertions(+) diff --git a/examples/configs/README.md b/examples/configs/README.md index 936efa226..80a08a47f 100644 --- a/examples/configs/README.md +++ b/examples/configs/README.md @@ -351,6 +351,7 @@ Managed-local fields: | `deployment.disaggregated.managed_local.mooncake.global_segment_size_bytes` | `34359738368` | Owned global segment size. | | `deployment.disaggregated.managed_local.mooncake.local_buffer_size_bytes` | `1073741824` | Owned local client buffer. | | `deployment.disaggregated.managed_local.mooncake.startup_timeout_s` | `60` | Positive Mooncake readiness timeout. | +| `deployment.disaggregated.managed_local.mooncake.default_kv_lease_ttl_ms` | `500` | Master key-lease TTL (ms) forwarded to `mooncake_master --default_kv_lease_ttl`. Kept below the consumer's teardown drain window so managed_local shuts down cleanly; set `null` to inherit Mooncake's stock default. | | `deployment.disaggregated.managed_local.capture_servers[].port` | required | Unique capture HTTP port. | | `deployment.disaggregated.managed_local.capture_servers[].cuda_visible_devices` | required | Device tokens for this server. Their count must equal its `tp_size`. | | `deployment.disaggregated.managed_local.capture_servers[].tp_size` | `1` | Target-model tensor parallelism for this server. | diff --git a/specforge/config/schema.py b/specforge/config/schema.py index 05686eda6..ceb42d0ca 100644 --- a/specforge/config/schema.py +++ b/specforge/config/schema.py @@ -287,6 +287,12 @@ class ManagedLocalMooncakeConfig(StrictConfigModel): global_segment_size_bytes: int = Field(default=32 << 30, gt=0) local_buffer_size_bytes: int = Field(default=1 << 30, gt=0) startup_timeout_s: float = Field(default=60.0, gt=0) + #: Master key-lease TTL (ms) forwarded to ``mooncake_master + #: --default_kv_lease_ttl``. The consumer's teardown drain retries for only + #: ~1.75s, so Mooncake's stock 5000ms lease leaves keys pinned past the + #: drain window and fails shutdown. Default to a value below that window so + #: managed_local tears down cleanly; set null to inherit Mooncake's default. + default_kv_lease_ttl_ms: Optional[int] = Field(default=500, gt=0) @model_validator(mode="after") def _validate_endpoint(self): diff --git a/specforge/launch_plan.py b/specforge/launch_plan.py index c9444ad78..f339dabe1 100644 --- a/specforge/launch_plan.py +++ b/specforge/launch_plan.py @@ -416,6 +416,11 @@ def _managed_local_services( f"--rpc_port={mooncake.rpc_port}", f"--http_metadata_server_port={mooncake.metadata_port}", f"--metrics_port={mooncake.metrics_port}", + *( + (f"--default_kv_lease_ttl={mooncake.default_kv_lease_ttl_ms}",) + if mooncake.default_kv_lease_ttl_ms is not None + else () + ), ), {"CUDA_VISIBLE_DEVICES": ""}, ), diff --git a/tests/test_runtime/test_launch_plan.py b/tests/test_runtime/test_launch_plan.py index e6637a8df..0d587315b 100644 --- a/tests/test_runtime/test_launch_plan.py +++ b/tests/test_runtime/test_launch_plan.py @@ -667,6 +667,7 @@ def test_managed_local_plan_owns_mooncake_and_multiple_capture_servers(self): "--rpc_port=35551", "--http_metadata_server_port=35880", "--metrics_port=35903", + "--default_kv_lease_ttl=500", ), ) self.assertEqual(mooncake.readiness.kind, "mooncake") From 0790275125c3231c6b0972becc0157ddcd32d402 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 52/88] docs: add dedicated AMD ROCm tutorial and link from installation guide --- docs/get_started/amd_rocm.md | 348 +++++++++++++++++++++++++++++++ docs/get_started/installation.md | 69 +----- docs/index.rst | 1 + 3 files changed, 356 insertions(+), 62 deletions(-) create mode 100644 docs/get_started/amd_rocm.md diff --git a/docs/get_started/amd_rocm.md b/docs/get_started/amd_rocm.md new file mode 100644 index 000000000..62e1df4be --- /dev/null +++ b/docs/get_started/amd_rocm.md @@ -0,0 +1,348 @@ +# 🚀 AMD ROCm Tutorial + +This is an end-to-end tutorial for running SpecForge on AMD Instinct GPUs +(ROCm). It walks through the complete flow: **installation → data preparation → +offline training → online training → disaggregated training**. + +All commands assume a ROCm host with the AMD driver stack and Docker already +installed. Validated on MI300X (gfx942) and MI355X (gfx950). + +--- + +## 1. Installation + +On ROCm, install SpecForge into an environment that already provides a ROCm +PyTorch and a ROCm SGLang, and install the package **without dependencies** so +pip does not pull CUDA wheels over the working ROCm stack. + +The recommended base is an official SGLang ROCm release container. These ship a +ROCm PyTorch and an editable ROCm SGLang build, so SpecForge only needs to be +cloned and installed on top. + +### Step 1: Pull the image for your accelerator + +The accelerator is baked into the tag, so use the image that matches your +hardware: + +```bash +# AMD Instinct MI300X (gfx942) +docker pull lmsysorg/sglang:v0.5.14-rocm720-mi30x + +# AMD Instinct MI355X (gfx950) +docker pull lmsysorg/sglang:v0.5.14-rocm700-mi35x +``` + +### Step 2: Start the container + +Expose the ROCm device nodes (swap in the tag for your accelerator). Use `--name` +and omit `--rm` so the checkout survives across sessions: + +```bash +docker run -it --name specforge \ + --device=/dev/kfd --device=/dev/dri \ + --group-add video --cap-add SYS_PTRACE --security-opt seccomp=unconfined \ + --ipc=host --shm-size=16g \ + lmsysorg/sglang:v0.5.14-rocm720-mi30x \ + bash +``` + +`--device=/dev/kfd --device=/dev/dri --group-add video` are required for ROCm +GPU access; `--ipc=host --shm-size=16g` gives Mooncake and PyTorch enough shared +memory. Re-enter the running container later with `docker exec -it specforge bash`. + +### Step 3: Clone and install SpecForge + +Inside the container, clone SpecForge into `/workspace/SpecForge` and register it +in editable mode without touching the image's torch/sglang: + +```bash +git clone https://github.com/sgl-project/SpecForge.git /workspace/SpecForge +cd /workspace/SpecForge +python -m pip install -e . --no-deps +``` + +`--no-deps` is mandatory: a full resolve pulls the CUDA SGLang stack and +clobbers the image's ROCm torch/sglang. If a later step reports a missing +lightweight dependency (for example `accelerate`), install just that package, +also with `--no-deps`. + +### Step 4: Apply the capture patch (online runs only) + +These images pin SGLang to exactly `0.5.14` (editable at `/sgl-workspace/sglang`), +so the online capture patch applies with a plain `git apply`. Skip this step for +offline training, which reads features from disk and needs no capture service: + +```bash +cd /sgl-workspace/sglang +git apply /workspace/SpecForge/patches/sglang/v0.5.14/spec-capture.patch +cd /workspace/SpecForge +``` + +The patch adds the `--enable-spec-capture`, `--spec-capture-method`, and +`--spec-capture-aux-layer-ids` server flags plus the `sglang.srt.spec_capture_sink` +module used by online capture. + +### Step 5: Attention backends on ROCm + +Use the `sdpa` or `flex_attention` attention backends on ROCm. The `fa` +(flash-attn) and `usp` backends, and `yunchang`-based Ulysses/Ring sequence +parallel (`sp_ulysses_size` / `sp_ring_size` > 1), depend on a CUDA flash-attn +build; the single-GPU / data-parallel path never loads `yunchang`, and selecting +those backends raises a clear error. The checked-in +[`qwen3-8b-eagle3-offline.yaml`](../../examples/configs/qwen3-8b-eagle3-offline.yaml) +recipe already uses `flex_attention`, so it runs on ROCm unchanged as a +single-GPU offline EAGLE3 example. + +--- + +## 2. Data preparation + +Data preparation is platform independent — the same scripts run on ROCm. Write a +ShareGPT training set into `cache/dataset` from the repository root: + +```bash +python scripts/prepare_data.py --dataset sharegpt +``` + +This produces `./cache/dataset/sharegpt_train.jsonl` in the stable +`id` + `conversations` contract used by every checked-in recipe. For the full +preset list, custom datasets, preformatted text, and target-model regeneration, +see the [Data Preparation](../basic_usage/data_preparation.md) guide. + +--- + +## 3. Offline training + +Offline training reads target features from disk, so the trainer only has to fit +the draft model. It uses more storage but keeps target inference out of the +training loop, and needs no capture patch or Mooncake. + +### Step 1: Capture hidden states + +Feature preparation is a data-processing step, not a second training entry point: + +```bash +torchrun --standalone --nproc_per_node 8 \ + scripts/prepare_hidden_states.py \ + --target-model-path Qwen/Qwen3-8B \ + --data-path ./cache/dataset/sharegpt_train.jsonl \ + --output-path ./cache/hidden_states/qwen3-8b-sharegpt \ + --chat-template qwen \ + --max-length 4096 \ + --tp-size 1 \ + --batch-size 32 +``` + +The output path matches `data.hidden_states_path` in the checked-in offline +recipe. See [Data Preparation](../basic_usage/data_preparation.md#option-2-pre-formatted-text-format) +for preformatted inputs and other options. + +### Step 2: Train + +The checked-in offline recipe already uses `flex_attention`, so it runs on ROCm +unchanged: + +```bash +specforge train --config examples/configs/qwen3-8b-eagle3-offline.yaml +``` + +Override any field inline without copying the YAML, e.g. a quick smoke run: + +```bash +specforge train --config examples/configs/qwen3-8b-eagle3-offline.yaml \ + training.max_steps=20 output_dir=./outputs/eagle3-offline-smoke +``` + +See the [Training](../basic_usage/training.md) guide for the full run schema, +checkpoint/resume rules, and evaluation. + +--- + +## 4. Online training + +Online training captures target features live from a patched SGLang server and +streams them through Mooncake to the trainer. Every online run is +**disaggregated**: a producer drives prompts through the capture server and a +consumer trains the draft model. With `deployment.trainer.nnodes: 1` and no +`--role`, a single `specforge train` command supervises both. + +This section uses the small +[`qwen2.5-0.5b-eagle3-online.yaml`](../../examples/configs/qwen2.5-0.5b-eagle3-online.yaml) +recipe as a single-node smoke test. Complete Step 4 of the installation first. + +### Step 1: One-time run inputs + +EAGLE3 disaggregated runs require an explicit **shared vocabulary mapping** (the +producer and consumer cannot each derive one). The recipe expects it at +`cache/vocab_mapping/qwen2.5-0.5b-eagle3.pt`: + +```bash +python - <<'PY' +from datasets import Dataset +from transformers import AutoTokenizer +from specforge.data.preprocessing import build_eagle3_dataset, generate_vocab_mapping_file +import json + +rows = [json.loads(l) for l in open("cache/dataset/sharegpt_train.jsonl")] +tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", trust_remote_code=True) +eds = build_eagle3_dataset(Dataset.from_list(rows), tok, + chat_template="qwen", max_length=512, num_proc=8) +path = generate_vocab_mapping_file(eds, target_vocab_size=151936, + draft_vocab_size=16000, cache_dir="cache/vocab_mapping", + cache_key="qwen2.5-0.5b-eagle3") +print("vocab mapping:", path) +PY +``` + +The consumer's target head loader reads the LM-head weight from a `*.index.json` +weight map. Qwen2.5-0.5B ships a single `model.safetensors` with **tied +embeddings** (no standalone `lm_head.weight`), so build a small local target +directory that adds an index and points at the tied weight: + +```bash +python - <<'PY' +import os, json, glob +from safetensors import safe_open + +snap = glob.glob(os.path.expanduser( + "~/.cache/huggingface/hub/models--Qwen--Qwen2.5-0.5B-Instruct/snapshots/*"))[0] +dst = "outputs/online-test/target_model" +os.makedirs(dst, exist_ok=True) +for f in os.listdir(snap): + if f.endswith(".index.json"): + continue + lnk = os.path.join(dst, f) + if os.path.lexists(lnk): + os.remove(lnk) + os.symlink(os.path.realpath(os.path.join(snap, f)), lnk) + +wm, total = {}, 0 +with safe_open(os.path.join(dst, "model.safetensors"), framework="pt") as fh: + for k in fh.keys(): + wm[k] = "model.safetensors" + n = 1 + for s in fh.get_slice(k).get_shape(): + n *= s + total += n * 2 +json.dump({"metadata": {"total_size": total}, "weight_map": wm}, + open(os.path.join(dst, "model.safetensors.index.json"), "w"), indent=2) +print("target model:", os.path.abspath(dst)) +PY +``` + +Larger sharded models (for example Qwen3-8B) already ship an index and a +standalone LM head, so they need neither of these workarounds. + +### Step 2: Start Mooncake and the capture server + +Start the Mooncake master. **Set `--default_kv_lease_ttl=500`**: the consumer's +teardown drain retries for only ~1.75s, so Mooncake's stock 5000ms lease leaves +keys pinned past the drain window and fails an otherwise successful shutdown. + +```bash +mooncake_master --enable_http_metadata_server=true \ + --rpc_port=35551 --http_metadata_server_port=35880 \ + --metrics_port=35903 --enable_metric_reporting=false \ + --default_kv_lease_ttl=500 & +``` + +Start the patched capture server on GPU 0. **The `--spec-capture-aux-layer-ids` +must match the layers the producer derives** for EAGLE3: +`[1, num_layers//2 - 1, num_layers - 4]`. Qwen2.5-0.5B has 24 layers, so the ids +are `1 11 20`. A mismatch produces zero features with no error. + +```bash +HIP_VISIBLE_DEVICES=0 CUDA_VISIBLE_DEVICES=0 \ +MOONCAKE_LOCAL_HOSTNAME=127.0.0.1 \ +MOONCAKE_METADATA_SERVER=http://127.0.0.1:35880/metadata \ +MOONCAKE_MASTER_SERVER_ADDR=127.0.0.1:35551 \ +MOONCAKE_PROTOCOL=tcp \ +MOONCAKE_GLOBAL_SEGMENT_SIZE=$((32<<30)) \ +python -m sglang.launch_server \ + --model-path Qwen/Qwen2.5-0.5B-Instruct \ + --trust-remote-code --skip-tokenizer-init \ + --tp-size 1 --context-length 2048 --mem-fraction-static 0.85 \ + --chunked-prefill-size -1 --disable-radix-cache \ + --enable-spec-capture --spec-capture-method eagle3 \ + --spec-capture-aux-layer-ids 1 11 20 \ + --host 127.0.0.1 --port 30000 & +``` + +Wait for `curl --fail http://127.0.0.1:30000/health` to return 200 (the first +health check can take a few minutes while attention kernels compile). +`--context-length` must exceed `data.max_length` (512) or `/generate` returns +`400 input longer than context length`. + +### Step 3: Launch training + +One command supervises producer and consumer on GPU 1: + +```bash +CUDA_VISIBLE_DEVICES=1 HIP_VISIBLE_DEVICES=1 \ +MOONCAKE_LOCAL_HOSTNAME=127.0.0.1 \ +MOONCAKE_METADATA_SERVER=http://127.0.0.1:35880/metadata \ +MOONCAKE_MASTER_SERVER_ADDR=127.0.0.1:35551 \ +MOONCAKE_PROTOCOL=tcp \ +MOONCAKE_GLOBAL_SEGMENT_SIZE=$((32<<30)) \ +specforge train -c examples/configs/qwen2.5-0.5b-eagle3-online.yaml \ + model.target_model_path=outputs/online-test/target_model \ + model.lm_head_key=model.embed_tokens.weight \ + training.max_steps=20 training.num_epochs=1 \ + training.save_interval=20 training.log_interval=5 +``` + +Before rerunning, clear stale control state: +`rm -rf outputs/qwen2.5-0.5b-eagle3-online`. + +### Success criteria + +- Producer log: `drive_producer returning produced= prompts_failed=0`. +- Consumer log: `step N: {...loss..., acceptance_rate...}` lines, and **no** + `could not drain` error or traceback at teardown. +- Checkpoint: `outputs/qwen2.5-0.5b-eagle3-online/qwen2.5-0.5b-eagle3-online-step20/` + contains `training_state.pt` and `training_state_rank0.pt`. + +> If `produced=0`, the capture aux-layer ids do not match the producer contract +> (see Step 2). If training succeeds but teardown reports `could not drain`, the +> Mooncake lease TTL is above the drain window (see Step 2). + +### Managed-local shortcut + +Instead of starting Mooncake and the capture server by hand, a +`deployment.disaggregated.managed_local` block lets one `specforge train` +command own those local processes and derive their endpoints. It defaults +`default_kv_lease_ttl_ms` to 500, so the lease-TTL fix is applied automatically. +See [Multi-server capture](../basic_usage/disaggregated_training.md#multi-server-capture) +for the managed-local profile. + +--- + +## 5. Disaggregated training + +Online training is already a disaggregated producer/consumer topology; Section 4 +runs both roles under one single-node supervisor. To split the roles across +process pools or nodes, use the **same config** with an explicit `--role`: + +```bash +# Inference / capture pool +specforge train -c examples/configs/qwen2.5-0.5b-eagle3-online.yaml --role producer + +# Trainer pool +specforge train -c examples/configs/qwen2.5-0.5b-eagle3-online.yaml --role consumer +``` + +For multiple consumer nodes, record `deployment.trainer.nnodes`, +`nproc_per_node`, `master_addr`, and `master_port` once in the config, then pass +only the node-local identity on each trainer host: + +```bash +specforge train -c run.yaml --role consumer --node-rank 0 # trainer-0 +specforge train -c run.yaml --role consumer --node-rank 1 # trainer-1 +``` + +A fresh attempt requires fresh control and consumer-state directories, and every +capture server must use the same target model, revision, capture method, and +auxiliary layer ids. Offline features can also be served through a disaggregated +shared-directory or Mooncake store. For external-service prerequisites, +freshness rules, multi-server capture, and resume, see the +[Disaggregated training](../basic_usage/disaggregated_training.md) guide. diff --git a/docs/get_started/installation.md b/docs/get_started/installation.md index ff8baa5e2..744b6424b 100644 --- a/docs/get_started/installation.md +++ b/docs/get_started/installation.md @@ -36,75 +36,20 @@ the same `specforge train` entry. ### AMD ROCm On ROCm, install SpecForge into an environment that already provides a ROCm -PyTorch and a ROCm SGLang, and install the package **without dependencies** so -pip does not pull CUDA wheels over the working ROCm stack. - -The recommended base is an official SGLang ROCm release container. These ship a -ROCm PyTorch and an editable ROCm SGLang build, so SpecForge only needs to be -cloned and installed on top. - -#### Step 1: Pull the image for your accelerator - -The accelerator is baked into the tag, so use the image that matches your -hardware: - -```bash -# AMD Instinct MI300X (gfx942) -docker pull lmsysorg/sglang:v0.5.14-rocm720-mi30x - -# AMD Instinct MI355X (gfx950) -docker pull lmsysorg/sglang:v0.5.14-rocm700-mi35x -``` - -#### Step 2: Start the container - -Expose the ROCm device nodes (swap in the tag for your accelerator). Use `--name` -and omit `--rm` so the checkout survives across sessions: - -```bash -docker run -it --name specforge \ - --device=/dev/kfd --device=/dev/dri \ - --group-add video --cap-add SYS_PTRACE --security-opt seccomp=unconfined \ - --ipc=host --shm-size=16g \ - lmsysorg/sglang:v0.5.14-rocm720-mi30x \ - bash -``` - -Re-enter the running container later with `docker exec -it specforge bash`. - -#### Step 3: Clone and install SpecForge - -Inside the container, clone SpecForge into `/workspace/SpecForge` and register it -in editable mode without touching the image's torch/sglang: +PyTorch and a ROCm SGLang (an official SGLang ROCm release container is the +recommended base), and install the package **without dependencies** so pip does +not pull CUDA wheels over the working ROCm stack: ```bash +# Inside the ROCm SGLang container git clone https://github.com/sgl-project/SpecForge.git /workspace/SpecForge cd /workspace/SpecForge python -m pip install -e . --no-deps ``` -#### Step 4: Apply the capture patch (online runs only) - -These images pin SGLang to exactly `0.5.14` (editable at `/sgl-workspace/sglang`), -so the online capture patch applies with a plain `git apply`. Skip this step for -offline training, which reads features from disk and needs no capture service: - -```bash -cd /sgl-workspace/sglang -git apply /workspace/SpecForge/patches/sglang/v0.5.14/spec-capture.patch -``` - -#### Step 5: Run training - -Use the `sdpa` or `flex_attention` attention backends on ROCm. The `fa` -(flash-attn) and `usp` backends, and `yunchang`-based Ulysses/Ring sequence -parallel (`sp_ulysses_size` / `sp_ring_size` > 1), depend on a CUDA flash-attn -build; the single-GPU / data-parallel path never loads `yunchang`, and selecting -those backends raises a clear error. The checked-in -[`qwen3-8b-eagle3-offline.yaml`](../../examples/configs/qwen3-8b-eagle3-offline.yaml) -recipe already uses `flex_attention`, so it runs on ROCm unchanged as a -single-GPU offline EAGLE3 example; launch it with -`specforge train --config examples/configs/qwen3-8b-eagle3-offline.yaml`. +For the complete container setup and an end-to-end walkthrough covering +installation, data preparation, and offline / online / disaggregated training on +AMD Instinct GPUs, follow the [AMD ROCm Tutorial](amd_rocm.md). ### Ascend NPU diff --git a/docs/index.rst b/docs/index.rst index 189e78361..3eb5ebc98 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ SpecForge is an ecosystem project developed by the SGLang team. It is a framewor :caption: Get Started get_started/installation.md + get_started/amd_rocm.md get_started/about.md .. toctree:: From b29cff3896018470897006d35b73c431d392b2b6 Mon Sep 17 00:00:00 2001 From: "Chen, Todd" Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 53/88] docs: move and expand AMD ROCm guide into dedicated basic_usage tutorial --- .../AMD}/amd_rocm.md | 118 ++++++++++++++++-- .../AMD/imgs/mi300x_eagle3_acceptance.png | Bin 0 -> 111855 bytes .../AMD/imgs/mi300x_eagle3_loss.png | Bin 0 -> 175594 bytes .../AMD/imgs/mi355x_eagle3_acceptance.png | Bin 0 -> 71877 bytes .../AMD/imgs/mi355x_eagle3_loss.png | Bin 0 -> 144214 bytes docs/get_started/installation.md | 2 +- docs/index.rst | 2 +- 7 files changed, 113 insertions(+), 9 deletions(-) rename docs/{get_started => basic_usage/AMD}/amd_rocm.md (71%) create mode 100644 docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png create mode 100644 docs/basic_usage/AMD/imgs/mi300x_eagle3_loss.png create mode 100644 docs/basic_usage/AMD/imgs/mi355x_eagle3_acceptance.png create mode 100644 docs/basic_usage/AMD/imgs/mi355x_eagle3_loss.png diff --git a/docs/get_started/amd_rocm.md b/docs/basic_usage/AMD/amd_rocm.md similarity index 71% rename from docs/get_started/amd_rocm.md rename to docs/basic_usage/AMD/amd_rocm.md index 62e1df4be..8c1ca94a6 100644 --- a/docs/get_started/amd_rocm.md +++ b/docs/basic_usage/AMD/amd_rocm.md @@ -89,7 +89,7 @@ Use the `sdpa` or `flex_attention` attention backends on ROCm. The `fa` parallel (`sp_ulysses_size` / `sp_ring_size` > 1), depend on a CUDA flash-attn build; the single-GPU / data-parallel path never loads `yunchang`, and selecting those backends raises a clear error. The checked-in -[`qwen3-8b-eagle3-offline.yaml`](../../examples/configs/qwen3-8b-eagle3-offline.yaml) +[`qwen3-8b-eagle3-offline.yaml`](../../../examples/configs/qwen3-8b-eagle3-offline.yaml) recipe already uses `flex_attention`, so it runs on ROCm unchanged as a single-GPU offline EAGLE3 example. @@ -107,7 +107,7 @@ python scripts/prepare_data.py --dataset sharegpt This produces `./cache/dataset/sharegpt_train.jsonl` in the stable `id` + `conversations` contract used by every checked-in recipe. For the full preset list, custom datasets, preformatted text, and target-model regeneration, -see the [Data Preparation](../basic_usage/data_preparation.md) guide. +see the [Data Preparation](../data_preparation.md) guide. --- @@ -134,7 +134,7 @@ torchrun --standalone --nproc_per_node 8 \ ``` The output path matches `data.hidden_states_path` in the checked-in offline -recipe. See [Data Preparation](../basic_usage/data_preparation.md#option-2-pre-formatted-text-format) +recipe. See [Data Preparation](../data_preparation.md#option-2-pre-formatted-text-format) for preformatted inputs and other options. ### Step 2: Train @@ -153,7 +153,7 @@ specforge train --config examples/configs/qwen3-8b-eagle3-offline.yaml \ training.max_steps=20 output_dir=./outputs/eagle3-offline-smoke ``` -See the [Training](../basic_usage/training.md) guide for the full run schema, +See the [Training](../training.md) guide for the full run schema, checkpoint/resume rules, and evaluation. --- @@ -167,7 +167,7 @@ consumer trains the draft model. With `deployment.trainer.nnodes: 1` and no `--role`, a single `specforge train` command supervises both. This section uses the small -[`qwen2.5-0.5b-eagle3-online.yaml`](../../examples/configs/qwen2.5-0.5b-eagle3-online.yaml) +[`qwen2.5-0.5b-eagle3-online.yaml`](../../../examples/configs/qwen2.5-0.5b-eagle3-online.yaml) recipe as a single-node smoke test. Complete Step 4 of the installation first. ### Step 1: One-time run inputs @@ -312,7 +312,7 @@ Instead of starting Mooncake and the capture server by hand, a `deployment.disaggregated.managed_local` block lets one `specforge train` command own those local processes and derive their endpoints. It defaults `default_kv_lease_ttl_ms` to 500, so the lease-TTL fix is applied automatically. -See [Multi-server capture](../basic_usage/disaggregated_training.md#multi-server-capture) +See [Multi-server capture](../disaggregated_training.md#multi-server-capture) for the managed-local profile. --- @@ -345,4 +345,108 @@ capture server must use the same target model, revision, capture method, and auxiliary layer ids. Offline features can also be served through a disaggregated shared-directory or Mooncake store. For external-service prerequisites, freshness rules, multi-server capture, and resume, see the -[Disaggregated training](../basic_usage/disaggregated_training.md) guide. +[Disaggregated training](../disaggregated_training.md) guide. + +--- + +## Reference results on MI355X + +The offline and online recipes above were run end-to-end on a single AMD +Instinct **MI355X** (gfx950) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi35x` +container, training a **Qwen2.5-0.5B** EAGLE3 draft head on ShareGPT +(`max_length=512`, `ttt_length=7`, `flex_attention`, `batch_size=1`, +`learning_rate=1e-4`) for 1,500 steps. Offline consumes hidden states captured +to disk by `prepare_hidden_states.py`; online consumes the same features +streamed live from a patched SGLang capture server through Mooncake. Both paths +converge to the same loss and acceptance rate — the online capture path +reproduces offline quality on ROCm. + +### Training loss + +![EAGLE3 training loss on MI355X](imgs/mi355x_eagle3_loss.png) + +Draft-head loss falls from ~25–34 to ~15 over 1,500 steps. Faint lines are raw +per-step values; bold lines are an exponential moving average. (Offline logs a +handful of all-zero steps where a batch is fully truncated at `max_length`; those +degenerate points are filtered from the curve.) + +### Acceptance rate + +![EAGLE3 acceptance rate on MI355X](imgs/mi355x_eagle3_acceptance.png) + +Mean draft-token acceptance rate — the quantity that translates into speculative +decoding speedup at serving time — rises from ~0.01 to ~0.15 and the two paths +track each other closely. + +### Summary + +| Metric (final) | Offline | Online | +| --- | --- | --- | +| Draft loss (start → end) | 24.5 → 15.2 | 34.2 → 15.2 | +| Top-1 draft accuracy (`acc_0`) | ~0.24 | ~0.25 | +| Mean acceptance rate | ~0.16 | ~0.15 | +| Training steps | 1,500 | 1,500 | + +**Throughput** (single MI355X GCD, `batch_size=1`, sequence length 512): + +- **Offline** trainer: ~3–4 steps/s (pure GPU-local training; no capture server + in the loop). +- **Online** trainer: ~1.7 steps/s end-to-end, bounded by a single capture + server producing ~1.9 prompts/s at ~10k tokens/s. The producer generated + 1,719 prompts with **0 failures**; add more `capture_servers` to raise + producer throughput. + +These numbers are a functional reference for a 0.5B draft on one GCD, not a tuned +performance benchmark — larger targets, longer sequences, and multi-GPU trainers +scale differently. + +## Reference results on MI300X + +The same offline and online recipes were replicated on a single AMD Instinct +**MI300X** (gfx942) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi30x` container, +with identical hyperparameters (**Qwen2.5-0.5B** EAGLE3 head on ShareGPT, +`max_length=512`, `ttt_length=7`, `flex_attention`, `batch_size=1`, +`learning_rate=1e-4`, 1,500 steps). The capture server used the `triton` +attention backend (flashinfer is CUDA-only). This node was shared with another +tenant, so the capture server and trainer each ran with a small +`sglang_mem_fraction_static` (~0.12) on separate GPUs; on an idle MI300X you can +raise these and expect higher throughput. + +### Training loss + +![EAGLE3 training loss on MI300X](imgs/mi300x_eagle3_loss.png) + +Draft-head loss falls from ~25–34 to ~15 over 1,500 steps, matching the MI355X +run. Faint lines are raw per-step values; bold lines are an exponential moving +average. + +### Acceptance rate + +![EAGLE3 acceptance rate on MI300X](imgs/mi300x_eagle3_acceptance.png) + +Mean draft-token acceptance rate rises from ~0.01 to ~0.15 and the offline and +online paths track each other closely — the online capture path reproduces +offline quality on gfx942 as well. + +### Summary + +| Metric (final) | Offline | Online | +| --- | --- | --- | +| Draft loss (start → end) | 24.5 → 15.0 | 34.0 → 15.5 | +| Top-1 draft accuracy (`acc_0`) | ~0.26 | ~0.28 | +| Mean acceptance rate | ~0.16 | ~0.15 | +| Training steps | 1,500 | 1,500 | + +**Throughput** (single MI300X, `batch_size=1`, sequence length 512, GPUs shared +with another tenant at ~0.12 mem fraction): + +- **Offline** trainer: ~5–8 steps/s (pure GPU-local training; no capture server + in the loop). +- **Online** trainer: ~7 steps/s end-to-end (1,500 steps in ~200 s). The managed + `triton` capture server sustained ~38k tokens/s prefill and the producer + generated 1,695 prompts with **0 failures**; the single-command + `managed_local` stack (Mooncake master + capture server + trainer) came up and + tore down cleanly (`default_kv_lease_ttl_ms=500`, no drain failure). + +As with the MI355X figures, these are a functional cross-architecture reference +(gfx942 vs gfx950), not a tuned performance benchmark. diff --git a/docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png b/docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png new file mode 100644 index 0000000000000000000000000000000000000000..91260917d6fd425da6ad3691e4cdd142ef37cde2 GIT binary patch literal 111855 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 u=6@@IpMeVtLqz6Lx~CD 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

Yq?8O8APN2AWSO~uOYX~#e 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

RzpKMgXTgDN2D(&0&L1!`_q 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 literal 0 HcmV?d00001 diff --git a/docs/basic_usage/AMD/imgs/mi355x_eagle3_acceptance.png b/docs/basic_usage/AMD/imgs/mi355x_eagle3_acceptance.png new file mode 100644 index 0000000000000000000000000000000000000000..21f3b6ea1a809d09d4a5b38e3d842ab84e06601d GIT binary patch literal 71877 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ƧL7twpW#d+P5CRHGQ98WMEAq0_%dua{J{_@M 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 I-We4f&o#x z|Cf}s^sx-n%_=IXIEYt!pQWhziq&bVUU32xHRFPInVg(lg00-Tw1(-Xs*2$9P8WIp z+c$51fGpW@X?kj8>09}Q)JRZ*-rB^vzOBt+6$v+@qA>nrJ%=t)ln>{%N94!mmX;~m z*(Mk*{CZAa-pBFrOsi)%4ULQt&sQkae!j^mHS?m&G|I*f0b#zlj zx626|o85zhgE4tZRH5T#9=wX80t9U^Z}5lqTjbxx?Bt(^gPj(WuiQfaAAbDpK-FKi V^l|U(MaeB_?9x_GRXcp{e*j-KEPnt1 literal 0 HcmV?d00001 diff --git a/docs/basic_usage/AMD/imgs/mi355x_eagle3_loss.png b/docs/basic_usage/AMD/imgs/mi355x_eagle3_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..0cff78c85298cc62993a68178cfc5031124af0e4 GIT binary patch literal 144214 zcmdqJbyQVt`z^c(2?6O&DN&FV*t7_U7=+T@D4hZ#AuX{90~7@WQ3NEVQ&I&~8Uaa> zE~ODT*D~Js{JwGi_{KQ@oiTjIeKsa;UMiXeDc z1i|GbB7lF1Fnw4G|C9Dq(f7ROV(aN`;ckOmvG8lpwkdV`V-XZAXZYT8C$m=6~3$g130}lkDK0yD&A-pdYir^rKnzEv{&y&Tc z-pReayKVJqFP!WQ9%d~z25Zl$JikDz+z{MY%B6CIX8#K%f4Zr_K!A?G8U9FVp+7pX7y4oOrLMZo75+rvz4{^(+Ox;@>|w zfgU|5OMNxvk?D#6`3CI%Kh(c9Kq-QAN^1cV+Nn) z=Hi!%-T7Rt0#iAC0lMceUSzn8)%pDFl+?O<^=hAzfaJxC7gwj^6fA#juT`vfU!r7Y zj!sTyc;onmid=r1L;VSca!n+TMG6-?6@1`0SorMn6s-znLG32fP`OLo;Gq8V=g-Y& zKR?@s?>Oc3#Huw$WRaPkK4Noech zg_;B{@8;5w#fSSR-+laeY~fc|!-og_Z{1jSHx~2+ z+fpQ)m@wF;H#d4dO#9F(a(^PCWLR0bdG#vQ#Y>m)Hcj4KPgs`G(bj%GJX&C;;K~x= z`}6zUQ2VaQwX~+Q3a<@IMMpGPRnyb%QlHpe61%N*Y z*w%`sCMSj6zg=i@diClRCHtpocMI46{VLCsu%*I|gTmjweKUWb$fI9uP5k}i(`fjD zlJ3?o&y)zSd=qYUo$o!FXI9U4c(7H!)~9D?@wLh;+o;^Rd5TfQ5`S`Pimbe)wUywj z_wq>P(BVvHO2pnuy~6dwdnYt3zr9O%eAc-yhnCk9At5N3pSkJV9*G3WO)vgZ~9ycs=y!+f6AJ{%BC6#&OUea=+V_Ca)t4JBNy?JgZC%W?$1@w@0$EPcshG! ze|IA!)q8dPZMI%Ps+nILo7}G<+mBD~DjaO%mDqi1?UWpgQ4ArNE0cMu9>Xd_m&iQ< z_ja!5#g&lM6poBz=P8s+$2%U0&CefQYTp-yV-AUC zuH1H{0V!;XAXj~p_TiaQ1VfvffS@3jhlfW5ah&=16lX6@&hMePeQzX=x&Pj#_#%H4 zr_r11l-0j}w0|-2{+c!XVzl1%utQ~N%ugY6#n8=7qsq}!Guk^5Mbz^}=DYU46@`S~<)X-oc z-=x&fOkY2Hp{Lx%j3B7h_kKAXK_a>@?8ihnRTg>}P4o`Mu*&h>>&Uk+85Z z2620B+SoV07+|gBcW1dEanBF1QB#MIEBvCkd-pEJ{Toq#fti^Kmo}t`OU)VqGQPhx zrt0oqT;??1aHnD+zk1viv${F#qSr@(gLp0Uw^+r=5uA0qly_Nv=Nq+ftFUD=(THcy z6=ZxS>U(GH)@Za<8}^w-n*26=QCFFhQN4UHhxuTkg?_m+>oO#J6w@tD*l@DbEJZ~{ zJ{x^{I7oYvP=rAH1GzVmcVlB!AcBA8<=AAYKb5`?2{I20BiP23k;Z%FaXNf2nw{{Y z>GJ*Dg1X?Q2_pMGa%Hcb{8%Pb^N!=*gF2hH3d zR@YXTeZ>=L-*B+MS7iPc7ioa|A&G9GDqI zwHz?)QaMeBG&F{h@cUg4$QIPg;L{zFVzk;`@6o{Fi_Q_TYDJK`?Mad*x5eRd4L|H? z;GutV=lArs8DF-iGTwq?ib;69_o&D$i}V|#GOtZFYu~oY(XO!~BxjWlm07O9+gKQg z;2`o@cwLSlcArvPj9hBNa`UT>K6>=1@QdRGb@lUU^8RlsS9vsJGQF#tCK_P*yYdXg z^k3^j)t!14!FJBgt!Qiww*_;nJWbl`C~UV1G!XNWFRpW0Nd&Qha{jw#mh1MMdkf6i zB~r*<8h^kOn9@vQ%T0P@C(2X42s-etFr3UQY7+l(nY^`#g zIA`qDr;}|`Es>|o|M<*}VCXWFvt5}1{(U~99UYh76~~m1-Xd`tdUL1KR>{zisWp~4 z0!rL@&j1&wv1DqAtI&k@oS>Sr3D8|SE7@O6HRt_0V`nxqE6=%==5R&Cp?zkQsKg)^E>=S=c;indMTFU`wpzXk=sKIRFknjqjdFyGV5 z&Yp=rXey0xM*r(ptQwmU0&;%4do=VwZM0s_kC z&YgP@Lybr{4dI{lP0%#R(=WnR#j+?uLGFV4hh|N8+Rz-Jgno^WNPgvaCX++6fSn^T zBA$zbU7v=GO<-+&mMdov1QLCI*UiG!BW)gD>@g3RO3kq97O6=XwZRT3QUB z%(>L>hr{zQ#4?GW`}w^Sa+myxtS=R0U(bm}=m`At3G`4&!fEmj0+eDsAoYJF3R&AfNF3}GxRUB01{A z_3?>Rh)w&$3Ak9({gc;LyDtgd*pH5mMsIfUYc)NYJxgK+>5aIR)?tQYm(ObbrC@ zun~`3Hb_HZUz^^$#_kiF<7f?|!&_dLo>e0V(sGyZU+w;7%UY(GEw661cgA-IXsT5#r7x$vEci4^Q_v zT^n|iU^c3B&+1?Hl>V|h(Zq;I+*}+i%#Rakl|R_FA_&TQ@j|rvE8AwkXpv)`f<#Je z?OH3d1^cDxi3ordgG%IbG=&V7c1)QZUzXkUct<_HZ_fR{Ji;I(cUEN^7K zmYlbNB}<=AuU)F|d53FO=Tu`DCI-y>NByVhxSkbTvgknnX@Qc_iT;P?0mPylL6Awn z&h#S0HYF>6FQzy7G_nZ&P9kRQE8y(mH#hE??Y#{lEFIJuvol9~%;i^FGDtrJkqPMv z`h(;?om^c;Y>#AFhu^QQu|NfC6|Lb-z+b|MfV_MvEiLWbX7%QfL#f@1H*d5Z#>!pN z=nh%u#Oyr_E?v50aO;*dUPJ1w`IEfu(7cz1O3@GDm_rMS?$2*6HSy>a@0fOH$M`P$ zSBPLwB?JuAq)CpYK>;aT8ZKu}Rb`Fn`UG{yKwVw^-JbjO13mja=rLu0>Erh(l+U32 zGY1FioH&5I%a5pZ4*}Z+;g~0x-g?#l@ZP;94AzQ~kVr zWI*(?>o)Ww24r*4DsFB_iC8sthK#=M9`6-PWDwvTX&d7OjD+*Z;A)lzP#y;4E6EFJ zxnkI<L@EPg2we>MbtJD%A2p@2a~ zY}eM_d5S(tcO9NcCL)iQYg<_MTmS*mp0hy^X;Te%@UD;6NCC0JX=NZFWeBRNkrgnm zpa4AA3N59`fA8kE@82`b>H`4(47mmDZ|EUWGKrM_yX!bXL8TNCq>PgTFIrDA@myQw zIdOt;z{H#N$IqW`fS?FoC)p?ifm(Y@$UZxSUJx)shDr6!Jig z4AOezwz~SUo6942p$|_$Nos{=wv=Rkk|p@TR0Xt$Uqirn4xd$2Fra+A#J2Oz=K8uP zDT6SAf&u_LjKyanB%x19T%KbW_@d5@??EYkafX~P>s;~=QMwZvsm;LE*8p&2!Yyq7 zXcuzZ-xx@0AFN@-xSBUqA~f`MS(lP5f$6{d@BxrBVnzB=BaHxQ+D&?U9ZG>w;ee^8@+T=)0FfP?gW$u^apZA;8g4{q#ogBj~d93H9 zM%z%S!*Mk#e8sasad41=y4_Rr{jZ}fn!=BtJo$cg8`%H3+qY!{4tDHneth7sSSlNi zPe`C#!>DLAS9vWU3<@Q@_lUhmeNO?E1e$$)W99}BQ~s^PM`wGwwe zmdWDa*NzW7Tan`z(I)^wQP%e^d0JB$IPDdhr1y+Mob3z(SEPLq z6M5o;2ei6@;zf5{p4Q{w5t#37F6l#*KbCaKkGAM&Z^uh-7C*)&i!r~QX;@eRJD}L+ zx;!G)bTjivBGbxeziA z+C(e7XMZ<$Mvs2wJn=d0Ue1ApX36u?>Mr5r?8`~_l@qy9&>~c~&58DEsL9vWcp9ex zMJUz1D;A5}KLguFNq)pJ)^3~O)8tH&&MdCO=&mT_x`;ifMFCs%I^XK1_cbu7jLwTRMLWOR|&ysM8kx zMl^Tfhe?H>Qc_c~fEOnLYd1h8qSD0F0a$VIpsB(n;TQ?1n)CGOE7URe>}C^uYE;NH z*&$F6yn~T|y`^v~eutw!5)-LNnI%Xek5Q(z3c!o7Q_zUcCv_mta@1v*UdS{Cs&n&- zs)|X+u5F&qNdbXgy1BP>d-T#DHHm_Qfbw&Ri_-$^?aFJTZ2^I!(6*BaxoEM-pI>oq zE7KPFT5LmtDlAS4;yAqzPaB=`yiI!rFjQBqpZB-v=^YyZL2`gWxX#Q{?%dG%;-IWr zK^c43*%`(cWgc@w%~AVPsO!mErriI)}Iwqs$fLAd-s~n@gGpxqmU%jA$Gy-0X z17&BI_yr}``EfvG+n_Xx+*-e$+-cj9rodi4;!Yy%y=*Lqi=m8bZa9XpWa%FS^(Tm7I&a?lwdZ6_G5qO7h6$rd;;ZHkRW%a zFpEQ_m)ge}BcTIK0wl*n(A%kUf^vQ5y>%m0K)1y~5f)h=%@d@1P%h9G04+nm#Fh*c zx&7j{SGvJ7X@P72(@A!(nPOOuk+RFPpv?fX4hux|h{+i zrajBR&(t^1{tnlS5~T~(H-Wd){dxcM%GkX(02|3fq+GomS4#H6LE zneKbVm@j8TeItK$y7?}(tMpUoPrz==oQ-U4dC}G?pkMf#!UOw>fAS;-kXx_Nf+N&f zSI93bNG6QuECm$suzZthOAx+%Ao7*4Vmv%YKpa+o32^3#@M5N^lEFx~(0vdxo`J@s zUw2z-@n*Gd_{$8wcJJ4D^{OFDC)!1UDic?52 z;wc{-Np}GQvQ#=yrN=}yAlHQ)bos8nC7ke-?t-#{a`P%DVq)Uip;G>jPi0;j_|=3> z0U_pS7c~vov7w=*odAlo8m(FhB0CN;7O>~!YixJ{c=i>^uc2dshpcer#)A3-!{z+D zr)0dBdz-b>yP3|fFMgp0WCT)Hq=nk63Hr%_T!*_c0dMse2h})xA@t->rQoJ*7(-&a7;I)XW~MEH*8#%GPm^+0BrT z>mwEqxCyjBTyXE_%qO98k5|rFX1CYEEJS?!_N!pU6iqHlVViXHOu`blRB;iYB$Vql z&~Q$&1#C|g?&4k~wfdAQhtp6f@se9@d$Hsb((w6dtteXP5)u=Gq4g20pF2tZVb^5u zdx{HkHd>2}u$AF=OWNB9K+pnxwX4d@!6ar1q_=QElPdEzEUQH=14rcb&`>T#ZE94m z1tmDosQefdxTk5+mlYLpma7&7(HaPfPAf>M3QFaII((2@)GTc`yx4HYQOH;3yS4GX zsEtkO+&Nsd2?3OL$j?0$BZAV#df37+6JeS)5&K@g-vVRL3o@u;<%O1d>Z!7KfvP=& zE*(%L2NHbIGrukXYNba;>kl@BIvXjE@!nI*?G(S!%?$#z=`hOhI#T7BA@I;<{Q!Fn z8rV~5T8PS!_txE}xZ3zoN86wx$0sIQCR33=wOcmNraztGOmW4Y8@_e?YF zd*H!O_NPb89@@zM><~kXCljdM!p90S7{zQ!i%Uup9z4+bK2&nbW+xvSI4MXeS!Oja zxYc}Mk=Hq&4K%fe@_al3(gpyq=H;1Sv*s5NBoz!gkBi`PTIKlbA0;=+rQ+f25oho-ZT z=PQ_WspQ>`q1~9@iri(GkUpPBoyxuk#JOOfUXhalNYCOfI%lD0An)@ zN^DWd)!oBGTyg+j?3^axcYmVrYc<5y0H21x7e-vJ>$6$_3jk-!ng570mj%uQP$V_* z!1s)O?*$LJ+PN1g=WMlV0E(`z1RgR&PiPV!Fvde*iX#Y=I2tnA5~%w1;j+_)<<7Ct zOF4OY2~cI}4j!TFWHYfV%S@6;cM43V2uJ!s&#$+peP%q5in1X2;3A{9e_oOO@s8^{ zq>ka6>p1t`heFjg2YHL{^l25qdut$$gbRo+calkt1fgRrJyTyEASvlzSKyJtN(1ff zPw}chi3B--9axxZeZYSF;uajUnRzZYqE=Qo`I2DpNY74M4taP)6N; zZM!|s%Ch+J?Y%|p20FgWjFXRotzi9H?X5ai=MOSuI6Ma`CuZy9($#@7d8(7b#Pt4j zH4J>B;o7Vq{Rvfh&IRAvn4kEy^Ajd4FE^{F!styuck&>PV|A>chzSGr4uZYrl#*gZ zDG+pBK%#Mg;C{iD_T05-WkAJf8hZLE;9L^FYJ2MZ{ZK3@RI`eU7RfwlsUNI7h&ee= z)lCvAQcU0NnX_j1U#F~|&&kyX$X zOSa-%o?=#{ZR%!H0Pirv$j||J>ZOYpEqU+V?R#}C!o+v_!6K}hsK5(0LG_pSYN0Xa z0SM`^Eh&N;R|NBqp#tGsDp)a1R@La1MnLNw&ecm2^^GeN6dz>~bC`fw?dp8x5QM8hwgDnFP5A+=!1xoY<1Wuoh-B=nafFQtXd^8b z%rv|tG;)CGo0_KHOa*!8$yZTZxqYI_5j`T(`K?sQgxHxAdVj7{K)|si~oq#|pB6#GpWA% z!ZvrpnW@R|0apgzpb)RA|$!5Ae4w($@!mtzTeTI|#~icCMc9?)nW0 zt5#A8kn8&^+^lR%zUaHVi!OYAhUd5W#m+!iS9fqq&j4178RN>43$ZGe(wV1~dEOD` z7~%CUeRj1F2;pr<#mCNX_q&l2Zj444wG|1Bl`bwiqUu=*faKzEGG@_~&Q=&rI1Ifx z33E^FhOsbEiEL0+$)w8D0`k_zw&W4$;zcm=FHvRpzt1XntJftBGzC#4??%rHLpf_M zmScp~;4Ns%-&3RL!f(?Lz0pS$94QT-hya>41!G5_AMZ~9@r%h}qf(?$&ME@9W9-&K z3neI4P)P{v9$EqM{!cjU5lFsf6gt9a0~OT_tDaHfE8;8Bs5Iqu!xlAduTA0HcX@gi zydLMs4OoaX2#PG42n0-P7+BgYx}U2V9`_37h@4l~(xQYJZ#=XUfCR@yL?~6`*jk`e zazt^03LFBpIy~kT512$g5d5`~WUy(`tN{|Tgb4`d_O|fHGzHZEfbv0C;nYR5kWu(T|*rQj~()% zaghmb`*5=e1;M*58bgV7#-N=nj~It+xjSiV~SNzHa$FoW=M^z5DQoVU^|Bhi<5k0an^#^lL@ zcP4iUCjWXt20*+ki{185sLwiG zwEy|6HhIqL+<6$5xI%7>xHgep(9nngb_KV()}_im0hOl_LQbpP+2pIdOWh7HNOqYH z3*A|lY>uV;vkheoX*q>WJ3_|f>c43~$|(B0IO6i3@7ETM5djAL40IUaKgS~QpgNB4FxjcT z(tTG9lV6e14Iz!35F!OqO=*Xr_q-CW(% zu=#Azc9o%8pb3ah7ongvJ(2Pd2ACN?BNgxq1Gb-M4#SlmCy=azn4UR-G?FMJsr`r+ z&4hd9sr`{qw*R@Cu(>%vs3u^HFmx++#uuoxD+^((K5g9cWcU>3akQ|OnGayrGme84 z!5sKYW~23XIkm1V^0zV262dYvI30(}$Uw;E0)Xk|&vt9}c>O*K9HB2xqM{=R|CH?P z?CY`sm|Nj0WaYFpG#rwWy2gU^QBhG144J{NbV&b%>HikBeVY3EL=jDQptr)< zgb>uaVgZOZR~Eg#rs`~q7{x?xxIgol;}*HQt@kr+S@#@B#MK6oSvOcB>O3%mmLfc z)3=uGg;@M=jMd3Wd(DTUQ&)q47>EI$WDN|4d9AWWu1GXe}Y2qIA3?mRy?H}^P8&}Q5$`eo?a62%Vb}4qC>=fA9jMRQJokZd?U$7GzBlK$ z?u;C^qko=TiwrIOAZKEGd$nCzUOvd-KY8-);r`g+Gw7{wFYV;rU?N*OJO$0{?&EI~ zfrtB&mwBlJU{;yKc2OV2e?WYIn(N5jPuDhzo0QPU&9*8 z(m{Adxf_595wCgerT}ueb&B21rD$~64+W0IJxoWn)JunFooKBzUV81c$?z9)_VaUf zS9HWU{3({v=x_4_wL8a1=i z@x-tsD+J2JKWwJY>_CnIT!PU?7(lmisL?Q5cw}LQoe~C6ib5_Gm99s*p~gB646mka z8{Ay^S=zSnkmiy$*Ik9o&#t?iuG-oUes|t;ZL^Yk=7+?G-A-L+qu04_|8O0zs5uK| zBoj0hRE9v?0d#UJpa;oP9=s31fdZ`JIG9Euxz7SqTmzAIDxBH50n~9K1f8CtCI;DG zt4-P0ULHd&H^^B)i0z)NpqZQpii!?cf%K1~YI>nXlk4`XMRQBbjeYciV3uzMZb#HJ z1K<_)EC%96kv`P=*#UXy5lqd`k23mz zzMX3~bdiSOBP8)jNfA)wCSJWvJ?fJW-mY{|Vj&KcVFZqjoZw&3;ZOL!-}>ww)aiy= zW5A!ux^9?@3vog!&&#&4;>*@%%1B3uE*tRC2Z2JIraUJtdy-kniH)K~F>cT;3VG+SmVO$U7Yos$5^G)ft@GIc<1x7%s|Ef3CE}HW|nkr z2GRG7q#m@uVdrWxR6YCg;|IvZi{)|B%K$J{vRt{TwXY}%HyOXg>E^9_;qYhs=+h2QHbP@S(x(sI~V(n|TB^gAUoL~kUdYWFD-T`*|739;$9G(^jLFW{96 zxQR1VNF2XbmCAkX9Oq56zOF*|eU zb)6l#Iqc*E##7e9s!l{qo5%Yt(m2^tw05seWQhG7Eq_lk$KUa%A4`7ch5 z-Tp~-l!Rmr1eqzAdPppmz_=UST3Q1yutv4m>mi|5S3>l{GD)78pW8oaHu0@QX&f`_ zTcTaFc7o+;FY!j&HkF9eEuF5je=PrzQR#rtP-RqEtJi=PMz=7z*%t`vIt!v*Vc=l^ zI?`BMTeFc4KP8}4`Q~95ny3o}^#nojJYr}`2LLt>qSxl|cTZ_ImS1j=1Y6ia?HHr)byssuARc;j4E({-^CtyzBGRe3mg-jpc|&@iCni;lFou)DoT* z=z{?~Y2Q^2Vnjo*& z8cBr4Tjc87?vu3NykAHS(?^n^b{)5 zwLOkB$5l?+Oeoja+8Xty=&55b9<5?_@A9_?K***`w|0$d5`~6Y;fQa2k7NjAP zk_&eeHK(JFLP+lMher3A7Gp%v4g+E9_2_gyEg1wSkqbdGYs~woBi&cd6);;k^fSIZ z*cZYabHqU|?97bI))Dr1%$~VR{U<&XP-+Pr)e>0X_F5ae2+aIY>2I?6ZnC_;G>8K^ zw<5zK9SA|fYy)8<6!ZpSAYiC-4kiF=;HV4piUDni9#vp7RH8{+hC%NW0c6_bPtJk~ zf~|ev`|FPZR0aACcxqV-$574maIgLlC0u7Fw%gbPQ8^MV(;sdhM1@ z_GzC7Z_#^1Ya2Y+@sgZf`A>KFTwb3WyZ<;+FPH_1wBQx~-nut;We+-oP>ktU){f3J z!btNFv<6weeC}0Ly7*#$%+30QwVztnv45w4BIohYz*s56RGH}5cr!StArnLf-F?3Q zQDj!bp_nLtHe%^pSLG3;v=`)l>WXhZR{yFyT=g%tP~Hgs`yQTh{rlDp$4?&gl-Zgf z1>;6h9{;|GlY*PYfiovJC(m2wi0pFCZAV;z*x~C-S@#Y8ARJJgka*X;yV?0UiETPM z;)3&&EJnsJ_RTt^ao2u*xya1f^4`6v1OIzFL)jq-93_qm%9 zl0R4|94HZ=5uMku!$Bpj*Q?`O{;d@e*9Al#$#>A`xoi3f;^iEVvj6p zpO=(H`;YN<#{PR4qH|%eC@m(5N;gkF((%QH7>rxcVFpc#zc(`Ve4w7;qo023HE%v0 z_xA(Ybf{Y#{7O}*UGcb>SjolKsdgBQ9)+p|u1Z}rS|%F9z*DEj! zd9&$|U)j1UE0{zAU1uC4m~Ea401|^$f`SUrjdaf#qVqbKJX3up?-rYQ@}P1X1#Rf? zAuSaK!XWCMplYpPZ8bhmEvDW&B|<0Twv& z@1N3KD)tZIl#(m3m7UTP+}zuxUmPXSf(72C^T2bwh68@Bsao6FHQAONVf_T`*bPqo zjKd6m8|JCPU`GG~1Lv>6s~HQp2X#S*`)A-l+-skS&ZPRpS3x4xT0#AdrkeU&a<}Ml z?kxN2FI$QKHj{`wx&=44&#fj0UF=m$A29VJOEPvR z2DKS9^EefKBGvf1d^GCYH*1(sS;D!W!TP}ZF%f{~s8bxQ-wl6`GoZ+` z2>McCtLyg;#yo=(0(fk)+(16?Ckq3a>_E@01b{q~GPN zSs{e69(SAD+A@KuZG#ZmZnH>xq%x;p8{fwM5jSpdB8w_dKWD@74}5NF7YZMnJGe1M zN$%&Z(?4}(8O1S*&esuj{2dvQAHJ>gmpA#!N>jhm7d3o#Ky9X6V+Mze(Ao|x#1gP1 zI>|y!7pPHx=f^aN75CEozF*Uj3+wK_dO2AbJrgHp_pupFik~5*VThG`c}hr75cT1F z2EYyDV?1b^tx$1e0kW{6f&;$dTBt{*F@uC718O7#Wnu#Ot14U=^&F#fu)tqygjF}m z>Lg4`?P-y8ko0l(Xz1uTWn_j6bChpYl1fy0-m?+VN= z4<~&8Qll6aDJu8?YK~h4gXeEEjEGhEguL9X^#%~@(bEGaw{6cTHWs~@WS!zef>k9s z`vo}21@B{2R4`>##^t&GW9O-`zdN%r4JK|SY)aEd_w{@uXtq({JspQ5I!!RHeOc-a zP6bp^e_=o# zNIZ%Wfa*VXeF6tp@@qE@Awy1#`+{wP5FDBIBC%fcy>Ef)po=w*PW|vpBS6>=M{V`Cd7!(3(^8NqIUw&;?uGHr?Q z@!12;+8jePK}iYnN6GIm2#Np&a@o}&i$JKtXqZ3*oxgG8BsxY0*h5H@ehL)V^I+Wp zB_+2smQjoV%|#f*lWnP~so@}S0#IVMD3=N?Of@DtIIgCR2{TfImt`-Sh#Du7dYp4g zW<4S*q%XEWUc(ti;!<`@EE&%3^2b&>u#ZqE4-EiW(%Rio?@GJS1?ykLCrPvg9VKT2PVpR;M5n& zcs5N-RGELbpu8McBbFm+cEbIf>AhLaQoO{nmNLiA))`?wsQ6;mzCC#7R0zkk&BQiMxsT)Lj+N~{x2Ejb4QmRgu*fs` z=JT*{Ko|z5;r%T+&p~=b`3Q{hkyIfgZWBOmIDCSIcRsk?e*km{og)n$;=N^Pf+K#P z!CE1^zuprt4B~GHA)5~Z?u(Ob{_9#_CB&z4Zov(S78*40ZO+RV8ds46Qf!Fim^M8jN#vEYyZbP53H~?B>8+tF!6%*m7m>!9EkGK({ zCZNxg$-l{vz3c=imn3QS(U`+*r{5LqU@$I5}k5o2`X1@v%>+E6Qeo!V&{>Bm%TPK%GVVBs#}HGX{){UIyx! zchI$gzc<0@XzX-*k`)}Lx^Ve&GlXR!*nr{C4=TCMid?;RtqWvhILj%7T3t!^P|fsa z^;g$nV4ZBjb;2;91nUopFDraG0W#t<6^pZnl05)W(m)D>V4#GDfFM-}u7h|r?;Iw8 zkc@9hTN&_q8J)2ue2qoTJZ~+p#(e5O|2pBI`zlLdhQc>op6do3(!qI>E9ZA(cpgZ2 z{(+1cj@&#vs520Bn^P!Efv!mp^&=T|{c5Bw>opbSh@^Dt!|7A`?QuL5J)rMIcq-M) zoxi>*rh8Ta%Dx+bAuD)v^t9I4ChCwLuY);hw3+C$3*gyoLC*z&yu>9YCT6angZcPK zOqCit=D)=XknD{2;Po&odaAWs2_%&k^=@YX0R#A6Txb-Y1`yqEF<85&f~*!6Gi8r$619ELt+7I^i&aeBMFb zCl8JfM!BCQvk01es2P;bMrZalhs->{sE%zBmR*wOTsqkDa+(QDHL$SyixVSBpvKn# zLO~@|AkNqNnuw!8K*K>>wkg;qU_fvIFu1w(C@C_IN2H3KqHv(&Xb)1lzhBn2Ri%|A z@X~e8F8)&VDciz7>5T7o2jy<@+{92X^XH$1wm`oI@1l|+dFCBZ=(k|N2f%<8etI4Z z&z4qJT{$}ZsJRE@zx&4G5cFa$u%JNmhVh^zXsV`1xC&$$UiKzaCNmUjJW*Mj)-B1s z)%2+20+V~(rh(vT<+n$3yp;bkBYNgOT}&!4d_c9WC}~6~vP~ml!M4<$XQ5g?+J_TxOYlng=Utn#z!%8d)CZ>u_KZO%pi3r$HTy zPym_)w7?8L-ZmIm;J1 zXtf_~nU&IGZ&tH@`SJyI1%h=^^7X-c=utR`%+1B1rU~K8in;yjuU<7KxZxP=+ktyql;}1 z{FP6*2ktPGo|iYxAGr3ESe#yYTJe9F4EoGPAB}rT!*8buhB2LR>l=0hiOmYmQ=nhG z12-{rMV7X(UiRsIzZXL!=x{0%JUysw3@|kMdn^D77!8|Vy^4zz8omzL8*-E|Ni#>K zSvc$=IijnniK>ppq=&(9>JvVOYei~XQa{P>`C;6IGjtMnu(s8%Q;JNz7!P5otTg|w zkGaHi|6R}(0Rk6_mG{zQcmkhr)otp3naPLtx{zWDnx*K)o_!C_mi-?VhioPWE67Qd z>p{a2po`|cYHyhuJs&D9iFB&)SsR?!)qQxg{@fg#;|xw8=E{Ocfe|wtRmRg){swv% z${U|Oe~yoGAV^vq`|ZaY5OVXoD7qOyUO7@0(nqS8df0arPidgmCW2(fOn0co@O(vZ2o zvtn1bFM>Suz!iy*+#FF$(N5Mck;Tjkn4gJY1;VN5!bGW<0MwK=z0Xn z?FNTe{`>onUGal5(#BL@n>MxYMCAy*{m#h{^B%FXu05^#L7HB9N9jNDrK1F=MB&#; zXhIYKxdLxp6(>Td5s!zI**>*!@ReHJ zlQ`vHep+QNl`Qhc*IG-V=Px{VU4qokHmU2#QK6kTy9jKbaE1}O@7w=aXn1kVTA)F( z=e*8S@c15Mu@^2t{~TIiJH9d;(dNa^BWXPHEdBA%L~|k8<#%w1JXI(CjL9$E+jM0c#XD=Y^$0zIfnRqWnl~R6t@z*r)|98A^I0m{aie6AF0_xF64YcTQXaUl)Pc_=hZQaV@ z!K=d1&N(h{RUoEJ`FeM}Z#SDmqsWE3T-cce_u-PNvJs(o^ZK^hmxQzPY5tUb%G&>Z zSig7x9kmtxg$J(6T@eD+COV8XcP3BiNa$1394BrtDs;tJin%rEH;P=cHNwRGB18B_ ze~~~4H1+=|c4ygF2?lK~W}fcvoVV)|bQG#*35|35|-WXc_+ll{c->##(#w1Tw@Udmly%o+mCDbCys%8nKIgs~k=09iA2QZ-OrqXLl69Q};*P-~$1WEKG!;6;XE`XmWeVcbLDJQq*7}Y+b*-0PK$-`5`o zEal%LjIY1Wt1c^o>|C-n`z)1n#D*4Y`rhq=nJd+D0{bWQpYF&B|IilFFnIl zk#ozErAbft2}k@AZ zWR$dF`2hoAI9=O?;+$Jn)z7(@>T4-2P3bO{2`RlV%i7z)e`X2*f1~t$YP1(i-yxv5XeteCHbh@@;i;LnN@N`5rUq&<`7Iy6cE@UeN8kkT`|URW}e@^(oB3 zKD#<63HfGyGI;;CgM8xuyj-7-4iO(qNHgZ)y@0#bznENkqcTXH?}7bjLoveC9)rp*oS9TI7=Kg+0fy61M!sd((c2*VFSvu=4>=#+^CfSk~{@ zS3Uvsp3g9bi@i}t=Y;MDAt`KBLA8DC8|fUF0mo@#s~U^R zQj6BUhSYr0_VNHuO}c;nqsFcyuDA>?f+;Op|Gk1FdW9-!?D^~O-D;YnhP1ICv@Tp$ zbd10mn&~#lJ)WvyMvU?C+#vw%MAz9(hNdsuWNyx<(np^YI5gc69m0Wv};S1135*Pg&lRJej6; zxz{JT7QvLV^2uK7Q-Z-S^t4FS;zILC#MRgwYJPe0&yP9t>R9S?^%dNV$F$DBVrV|c z8y$5d_O39`%i}Mc*qyGu$I`|h^=PV$aesUAB8i0H)+ z4ZBOqLK#Xs16ul}Q-9caB*pI-Bf#a+?prOwghyAYKKn{rac@3CTR>j>Kg+47QiMjT%5tXk;^Sg6c!A%e0p&SZj_1U|aBkD1k zZyBp{!ei>)T?9QN@9gD}SbsT-!F`u1cI0EvVJO2`7*x-s=P_FQ<;GpcI-TY@Em}u8 zv&l9|6sqoLZ)T6t>#6mjK08yuR#Zpl2e#1aViDhiRfZK19a35~` z@xmEp$0tJt(7cK4Nh`N+Bi$m; zZ(9WQH7kwZ;f%=c*m{?3J!bZ{T+$SHrL8(p|sUVGeuzo_&NCI&oe6WZbRF z>fBpK$GFt6Q%gDa+Sl%7Kbj5YYLWU~m9+dhCiL;NN-5cgz7Yla3Itfp(w7Rg;SlRm zTNBX8kdElow%i-AlzL$!+52=oKYyVzj!khczw~&kp#+PwULPjfl1yL=UuskYo1o8P z6PJ}}tRuu(6P9`JuM7M4JdNUo0gIC7x86DLXvI&Ixa`9Vhr|+MNP2@sJSlGwV94jW zE9WQsynUItDPP)-;EM?HHwSUGoqRuz;rN)LkJnzy9q~!qCX@T&PpCvliliuK4I~)z$%h zKV8y|w5*YycY6o1<5RH|-#=#C(_!>4e<*!epPSR}B(59^^}>?n{iM&@xLn(}oZlEo zSsdn&WXc{oWG7lFKhu?jfBm$KqkcGD&yNA%sGLh2bFv64jb7%C>$lSqXMDaJt-7fh z8m+aa1;$nLmvU|DocEo1t`4pFC(MG6g?nGSy(5pQZMzxXn}|>-S#LX78>0 zv#xAoBr~s+juWuT*WGpNz$MqRY<++GLvZ=_wTV!ou*|!OQ^uFG8@s4p{<@_5r}yU& zqiZMN7@ZlHo3mG(I7=S(rf6wi6D^Bi-q2Tkq#c!q?mrE)u3f6HZ9`!={%JBUp-PG_}>}^Ly?81|p&n&eFmoc2lJ z`vG7l{oMDk>8=mQ2-xvW1zS??n{rn>^b^c8Shnr+fEx~8l5^{u=xx(Z-~0QBL?Sm@ zBi#g>)uwZ2gdMnmp%tC7`|^Gm8Ic4kO1yoQ-h;B!^=v|l1tHMFjGfFa8Q5i|d9g|h z*{`C5xpUguCl8zMl`3c_Cqn-3_wnB|P8Y&X-Gjj(#BSO$$Zo&4)XA2ETh?A|%C6>+ zICyH$?`y!*HAJe@D%~jg4tx=ZN_dZY=xE_fT+b8+KODBMGUq@CQjM8TeItUX$n}b! z23`*l-tC9=k9$BKrX@tv6FKf~3cc>2e8vUu%S)Iwy_Mg8$6S(E^yR0+geuM|WhGQl zvxWwMgFB3%zXY%c3KlWbq;S7b*vgtTFKK$43r*g(Q^_OA<=tj5;g7!@#}ft&z5am zv!9GO{O@6aE*}OKCU0TvhAhRddGUZBg}`J0En$KZx0spzH95*x%pzzUZF`{K;-eq7oMui?6mP~9smFz9U%~e}I6#_Bgfl69l(m2;89i8Ri`Pki{qJpUO?z zrqbm@r_Ea677jA~b=*T|?Nz_B*5SIcbna|8`*afAD{38}_)|q#d5kl4uv05o-*M2r za;uYyHU)lzD*r2YRUZN*)RASk%}HR7r-{t19)emPsCrW`v+4w<9{NKlvEN;VE=EMC zE|mWHZ%BKY+i6Uf(w;9guxNAXlnWDHvYbml!kOj|Te|LDEEdI|J#*mr44b}td&tgk zH3cmEPBD#GxF#<{u6nL(?%gBI;6Q9PU?WMnd6Am6Q0~1@kU?uh9-x2O{|Ls5N7IpE z=3uZt_Ov4fP@&UErlCB~u44y3xglnmx9yzheZeF~5-5X=AW=7b6*I!ee)TUqRyYur z&GG3*57@)4>YcQW{;M?x6iX2gR#K1NgWS}!E-pmtU$wChNZ(g9J!y+?xVcm^(O7~9 zc6;98%KLBT3paT$JYEi>l{8HNw3%t_+i9BU4f=ZwtHL3@A~MznteE7wIR1O@cxsMd z!nWtiAPi#FIH~QPGupvep*GiVL?lFgHOe-IY4{GEfJc=1Hogs^CkwoOz4y|;SFW?$ zQhsYc4awCio9bYu0PPDxAsk>SzM=0@2J%)gl-45!y1ieSrZ=0mXxvm-b!yDSQ3jDj z>P))-)ihsttfsFeja#ewxH+bi{Jn?}vo$)s=Kr^ehOA7IGXJs6H%5=|is8#2@D+@A za|LZDR`W3Ci!o-`P<)ScKe}0 z&LP;`_)H$`!rMvzUSErTz-z)}FH15h&~vJ-+UB311IEt66 zO}-Lp^u8aDy*}FO8vO5i*%$TS3P-K-gC`562IY!;wa2KO2!pjPytTtrx|Y9AsdYee z;R3Td(01f(l?{wU7z?ceMJLxG8S_L_M@7N6!tvngqj^%gZyz}ZSi$PuOH>`g=gGDb z-BTErX+||&y5w*got3BL955m+sGclTu+N*vUI9gd)dZ3Ni^)OiFA}RLb@`BPZ6%Md zhShHLflHf^KU0*0LAgHvb`^N#yFLUUpk3nQF12_7ugeD*k}sXMJ%9iB1M$u6Uq&x) z8rsxx{=RJd!-B|IV#nDrO17Fg9B&?-3E3L{zE&$W7u{!OoWYqa0ulAwnkLmRoJGFt zq1zyxL|?;v9S#J$8m5Fb%e6=!zTIXAneRl@-P}V1`_oTFz0g{XZ!12lr;#kJ1^T>_ zk7<4ut&rtBLY2f{C;a$+f7s^i=`Pf$WmSgJ&okP*lcXO3yx`Cgs!ZisCn#ARw4I$o z*Rw7_7D}Qvtfq@9U8%)8z>o!e1C;vtt)0Iz2kXK6B9SDoKFU`8&s&m2Q#L%EUcwh+k#D|3=qgLfE72&C5G1VW&R`0LaYMifgvxO|uw@7~ST>}qgV zzb?M3=&lTE9T~wCF2&~Q5R*U865?%~A@t(ze@}f>yO{+Am@Ee}ja8C?cHLWFz%qDg zH^1};yIt~@fj(#O`DI)Jz*A-`eH_(t;GqU5=Bq0@I@M{kD9g;GMkYF@oY&q*G8a=q zJJ_64f~y@oeXGAbA_vtG;B}&EZv?+eX}AfnGmDJiG6a$`XwB2c(Xu4ama=TA_r%_jG&*ibw@Tqyf+5h7 ziYpb5CgQ&3C|vJHTT#ajY{?$gE?yp7%YEq$=p2%*-w{v3#k1WI?*F^<;)yS10>FMW zovfYx(!ZMnblG|2+_m~I-*%;Cn)Gv=q!PuQ+0S<8jxmp}FQ?vb3Dzpmd@0mX8mYF> zos7k&n{TRG)-12HHL&MATy>|qnF&gk6PxLwBuzigFiQIN;m-GqYd`t&BZ8Upjf8-6 zuuwl5qD;iDbPgtzX5LmES9tsVA)E>I_!sfoc45@SE9Qh0+ny=NXq4Jl z^PPZZ@jR1VQ^d_Q>y<8378Lhb@i=>aMrv3lhmw#eqFALb!^%x7yy!vo>gW=9fL#N% zFkg`>)$qq5gBE3ucjn?FaT}NR%cNu?!3`e>O81%-H-o2J?|IO&gk1aYUG|2`>p}H0 z;@BL+h=A62%Tmb88OQ(sj38F!@$XhaJs7|RqXGd!3LwmAcff=Jhh2$q-}<3-AJ~Yh zL;&AI6I$~5o338q-#Yzt=k;SkQ1Yq()%ZGTzEP8r@n^0ldk5ej2J#x|$+Y99)Iubh zx^cpXltCl;pLSWY*GG*Mfqypt>U?J41LTEEM;8!t z{@i>n3j|33GArvPF+dvyPCh*i>rq+$r)F_tm;YrTPhy4~S^sYaGWJWgaF#1ufA1%) zyq>LBGg1ogQ|;Qz&z`0Svi92A)9SuYM6Q?kRg*?<++Z8*(wQu7^jZbK+ z6h*mGXmndsR}NTf&QTxrl8WV-&~|)^?~Ebl`P#Q@tm~q(8l);InNV;79{c!;#r6ED z=19t4ky!!#Rh4VcKHhtxQS&LxsDM!eQAzuC74wC}PH=L;!&+aTSC>v&g%0|JmF_|y zPl5bhjFL1QzP3(e@|CZ)jv?!7^FRXpI{7LJxgU+W0})YGMjHy^lrUtY6vT$=a#iY*U~; z@wl>9c0REf%1Nb{`dXA_gYZJdD_=59b~ji&67Y;#;)QJbyf4K3=}LnEKi&Tnyr{3d zKuEa)3g|?m$6@l?P#{Ir7zP-H0F5{7k_EJJILFr1q-fv^`6tTFvl-SQPhBr0lN{FtxjB~Uo^t*I!lEuj8>u8n zB*h}Z-%N^#!dT?$mxJ$&1B3D)ajK|B`ugMpP!Z|9WSVBY#NDQAN-75AR?IZkco(!e z=h>Oesv)tr!|EU0plhpMb&|e;=7WX}>-?}D!d+wZw627JxyWd zPBvbj6zG&Ywyyq;q;B5T)UJXo5Fz54$rzRAz9d6a5MHdN$9^_tXMefA<*51^A21w=@L@ZPMJUI-Sn{zrH0P zA6u-brTUZ}1sG=f0sLdqr3VavUs6Wik?;2o2?-hQ{(otXSv^P_Xr6)K(%IGPW7tW~ zXZIJjAz0Us@;lD;r^fQJK)@U$5EZz$(r(%y2%cPH)yN`xDM6m52Z{zLKnc3eb$JtY z*1vle4T40mO>=4VJ{rkiXiYOPFAby*JlMDdqZ0>?q+h;mkDRE!iMg-!N5>O4$KTZp zPSKjE4}^dft;m%MpR@5mUcZ8${^ecIDW^T(kD-hvU}yy42f;1@h2-E3qT>31r!&?& z-7-Tw(K^*%;@IuL&TOFQPwyh%kFBnSQ|7Sekr4?wwNpXto{)g=kunL_D=Y&IZI}q zceqR|vyHQsHyw@dg^OHuH;vIXv4`k zPz_L06C4DFFftsegx)ZsBz@CSICBB)b{J#`p+_qVdYOFk`CI0nS{sQy-XlTx$w)oG zong}C%VJGhrcZ3TN{`E6XM7-NZ8&NT4bzj2i?vPZ9|)Y6O1}G3h*nw@O9T=;InhVw z&C%_>zI!9~zxHZYwzmWrfY^i^6sd4s;*Eh62ZqzSNz+vGaku}MFvyn)z$$%-$O0%u zmP2r{e4vnbo*F8h-xvxe$b>kLVHCZ-a4EZ%d1y=Q{3XW1Wf#O;64yT7@KlZV;{o@& zGeafoNK-^Bphatv-ZnHgnjg~+Djpp%?=!TYuS;Fb)j1>|XDM%_hR!0yp=H0fvxf(E zCBus;by{9!{jPnU%YtxrIw3ji{`1W5rM(&D#(^@!(2Ayd@zkeZ5b16Opz9s~9BBjAsyO6- ztD(&yrU1J|I-mAte+0a@It z@5NS>bh)I3C&O)DuVlb|Ry=z{&DiQ3V|WyLQ<~P;-Pg0#SEHG}MBv!X6o+>id17=G z6DNR;$d0S)t#dk#wYnSWCxxr$b249~?FD;%st(6h|(cG%<&xc^~`UoF0mSj6m~X zF*&P2BM9{lG@wAz5l`~Yj2ByH;pia}F8Z3Jwm~#QtU60Ig9@te)jU?_4WkCg>jv_p zKOD;5jl4w+0_+97h%ljQ!+DX!K%pU=LKGrq>#FXz5H7Y)l5)C%-kR;J`W=90t2?6_aKIFZyk&zpppw zwBI}H&pNBFfcDj4LF`XLMWq|jp=SL56WvRy)8L&+IOu!dKhBQ6h2v;6d=VjFtQ*3w zl>x>w5hy`atlZ#o<~LHGtfvFO-N;>Oag^=%yb-kgK@7;eYOELX#>`vl$3r&VG%h{MVJ(6$O*xJ66v%D|vZTy1DZkt=JHXZ@44(*OP_3HtJ*S!Gbe$ifNN$WuT?c|%EQ7t7QF^8F8 z|3S!aj@Nl9L_Q)JnG#8thxeq+0Dz`b5Xfp5_`4>-Yr@v!k%F*z3w4x?%I8s)e$H$E zSb1P2TRU$sC`^;Lp-&K`Om94rl^5n9LVpkefViXCzy8#`Q zFaE?Qvz8})@JDts5jKB5wFtU9#S0EbI|YGe;zX}$OXDg$EhV1dfbPw|!6NNy# z;A>Dw&0C&`ZZI6v2iZ`wQ0_E&L!Eqz=~;{?7e&7gvjMw}T;)tis*D}R2=@>)E_7S&&l;si*m481I5fJK;x((j zOnUs6yGUvPxKFN$gy0LGd^^FWF$1+-N7W34e6c!0ZRy$RN=Cu3%5|jkv!^f<)OuDn z0jil0W5$NcAHmk+F)^Q9@%3BlK2!Dgxd?O(W~}|SU<$xDkk9}P=v3(t5{lYTpbVDf zY1+ARz5?)2Aq%;^;jY)eKk<+I{YepeGHL0{Rto8Yl*(`2Ib)$?;Yg)28DNqEg$%wJ z{=x|A*c9q}FmcQdC#f2(3dFP`{`}=LL6LcnJ92M?gF? z>fKYH{sEc01`Jm+?<~dqW-J$^B`cI0KXAc$v8odmRO=v3WR>U< z0`|56VUb<6HONA+#+y05YUO+U>7OA%dKe#8-;dZg1Zt@rxy<@>H+Y*wBE*l79)Ity zZWFG2gS=G3llo9Ue^?@jr)4<0Q=Njbq2<*FN&cgTp<$6Ao*4w|aeDmGJ5u-;!8O*~ z@a{R6)+mR@bAwgHs4P2{Q_%*^_f!{D{^a%Z7rLJq_X3wvszmc>15Y?HSY^^SsM8n0 zYaKKJf;0w|9MaRir24U*74pA%t*_aa*($opkAy&r#8^u z`^;6WI`t(IDEaFhc2K>4v-?Wyd2wO&gbg(M!)<;JLGC(zJ*2rAscwX?SYhWq(EQDs zD}@_r;60-hl2F>kbp9v^ot3KWQ2gVDF2J?45vLNcV;=cB%s5_IFLm?{{2XEu0kOqpnJX#SZkpcuWPxUIl@GUq3w!c26aO zv_-X%O4FmzFVUvjFiZzH5$DO(U4w3yi9>cgOlfd~(O|uu5o?Be9rg_C&`#jT+ZiOw z#*XcL<@slfN(zVvtPK&V4luWB!5bA^rEM>6d(c%T-|UpX2)JRSFa7TKSq>hg6wKN+ zdjE4uJrD0tN`wf4J;a88iFby)?zXHThg56yDn!8@FjYli@su2VZg;y6O!w=PM+NP` zr)mV37w_1w{N1~<&qYDh{E;|CdnYY=Xw3xA>0(GJ4m2^|z1eAcX12_cWTT_B&>X-x zRG&{5>Y9&X#qRsa{1U{PK4VY+x^Hx7HtjknG?Wg@PP@Vfh?nMtQ`B|PQO8`nPi0x& zzNEwdPN;PtwvXgG;j?rw@FCX7LD)3jreBgleGAUq|8TsMma`U8DcA6ugIec=|7)|g zag!)t%?HVn*|OuHL`ng0?;55r{VE^$ku17hlS99y2=nK9_2BuPz209v#(j{{y3hV8 z<6kJ!R0yFr{`@9wRdmv+dCRTh*uWOrb4<~FMo2qoo5n?d%jWn0nL1)xLFuLcAT1&V z?E2!}bo1u6g6vM1cAt)e^a8CO0<_GEh1gG6)LiG_>2T)ssw6R>!mXfxXYdmkeCgK6Zy)M^v&=egyp%h??vwG zT5(e9u!`TLr4#`}p1bsS+dq7_4t+cy#2l`b0^9ichHNgplfG!zA%2qkTcT~VnafrX z1myhZoyO_f_OTatBzJiL(>^}ePC7(2@U_5PDt5J$v$sRy?R&;S<&Qb9smM{ln#1eP zsIbj142t0b8}9j6Iiaf_>iQi3ZQgu7|6-8*J=8s>f(jz(b^*QA9p-BKt_{ z%T9C>{V!xdJ;9CP$LI4*XI3cfW?uBAKoS=g`c$i>>NU=Gd>B7`ZU?+e8h~6s6a_4b zSZ6ir-1sa1cIV{F0AC_@=P52t=+|{NeDmJlzOL&JFy&)cy-FBBnnS$AXO}LFQ3q!(X)qW|+ZNyZ z7pq?Yk!KC?4N=&QjWr2k(+kp4XcmP416l-*(yV9dJYUtz3brx;6;*mjJ(9@x!X7q_ zc)4V$BqEC&PHF7Thpfw?oM*24Bi$7Es%GFC`k@nQk~aN#q%=k*u$NG7}uUF!Nm{mNW9^ z!L`(9gTril)#xn~_k_rca|)xPVJ)P9ambwhmJmLH`*~}z4igmR*ZVhTCrb1jEJ-C6 zlK&Cqg3pR9Nzn5>$Ijusgohqx9)wZpEW^oaf!tdIXUdF(g%mMa&m`Hs`d$D=m7jPK zCsIkbwRkK#{?x}Oa(*cp5n~IUt|$LqOn(;|01hRpLBk}#OB=TaL6VW0tU8`jCW{xY z=21;EY$|js)ji7=Cc1P(pk?JJ@?Y?q$ww?rr`o2T6~yIhXf2kM{8!esN5;e>Ph?GS_=j}YJ9p&UMcXMfVEL^nzO~ktLFuij~_rk{ym}? zujBwJalm4!bW~LT+;vit2Onec3aTlZ!FQ*a37aZCBV0w-lB>LXb=Oy9t%*E_(j2~P zO1$J?WxB4^%e3l&5Wj;Hj*Mi4v)G|x+w`(C6krT@s9$~d4SyzcARPQeNOF`^VQa7V zHMwfZJ*wcVY#|Y$t{Y;j7qHZM}ojHhcvE6!kd5Ma`Iy4XSeru@O zReFK!=uk%l`5mwG#v)U28V^kF{nPbDB z9b$szsB~e((ZdrdF%}Y5k1js5VqUoh0c<`S61X)^aD!Hk>VF z_nyjh8{Mj@E``dp-Lb#-jL1Y*_eB_;ATKT8y0wCdqz%@S3t(lc`ec+`hU`OLL1+K) zCLu#WdijipdiKL&!Qn#;7Em-G9RnrGlKer z3~usN79Tc9?S)OFl7Vldf&sW=&QZ}5Dr{MUX&jc_^V=-6sPSlT`g?9N2Y|OItKI#F zSHYWRIr8e2ZQZR`gPU;`gkeoKR^Qtxy4S0xZhj$65L5JU{=q|XO9PdNK_tl>o$hsY z?SBf4J@_tBQ2(RM2d+{3X7zb$>g|JeQcgv>b2~11Wa%FHv5NI^G9tB6sa@YG&md_# zp!py;VrS#_(bY?45#Ic-Y7UW4lq|TrBj*c@1zqKqM|0p61Y0N7%8%zHSchGk=b|yW zBgekgmBoG(`sZc|aR#B{XfQCkup>a}+1iiKhHu^7ZAQc&u!QX^QAC@&fu*!@o5#db z_0fm+EFK~w0hhHoo)~bZAi15aU?)1I@sj?;d9;!yIR>oaLsMZyxn6RB2Z@R6#q?Bj zEufKWblm*-Y{D65w?$tcfW5!Ab35q{9V;$#DZv0>X`*n^Y{-|N#&o~l`K$)=03n}U!b=bo^s;`?)JhanC9b!0zLHpJ zq%mBkRppf=>=enb%e5 zCzdc(rwo4aKR91`u~N8riy8Iuy1juV#*|V}KiZ)P_t?fXuWXzb%h`QO8LxjBkV^|Q zBd-8_q{mHQXkAJp6fN}vJ=|VB<8QH@Hv=GUG_o*(;BmE1nobRJs3S<(c27a7XDtL;TZY>Pfs~~Mb2l1PQf0p`~40Lyy zsBJ91>gS9KfG1fLOq|)K1R$2kNpZO?QiPIo1Bgx-pL!L~XrEPPuVJR~>c;i76u=>2 zYN)hFDv_oE6b6nUf+33+;zv_<$FXUREv{nd#!z1N3PWKvT3!b#I|LW6Wxm-Mm1?V2 z=&&N-aS$9GW3GB%66zx!HBS@@@s>2m0gaHo|6Zx}ogMeV~`mKx+y5CkmN={3B)aT-}_uEd|&G9->z^KnIeVM6%TvPe&v`4Qook+t2IP+$am4Ih8U$kzvf(@u@EW|Hc zFvKF%{$fw5fdh|(P1rO^T*QXuN<3gjZn1hdDb~rjmc-aU^ynTD=p40y>n{q$HF{9g zonaLE<7+kWCQ>6Y$r`o<;Cf4VW>ur%9TN$5q0*UD^O(>p{k+uix$4JYcjkuagITHX z&2}CSS)X3N304EVVx`-ER$NAkunz@XDLuY6^R$#X#r(+4iFsCA*8HXAg7wEDt`$9+ zs+RMIz)2*9bYtJ98FF8@FB_(Y5W*ajPN6&J4qB`_xwM58 z;=8v@!Pw{x{&7<9dc~yqFx0J-EsV8gmSj4pGYpX}ykcH!ZFPL2q2*~ONLk{kc0t9a z_?T7axU~23WaLc(p3$}0N*X@y8;YE6w{euh2$ZGVvys0J$tYdPr16#YCZ8h<+U*z7 zFYU5Q5GAxVh+FVo8R^7GHLknR!x+~C_Y5N_8fmy(K-L&s>MlMX_F37;8JlkZ!)rZz zT&K%`&jTNUFlS%%wnh52Qmls7?Yx_z10FG$=W#fajqte!zgpU5c)1<5$G~GW@Bg=&$Y{Kvpr*S3wd!a_Z(;9Ti0MDp+E7FIh z9o;T4IIT3pw>fkVuh2R==*q=au`-!v#VpVvuS1N(YyBzMXdrnf04l5%Q)U6ideYbc zuiZm&)K&wMW(`dpMdPkaD*!&j^i3%)*J@-k4=gD0?Hy%Pz$cFMO}0!fR&`}5UTR_L zYM$<2bXlzy*|fMGi_N->_T1)ebZn3f+54VrMu{5^hhPs_QRpgBX58ExM1$Wgr_gC< zS6WcF<0DG9PECH$8q4jv`3^@9VJEYdbD=h8-vz5#i}pYmMkbE%W>aJk_t!_Q(VBdD z$>)Kij3xM4gSfuMfukh11%oyCc`0Sx2FVO5{ALTX;gDs;o<`W1WF^B1Ip!sZYm)s~c>%WLas zZ^p16UNEbL5WWs}y4=@;YHy}dgE_#6_!i?UeRFCOrakYhK60o93*DecJ#@wm6Rj#d z0nDd?Jq4HJ(Ow%1*8jO)1TE&NK|B3HP!g6M$-X!r{OWJ#Uyemw7mFJ-74yf(v>Ez4 zIVAO&y%MbQu9R`f4h&S=>|=r3vo zU9)(%JXS0{0u^3n#5}J&8S(1;^-Zy0Fn=h_Lwvk#zx`Hq#-LlJzCzKpl?uMkbx@sW zaN4Pyrf`_ zA%kGUt3$eX(I6f;?Qw?N!OusJJ~TO&tb{Cucg2^2=kb&vL19)M81qO&!v{<1cQh3& z{%ZZV5%Yc*z?U2LtAEldm_D@2P5c_sYPq2fFIrbey$y}t5qws+y8SwK+PCCbc!^;E z&z`Vv-onwwugyrx9R)z?Ha72oH_9syxIk@ ze~~)6w4s0f=C&lfClS2K?|pN*$JoAb$&1||QZ!#l4A?QLl? zQYM~c7ha93k^74Y{+Wl{Eq1^EYTG1v7++x%VF+TrwSG54>aNPn3eC`%?HD5eP$`ML zulh%4KoMbUbIdPm1Ya?pJGjQWh!e!hkhMTl8dA;ALZrz*>o?tZ6)_PmiVzD&eHvs+ zM4tS)dQGyT<5qSHWj04Fu=TJ)d*ZgR~Izv%cB(hcAvu!KkOfr*oEd**{FKw+iU zQm{HiI*pG0HwAb}#l?#M0C#pT+zn!S^4x{gUE&j7HY zHI&8l!xpteiz))OEZG~qAL8?4VdalMaBY(Kyh_s^Q%QTc{|T{#fTz4`#*o{9^KCMg*=QJhI7t{HHfV8$9KN9P~Fy!kiqTLZ249y2DO83b^TTZJ0h!z zb~sl%geZ7tywzJ+E4@}(Ubeye83v!qEUrO;1Goz={-yroso8WtsSR{gWira|iG;Rn z47*M*IDw|-P6X0zu7hmoE|mBJfhr5vQ>sxUyc!}KfX*9?DnO3ET>VK)q9y@sRsih` z0&p5XC!7DOCEE1ZBN>s^&vLMQ@`iDV7nbXcRCD$-tXi*NLPbl8}i3jXh<&EE++y$%Y#{JGFQ(4E3Dn;!pUPNlE&2x7p z^P07lYyB-@z2LArEj;wlXYkJrCg>-6;gD#fs#Ko7sU`W^xo8F{DIKF?owT3_88Nj* z>`ClZwy7`sKa5?wzcBf=Y8U-WK3*s|44=r@7?P2B(w>?bYb?)+^ht;Mkz7D#0Psr&F>KUwGqMZ%tDT$v_2th;v!?Bh%&$2-Nz#=jxG;bUc;h}^cli=K zTneCKE+IyjMC;3W834hyc9@sGc$Juvl^-`u18N10AZSyJd^RWr!CG2xNE^Ra^vUEk z4*|!jIC+Tq@wHsgG%Wi*jfYwwj0F>|p;uyfPh|0HwoNusogzf(hL0HCJr?hsZ^8Ke zrBAW+*GsSWH!sNr7xg%WLYrtxJrO~6uibM6(GPacy_Zt)z-BXa{YlZ#w?+JF4O)#A zmktyn0St5=y7H=r5eDjolaIOK%n8G5-`tdMH9&LP9iF*)pY6TR?!|9*Q%=BOKN)I? zKDbIhiUZEY7<2a|IU4Cmo7p(}qY?$W%`IrcBMucAR4;cS8Zgg)b#%yd1w-BxwL}?5 zQX<`6_tg(G*zB0`>6tz$npnfKv;?4-#=y>kTj^-hJHOXrxzx_$c_~afL*y+*&lzo`zN)U`dT+D)+8AL z+L;Hs1@B?74hC-Q1f8MsF_=q|>fESM=W#t#siPCBY{AHT0ZmwAZtSmj!z@wM6t@avz!g6wAX#)=-%WV$mwpyV26%#(O6`E^C z8q%Y>XX)o)&8dag$$>*yGC4ec-E!}IIDjsCox5_&#p254dP?4d%!>@A^CZcd7WV;D zRO>H!!vJI)2~c0j0ej~Az|j?Opqgv|)dZ*Bigs)0@o^Sr6TyV3 zeG7rD=P|d&6h|*qU+V{ADVj< z52$H3KS;0MSD<764AGc<=asq8b;BZ+f3YUDp~B|qZRI!VTV8LD#h2QsH;`Pdp1)il z+^KGnu9-(rhj;hgRgPsPY(6WWzn2FTzON~podd5&)nyXGu#JGA9&twzPzQGbgSihQ z`%!K0a3%4a@Vi@>Qx0=S9#B;BMz8xDd;N+vApIwyqXlJE6d0CGbC&0z5h$d24ui1~ z*n4H`4Mq^6)3E6ct!Gz>vD1`5OaGqiDogxvBvc7kVav%R{$_QfT9UbdECt|FFvDWk z%p3*td_hnkA9)F~&@=*GQHtvi=>d9D;c{9IHXi%8a>c<@D!-FW@4ffJV~-_Qwd?xCr6V6huAnpajhOE>03d4Rm;B++U*nlQppTY* zQN>go04Mnxlx|YxT>RaGFEIcouoM9G&R>Er0lp?HFr=^M^6=Szim+IC8pqF!I;=0BVt>?GyCSOK# zaP_X@#E#b&sGco-kUV8CrsON!kZuAJJKi(lia+zAd-Xql;kn8;SWyN+-mDeYMNplw zJDg2_?IvP!9x;Ks?SrK;BER3>x&ySHUpm>ONLhZLJ*JAotB4YHa-4;*Q5Vo|=sP$w zh)WB+X~j~hkurdaH1G1-!yb{n9N8K{+rFTID}7h*JikVZ>ZNhP0u70Wjpy2;^&z`2 z?gM=tV+kG84AbvffCkua8y_X>l*&P)_*|qO2oA=x5Qg7hr%-H}Y<$w1oYJ{x2pNK^ zi>vj0kyzCaymE_1UO)}?(}0TCaW&2ZLHmbz05Sqkl6~Om7)fMSu=k3Ob;)Y={NtyN zB5~p`6j6=m>eS;~#F<(i@2AMQkiJQ@-0cACB{byZG(^)|r$Q-WN0am`*IkDjIbhTcod;(kH)k&Bs@kWcjbh zEx&LNkqG5(`qCBq=)V^~TFSX!x1?=Mj9!n>{oLR^)bt6f;3z1jKIw`-IA*ePeU-cY z{a2UG=|<|Z85-3a`LO4AIPI;jPUbs)N-4T>v-oEP>xT?T zx@XPB3A!uE4AqPxdv`OU2rJB;)qBLpV!QyW zj|n}!GYRZbSZ_Z`^S5)}+Di?<228m)B!3H5CDJfpJ(Y5@!WwIK>)O_sz(^%zoSMzl z6PjTc^BvEFt)+hWm-WS>)Ex)MzIs}7vC(SnfvZfU3>?4};?AER=0I`gIQw%g(-;_F ze&rH<3rt7^27a+S`=OO#gL0TzuL}I9^W^u#x&sCg)BLZ_sDDQf|#?^m6O|#-`|A?3V$zi#0Ks z)o)au2zn@8nGu^2+wHzO zYti~B;>qX@=3D(~$*NKGHRy3CC>nu^zn3NX&KBO2`Jr*dyY{8fuErc~x75-tzj3lWwq9il z`3ZcOS`FI8`lH3I>9DF1p}gW3_(u~BV(>furr$1>=p_e@7yw+0Q9aW=>YXY6rskgb z-4!vQGcnL$N5kQ^yE4Ic8w!xnIWHdzFkmpLWaH{Dz$TvzOok-`=Z@#ISecr#+u5`cP3#<<@oX4yB*oG|COVnchFz$2 z3Mukl+X!0Vh9jphjvS$e?QgD-hdfA15J7}p{H!_k9OV%hZ2QZTSP7Othfo-BPpP$O z4t<;4_MCd-7WNZ)rwhO^@mOQnNrXBuftdIFZJQ@+eAONk&3``H++=ke`!E?n3R%d( zi5_vkeu-?_ajLn!*-~eEN7iPjwZ)RR15>lmz3nAA^PRdrFQZEWMN6y1=w?ty4W{1V zyC~{90{UbX^QX1#k@$(wBT4=Db&RB%vXn^x)L+h-E-3|PK0qoso)SY{;tFEcBqCxE zppIF~|I`o;0NI%or%!Z(3wPTBZ#g?p9$|Lh{|JIvSy>gC??o;*dS@)1s^$-y4_LTm z2E*6veM#oCq9(3_t31!=>Cjz=e5CCK{4D(gj0pi0&Ix@t_DX3*j~O@!Kw9DeCQTXk}ozctzo3#K@_` zY-Iv7t~Lf1{GF1p3&G8(fEP@t4^f(C@o)axueTRCHs4rK=3Vnf{#_u=l>8}}wVf;GyUsfGA042qP-tp$5$r7CbJECQC{$p0zhHr{cfZVts6*KFrNCQ z=MFKYLckV;xp`D?X00d20 zsB7+Ml!5)$>A55c(MXRnC5bkoREVgrN4V_NRO8FLTocFElrj~!e8cRejOSSGEM3zB6M(%ek0ez4kaej@o%SwA2NL?ha38^j(w^&oV`RaB3rl z5Ul}9D;TS$F9^6!@0Lc$^SF(t&^OlP;kk}~o=^Szo`jUW=v2d69h980wa<8+ALI-W zpNbbRowUu1sC6%_#H7&{3{}{+eE2Q%)s==LWX3aS91psn7{YVtxu1?$?B|v}iuzgO zllY<|BaA}ul#1(wF(4#FjN%NKgL6uXaoQe?dgG}}%Ik=t4gAc9_t!bwHG{DcmJci@ zxHKD_He_SsAtZ(6;U*qX8O3T4^qA&8VOvZ>m>X_gYQ!M97Y*RF9n*9#5j~aQ;erMjT2ri=R8Qmew zt#v28J}-0s-GVGErH%7W`N(M81;BPoozuXjXPhACEO`fi7W}pM12(j!IsO(H zZI6AE==YPzcahm!nW@5YE22q^bYhP`QMc75F|T?b$P7;D)bm) z9`8GV?f=t5N%%nTcHU=zScMaCDy{A0hD3j^RL5+|xqgv^Bk(#z1Z7i!L?sT_)2{I~ zPo#Bp5Sb{#7TKnurqDx@_svHJ?cF=LQV_G)81^4rU zfO}ak_op7+!|kg#EWy|S81kU^?mvgxtKA<&at;>CLO1wEkN;BVUB-pd+0BrIgoKV^ z7(AbsBcoE?s%F3&tW9%(haz&vDk$w|agb zcf++G@Rdk$|)H@R0kkpE`EG^)Btt5A79A+4-mK40T!q>a^39l-`v zB->qYGYp&1Fw;kDG?VA}5K&HNk@W4GdY>vkVdYPS$@3Sd7Hf5WD5Rds!W|cE9cPmf z0US6}{)r~$&j2k6`Vq;;E#i$Py=_r&Wl$F!Gy5e+$Dp{4BB%t!tfgI|sFQ6OwnS{C7*uZO8S)K$uQ+gn{U*B6J=}kgnEwpri`7tnS>*brblxta|6#iyu@IIpGNKfL znTva#*cc5Py*+Bf>dd;EbLU$VZDUV5Hk{N~spbN%JlG~-Po;*a z;d+2bV(6|U;0=T$^M=Sz(8n&PIfF1C@BUK#J&U@>T7VF0RosQP0rO;=JAx$mDD5Sv ze=zADH;kZ_hI>yQk#~S12a#tlb=mCIyPEMhm5vSDbvtu&?eK5HiR}Dr_v%W}ZE7{sg+Oi^&e2_2?7tw_m`+0$;0WS%xePc{NQuH@z#!z`)Dr!xRa2RZb1I z2(W!9w_OwJ0<7K)bjKUu8&Do&4LR-e1*?kg4hP7B28-v;5iNh7MK$VHOtqsCXADHj zCH9+Pn&08mR}X)A8YqKw?ILW;RjrloJdsHi0VG@$^b1?H!nge4@;YES3(5Pywi5|N zX4m+CI8?0AEwQh!uWg4@8oYMJ4DhVuYTl|edv<@>!D}k>etA!rXgL3QB22hXOV6%< zuKU%^?;^^`5$0754@MZ=->3hNR4j1T!TF9Z%MniCxE7xBVNeD}i=wOV7dFJA<@z0n zjcSq>9-^U{A1AGXKSO`QI_gu}jtJ9-8;|QK2XX4p*?6GwfoyPr(pd(I;f`Vai}tjd zGXk!cv(rxLV@5wiLkito-;;evdYG~bJ1@*N{6+p3eS|0y(E7qP2E38w$(fw$5<5u9 zZD8;{;}43z5{^0tj7Y3_>%81AWTkq;hp)~nk3IGn+6oIiABPL^m{fTB3U*3PeyVA+ z!1*A^+c%b}9NV%Jx(Qr0(#Qi)fvh{sP~y&}x#=9yayTmjc6~bxv1LR@7L~Y<^502G zpwGZ1^0rI#OvlOEWAlTyEJY-$5_L6@U!WxNYp~qgiDIKllj2={kt3)L?yH!;LY>U$0y&F+T$K zw&@sD1xkLpwkx`9emUaQOJ`;cS-`;;wwq$8b&mv3$Z0AHNTLiq%&yfO98JpfzMZ(v z?6;R1@L|ne4#{~ofGv0`{i>^EK}iXa9pim!^_SM}B#AkM;Q=ycGKYUg-@;x)Yn~Ue zoT3f!eyNI5<0z>k#;e;p!)v<1)+DJlRoY<2U)4u2?hX|p7u@yjt@E_13?j<5h8@ff zB?>X7t8?dz!~6Ak8H)FLx>e3Q$=c36?~xJ2Xhc0cc+b4oo_-#dm$t&H*<9I&H%>yF zpo^=?X%XBh3EWb#opwF)vFR%dD$pxd8|8X&@)inQAeJ-Wh8xyY3sUeh>rwp{EyZ)n zDyL>nM={?P{sto3UOB1#OO@-l1;>*+HU1ub_dF{01WIzY80AYeVlJ%=;kVL#L(@A6 z1b#Gp-L)H2T%Ne+>bA=I`&3!M=V+X^=6l$a0*TF_y+0>&5BI&T5)qzX)Ihj7A6CAn z=lQOpNds!zN7(VPv~>PW;)kbcKgfTX;fb~7 zCKCAQKBdTeu_yAD4r4Q8{esz4WtoBaLgBH1#@Q`RKQBz*nUAXdK>CkFpD{{X<9Ab` z2U>ykHShoXqZiC#g=RgCUTqXC*^%{Rm9+|CKgBAc7gijb@9|s}1QkZkqm+>;C%?FNsuNn9S=J&KYY^~H_`}mGmOMgM8tT}7t zkCnqJBD=rGJ1wC%?01kx6s{Rg0peSur6a{!FPzilHB=Fa`a~a4_VJ$a!4CeL>*rIZ z8Yw3Cex<9&72e%2f4^`dZN|ojO97+xIwF~Qzpa4tyl><(@~WCNzUNX7rW%@*E6FVX zD&HAUXlnXq9lA;n`Nd&So6Y6aq2jW0q5zsiPnwd!iyt@XKZ98;JA_7u?-~uh3cS^C zB+JsLKE-b_mB(P12wjtn^eY>)c)@vmXXr%k2zll8|9G#c0%gZZf`}RCz1oV6`PLDM zq%v9e8|ouSyNuvHUW_k*z=urZ)e>MvLkcs}ir&IHr0Z#{OTd-atvVyw7MdnAX5+5y zUV9Q-{EBQTe6>tek=leeA>Sotu@;NQBw()s@nQ6~;_m)0Yc zTZfg18~(xwe$XH7?nh0GKkFwB{sQmhQ&|vY2{tZ7^y)w9{pxLziXuc5D|h(Mz2{xZ z50i`&^vs$M4b13E&2^PL0%hcBwdBPK))~O+J{V;h+S^S2eH)@MJm79iDnzDEPfT?= z@z+g0!V$nWI{#ofivtQOBx|jXvP-YpvgCoC+>l9}Pl<3`>c}G3Zrb8KV)J~ny&+q$ z{Z(Niz*7CC_6r^W*-B?VCF*28t)ypK&nO|KcwpE`pqoA_`KiA z-4aRHqrO)xknc6b)$=ZY$#LN9w=?42hA~~t5dd$wu}XMZln|4RojUTiyn=#X8E-xl|~B>iIa>L^*ckYyp9q= zwpMJW)b7~)@^qDpY1H>1JHS>@)K*nP$q&yfiKx*TE*D!enW5%0K2@Enfg0iWpB=L6 znQdkMaj8E(sb#bvdCfD_8``bBtDV08DD_Ojia|Y)(mU_`oki&YnYf0xjvU#5l>Fg+ z22;@8(PCG-l~VATU}?ds*`#89rA-1qXp6cE%$u8*@L`IpYMuN?24Pz&wnrm5FD-r( zPjj_T873inJ5GvEdZ1k^(b4$qOXKEfd3fZVs|y#;%LWc6zpw%BDi z-NJbzgs6T>N#LSf+1EO|(PN0A)Dd>$d5nWkkX`cvl1f|ByAbgskXzliX!5DC|Myk{ zBo;+}yu!j`=jZ1c?%pw@^~{XPXlioKZCDdc7)%;AD5YP`eiCcT9nPS|NGD#%AdpfCy3@y0q$n<6Du^n?_eN(}oX?6U(|=(QMS3yGtRip}FW zGZmsMf7>-`Gf&$TZi6*Rn~QpZh0+0|BKO?Oy#5GJy=1kN9TZP>{=Z0u8e&(RQEyir zV1t=|jtdT9RwHE0b>F}7-THUDcnbTR)7tW@^Bwm`uotBi@(GSej*4ZtfkzoZoS`9y z-kqoa96w3agQ`){V2`fN_eHTSA!F)KndOAzrx{3;IhrI*z&f|gU+s!kPx8^q&DO%+ zhr)3wF`hx~iuw9L#UuSVH4Zg5b}nLaD>f5|hT`9F^-?Rz-3&9;N-d`^9IjG+K??qA*|6A4sMLb-p)&E}u|kQeHW{R#g+9Yhgi^t5>p%Uno=DL{DAG-l zTPc%AnznqAC*Z|gn|f)~2Jw%V0KE$B%_QAPNjq0=#fAv3y!kRys|u%#DlvnZ_R@-M z{vR}EuH;dh6H&6Gc{-_IY%&d71~n_@ng5(BiS$3#*JT8q{7$npe!8n=7}UGSlJDeE zsMhWO#|Tt95^6fyvK&R4y(i}29ckrd*-|1Gu+bN+@bV=tiGS>LR6yIJdDlA2sPOon zxP~8yYwIxf(ms+#+*_s@?}f9$#im{Jdlc$WWUdrfqw!6ZbwgJI1K&rAe#!f`Y1V?0 z7|bt=s(Cogk6}*Ao#<8g7Z%>So$WV|*3R)3#ceQo#Ac(-Zg9(Ma$TWsD$A%j{4i;$ zwMIU}pyJ0`N;`zh%m=HI^u8@*np9({6r-X!WCT(HO;MHW7PFMQWFj+mP|9LX_d+Z@Tv zMH(~DesbJ-=F3{+dtzgjD+IH%@i`BoENW>o3$6DWYlyF^TQ9Wkm%=}BXC7tSsIJ3c3LxssG%Xy4j{Qc(P^t?jsJ`XHqaL)}sy)eSB72VHm_c;qceT#7+n(8HR7Y~jPV~#2 zyzHK1+5#|~MTPy80V9X6?)PfB)Lq$#61}2{sbOQC(b(OFyN2gL<(6)bx~H@cnv~>G z0}E$nV;=UOc`eYT%V`iWu!89$S8J^#bF`t!(UJT+Sp&w(6sip3T}-bE(zcR#^D#57 zdj{YAMQ$Q!g`|RdtNS{!w@@H)ok1v7p~t7FvoYMdg^Yy1kJZMjb;R6>@@tx6o7*rd zR7J0q3Wsf}ydi3_Q>29MGXI`~QD8-0mQCl|(SmeYK5Fl} zDC$yjbOh`f#*7_IIj75LmRYuYs9c+FC&7D`Abf@!*ZdDy)HZ}#D+S$j${vwaO4E$G z@Ka7CfRUftTG}5Wf{og|f$XFy+@Ip+5QAP=zx4Ck7owsACx80>|sU=|!@x@qmW*0IOfH6MqA5iA$?|)JjI;sbwl%hxD z&btk*^xwN_8Z^D$DB5`PglgU8wXOFRoXY3*QJ%xll% zENs8p+ZO5f^43eowK0Qjzxu!ohJgn~i5%S`o8dI~*QKTFz#sDmrMY8sUANq;FQ*OMj`Q}FP2GJ>2uWz&?8&e`#WL;B4;A@V zmKoA4xiXStHbCS3>bccv3fpw`y@(v;LpWFR!ugOA*bSC#SXM`tkkD$f?W>2c@G|MF zp}4R8EWwk0`ro@yAXuSe*Ult=YWI(ZXhM_OA_n*Vwr4=>X?(LV|A@_zV^m#v$8$1c z7(|gXVZ!Sp^QJ;4I@Lahim^D93GX^@-0xfxLzI*f>(~`Lr6shtDkZRpXm(!P#AP}j zN7oTSvJswlGf8Xb8|!I4N~ZJYV?oV3$l8KK^(Go1JOZa9r3x3Bg? z5jn#0kBlwFMC1{_jw0p5o;j5FgY%$1iK>pY>b$i+7m-9ukT+#JE%}UAIT>k%*`D^D z#cKJcK~%Mjhu?MpQiyDPfEoQ4j*L6|t7%WJqArTDn2 zW5u8kmW0Pdbfb5ExeI~3%1ket+L=_TO;VrM-cdTtxDwmmnzGn+l8d}^IcQ@)uW6O+ zS3-pe*LSwts=fFLVmKOPc(USZ5)O;9GXs}@dv*pHOrJB%RYuC2#P3e?P4=tq#IDY) z7&>E>-pkLibu_K8eGX;$yLY)whYG;_Gay#=VudfpXQ*w5uDF9j%#Yh-AM|*?b^{9D z2b>_;P`gzRA^}4|B?LHLz%Dh&VGCsSGE-Wjic+Y!yfbJR5l?X{uHZ&k=>QUZS9v|A z!mS)a?hoZTuWqln@pAcmy^9dqqj$!C7PjhmQBhG;m3;KOsBi$#f|?;ojL0Qpyw4(p z7z8;{Y5F`GIU0Q`*xAp0^apbOU-?^`8`N!}Yj~UXp0xaC{bj20t+mU|^oSqZsOBrO zfq}t)WYw!Dn3!s6W+o$%gFpZVtmWH^<^*2!;qk$6o_dDX=!DF9D)j&Ls*TULV*+Og zN#aMtOXA-pfA$X*7ij|MKAwp*(Ne<+c0HI9<`mKXqeOqEQp!rC?G5GDrOd>T~MizK^Al8KsYG`^H7nkv64hHm&d4 zDJayxXSJTx&$IiDI z$y!^z$G0$a7LhQ!x!nZ%`B{{RN_aZ~j4(y7G~GuD1H_!URgiZexm<&6BH}1T{GbWweo|4uNUqA4#Q=MQ~q|F5bPh&h)GO~ z)0p_3ky$gOb19k1Va+EuCuz->yoa^`22*5@er39YmmBG~7x&cNl}^OY7>Z~(Oh3%9 zF?04|aISQ7;RKg~AM=IJWQ*vndUc6dp_B1l3!iMLVw~Jw>pjLR?_G{s-^_6c{6V{d zjZ}6kjf!zpH~O|Y==sIn-Qj3kt~qy%Rs1UwAL1Km;dYL>d5lz>@Il@q)NTxkyVi)o zDnsBeJ7KhSO$^YF#orIU1=S6FI;-CIJc>j*sZjGEa^)eT=TsRpk^ftNL5mlIu~CnC z`!D@1y0N1B$;k56CFcgsvu(i(69DsoH5wCsvW0PuCsWQSypP$(`1SxWK72Q~@SAqm zCBYbQ3j-wb`qK|4U_jawP(3<5-3_M!t?y)b*lj0%XwgV*q}fyTq&hQIJCwnqq5SFD-s&N=DJ>;Nk> z(8SSK4!eq{os<3{LVwS=pmeV=Yii(XU-d6Dab_>5=>wl zt^>LCGMxb+rgY22Xl25EsW}`G$LrImYE&*owubf|K&cWpy1K?p<;XKS@|NT$c=oA)Muj0bF(WBmV_o z7i>z_a4|sUa^8#VGATvW-<1mkK!yRFQQlePc+&DNJHr3h`sd#F(e0m{?7;Rr`Q%fW zHx4Um3@yObZ%rwV^oqv|XV+8a^3PVTcwbB}MTGyKjShE**M-_$mT9fE-v*%2*4INQadqpjMKL1?WXQtbiEhs4 zfQ^9uiI8bWJd}>y&|7QWa*-uNTBUU<_eVKvKle1E9*0^CGI6$$-oo<|b)){}#Vf0uz3tI0$PO0DGa-zJE_hD0M2?uv>{Z11F z$NpPuA`J+XH2)yn_^8&nOS~UMBCAbptP_=5;Y!}5mwhIUf?2*d+`N;4| z_D%6-!xTO`Zh*w_!j}B$UblN2KkIfsom5k!G7ak)C*1X_t>#%?-@gj9a5B77V$-St zh11Rq#R-imo+qsdFwqxVWBJZ}x+VrD#$2C%l%+1DIpZqCFxb4z;_x6Y7C`>h*Toly8Uje~D;;ba$`^*wz5c@z)`#lX8o!4@ zywNG&?xFt;Yg>=&;~tym485IZbu8+Pdjoka2AkW@v^D{(UH8;1{ zi7BcE{3{28H5vGm)^4+9t0X8O{xNm@MjW$ypD30V`^U&@^K{~GH?NB}3j+%A!~ie9 zdXn=FEHi{Z_u&YFturOiya7@t&+366XzkmXQ2-JdS&vunVRq0vR{!w$Nax=y-7i+F zO~&Z2&Uc(XFEww*JKx{~zDHH*HzxJi75&@(v$s41e)%jE#Cd=I<4=b@#RwxGzFTT& zp`XhzZ1R7aOR>%Au*mo9gzJ>m2r@fbsWTWa9}n}j(K9xP8d)l%*}dtXO;*n!1j4cL z1YZruLc20vs~wDweNaX>_=`ITfnw>qh!!jDI{PYp#m_ZuB%G(t7ES9-pubtFjK_rrXVDtl@ z4lIwei?|DB5VPB2&%IP*xxH>~*Y0h+!`>L&~g!$@6ZG@q>Dtz=XqsX{J59%0Uc>plZxf zR6uz(6WI3P%@;oAT%`9o)24J*?!|#Cw*-w0TV`$6+oX8KeJKW3ShES3wEjbPJHxJe z(|$Dj_7fgMSaPhs@MLXWhpSQXdi3XA^Km)8hY86*Ox3M{las2}O{B2N-BM`YWJ{k? zBh_$Wn%A$S86<2Z5IrMULGR&3*iBp@1Z8vHgu@Z$=VHs5-mr7hY!RSaSIBS6bK>)! zfE$3kW7|Xx0(-ZO!O1+cjMhpam1!~(h` z94lR!yB@=yfJcXP-}rR*+-|$?10VEuHH=UqryT?fGke1s(^)>nej(lXw`3$L_iaJt z&Y{BUVp4GH?#}7zk30=|v*rvWJmN1& z9e3eXs(yt>2sWaI;L%uaX>wowyx7afE6$9)ecil0-PK%{^+Uwq^D?kH)+_bVND_|B zP0&|(q3)Q1QWPDsuP_mt&;cfF^8ZUJH@>_86yp>f(QlJ`SPS|>3Z!*IFs!~!(A>_3 z>5;sT>yZDuIL%$p6IP8`vp-|Yd>M3#?#7-w^tAe05ai$qm{?Rw^7 zdmmz_`Z8x!FbX{{O2k?Vps11jDyB_m1q|FeIeI8@6LxTZ?PX_u&cO3H+FGTg_=6|n z&3X5>{|#%+%@-OuUm)d9mQrD-CkYNJRZ%r zmau5_Di6{Ikyn>sY>kY=`%@oetTK^qa4DIOEgh0b&yXDm#PwGI-@c*PgNH?wzlDl`Y01?ZrP?9)5qlY|#0DyrWs(L@5h z$W++L>5$tF8MgOLi^xHqRHVt4T99V6qALRwq0&IDLTm|uKmjh6$OqQdsdTN)v5&n5~4R8cJjujj)sU+Z;xgpari2u{oE6E&QfmWydN)XVst$m$q? z{yvWAMLDUJTwfA{YLUo}kvb zgxjlFE73_CXC{j3w&{Nux;R2WT7bDZf1zPu_Mf=u2EIzE9K2dy!p0`NN$l}X4@c&^ zWZb6+n8!g}Vm`=^%+6BRzAxf<^(k%M1Fz)`6~$_HULrg&Zzm(xfq_ z7Bt>;Ef6>G)tlEur&)$*G;E85WOJU@ky#pE=1ke!HoU*ID|8_74Xul;?QxG;;Owfk zY4Sb-kU4-JtKN24I&r4k z%O=q~%L1G2JP8D+Xcf(mS##A@r*1>LaYx;2F@b1+(tXStO^}(WX7;hDk!wZXgl$8l z4X@ti*O!hpeBGFiaMTSCnf+P4MpYAjJL3w;%k$n0QA zl{{X<^9f;dpK=Rftak5Nx*R+qw!UwNa(~!l6>e4(vK;+@--4PPYICO_rI!ig7+;^@ z$*>w%wE5z@rnY${I{>?_{$7!_pP+pYrZBe*d@*ECwds1+U9DXj0JKgCvhd#+3p;L) z5%<^AQGI;a2)hLllyt#lZ&}W#Rg8C29?j~O-~ZZ-V*n6)(Z#amdiK!{EdbJ2ZOFQQ z>c0E#gxOxivymaP4CSjXU!uBS3<-Uof~Hx8gC1FhWp;K&`uw&uq)FO$#!bj}3?E~G zB)=ldcMaw~xn09eZQgYpX*z@_$5}4pG;hiMosyS+cv#Cp5hlyAbe@8jrz-|C4t2Na ztdkdd_gW8ot3w!#t#o8&dnzIPvE6?sL14xo;lRU>xS8gD>jdbAaB041;i|$RSn~ru zGPEv|J{i`jEn*2jdC0W`WmT?%E%|bBubo8|IAUpsovtS@O>-C1%CUMcWf-b`-i?Br zSG`5fW6;n&1EQ~a-aqd#P1V&@V&qN@$rb;Dr0{695LZi$ZG|!}6xp>t3=QgXIYwu( z1(alhyo3ufmB@9b&;e1t`=t9yobx>y^$#u+GJr&l8LAJ0h3*Vx-k(<#nohXTXOZ4! zNHD1l=nS@59=-i}VBtcmRK48;r+5D_Lf)>rH`PRfP zqJrP^^Uv->JgGdCEPHgSNfhuhs=_YSrwUPO-AEbGy=vbo#*{^+xf$E_{^K66R)^Mz zG21^pIcqiT{u`CfR)zk>p^>WWsto+^D*+TW)+mt&H)+ChogP-XSMMg>i_uJfgn``( z=A)LskB=qRPnCfqjLqsv&DDmuKLE^BE%2Q*1Qtq8Oa)jT24j5?)HpAV6O`H`Jyfz+ zJgC&Nf>mUtnkGBP>$q|{>?p4oFJJh^i>c71BTw8uOBa@%J?B)tIWYxEb!MZeMg_FY`C0fB`z)1D4?jEZ9yz9p;89p^_h5}X zblnrP`uTi-?PXXA-x&{khWWpdLku5~|F24n{?O1$+W}7R>H7G3Bb2gS;b-~#n+;VCs5WC< zWex)p2Fk3vZAcZE<@3Q1VpJ2D)F`$Jn^7YS1LRKoqFalzC)|v)@4w0o?`2-z<8I2g zc#bwyI8Igi0lSP9J=IkC9E-6x8$AXJg!0Y zGQ5FljT#R2-$#5dMQ{!_KzMoAyq1I3WJc(es_h zwv_WMy=B!87cNEP7rmp-XNz=YX!Mtt*9M`8Zevf8j3vE3_s@U0)l~(_c&;8J4?TzK zo5TT~e%y3%|4#Jhs8gnn%zvAxCOI|Qvws^?etdp>h}p@a2sg2t&-(@q*g_=IuVqng z={00T2MC|_4h^DbM*4V1S)uk#Mo@#O9W9?bpc5tZAb54POI&e@1kuX+KBvW;vMty~ ziTW5xVIbNiq^GghX)~bm?3Wd}x+a}AeiCl5Splm*E}siJ65J3a{-|C3l^BjN(R`U*-_a?DpDubc6}p2_pwpp$X<{19%) zb59*Rs6^4?&d&UzxaDN%fM^9>RMo@N{Q*dM|Cik77QSJj)8;U|u-k^h*XbLRq47dF zGEIsIkq<4}kKJs)&tvn9DgI|Gva$Au(N(`m^paOg%o?tbpml#^PsNzrIGt}iP1+xD z`~Qfbl@Y0ox=wK83^&-?=mK=w-xfX^9(!{yIcYk|?&SKLu9w9+!4&eXPk)oYl(t@7 z5JX3Nuuse-E<}^rehRjGY|AVk3or2=$F%_f85wIZ;_)nlfV#2>YFCGb(m?_ESE@BT zc$l_y%Ki*v?nEyT~i}#meM3#Xh>ju-tNPi%qxX`pQ5zp%sYZ( znS%q9!pSFwJVDrjjI0t#G6N(UbdzSU-QpwB2Dmmgj2kTaL~~5Jslg)#E|axG8fy$l zNpH^pnlHd8^mwVPb8!6AZc9`BzoM)wEj@psxg{#l<~vP$`$b5K3E2@xWPHeADIKHC8|VCJk-1kVFnVP8Gq77;BW@?vOy;* zNvCE0jxS{W-W5Q09UZdshb=~TP?k(Fh~Xz(qe-s*j<4WWEz9iO#?eOD_b~X2wqMJ) zDwhlPF8a=yu=u7tFMokM%-aesi`@dtPs>r9z!c|NIaob1vCHvlz3YueYf_VV8#;of z&VxkNyW?l+qXA3TMQ3Uymlxt;{T>{Lv8!YMYRIEFNs)AQ%fmd4Iz#_q$^Dq!r~H19 z?f@mxTjB8Q|6(Zi$JJz#<`AOA`ePy8A>Zb1Zc7Ox44LAbWtqZF5!S*bPv}hum|LSL zYB$YbN<;A06yoasCJAM$bOpZYoTg>`wcHDnvR{MKeh zbWPukMi|RJ^}wEO1jyu-k){-4XH+M#B(~3iBCa)TlYc2c0idga0Xps^<$9HsY0*5) z=GJN)NjfEAPrgKS05FTQ@eC#DdtOI>xsea`iCsQDSIP8SR%McH) z9Rjn6r8QDM9&ut9`&lZLJ>^#VTiv(0W713YIjIfjGi&?bj)=kJjpNKx#TAvot5|YH zMQKcbj0c=QRnQ$Ogq@S)syr8JXmOub4#&G5Qnagyzgdmk*X6XcE%o#sGw1 zxAqJ7NfP`4!|lxHPXf#p{XLOE+*7DN!S}SeEj)`SFNniWPD)oVlwy1}ocay4+W440 zF#v_)5qXX@yJ!yzp!A-7W*cll7E~-)VM9yfmKfk%eIhNfqUM%^4rvYmlh$Ew0?DF# z+`=QbMA0_)eLJKzW^VaF<<#7(-NguX!lmGet;Kaz3t%)^0}Zqs^wo>`aB6V6^i`?z zXn?`ZCK71P$La_xQq+@KC5Nd*lF(_*Vq4#hR;=4dqOz4Y4@R7Rm0tmOzC_J_+z&x( zL{C1)HE6z9<=|#YSS#&yfF3ZIIuljOPOT?`U-gY<~#@?>Y2gQ_8_4N0-+0u6z zk2(-ofxU(*cXr1P-QOq6k^2Lshh9bmt{$l@5$NqZ$;kX~1<^9Ynk?Gg1)^hwAIx@r zE-UBttGy_OXtg6x03B}KqGHeack zR`qbzb7#`dWx=`ka|0KTtkk`Bv$T=efg~%4JY>vM>RfDge-Gt?HXtEP0{%Zka@55P zk;BcHyOad-*uk&Nx1K+KxE6#QOa(hlH{I_ki{a6t0)8YIP~-M0n^vJNPGeekW|rHK zdg+#z^9bQYiNm$=pdH?6eNpL|Zkp{;)paYj&19_*#n1DoKAfIU_&Za;IpR>7HZ{TB zYuQ;offSDdD0JypkBgxOIjz)xU*D$zsqd$;Mkk)B1+(gO`nwW|P6~$A{ z*D)t@T#A$azUWYo`uDdyzheqaLlo*Gu+w<8S2|i!6h4o$gPn!Gf@sRz@@vxmI7RIJ zq8)M72WYEtO%GIy0LZYRm;qZzLE(v1K_Ud$DHOnJoZU16;2{_gHvJ5M-Upqf?qeU} zIVpghX*D#1VmbYUHTj%TvYT36a7Ue@kEf9}W`H~Locsh(RVDQ}^u+qDeYtT@Y?D$i zte<4mbD-1&d!ZV>oYT47qH2>GvkB_n>3g^C-`tt+HMy~0aY(C^KDsqdz*m#wm)?>G z!V`|{c;qg}K1@O8i!R`ZxVro42>rsI93GJUyIvNOY?MC$4BkNp9&aqZUHrN0ACSBq zAGm+!PNIerduNFx{f4pY|A5Q$IQGT%w!$Ladod)bdHE}%$rNwNlh9gt3qf12&{H=F z(jFpft=yC}3cW|h8p?Dgk4VQM>g&TkZUN`z*<*Db4`uhIN3RGvUpA$`qXWoihW1y% zBMW8K)pq^yIy4zKgL_%V!Tdf}#CZELD!&>oB<;RV*{F$Jx`r?IqP;pQf=cmkLL|5g z(N&YR?9#o|fYK?L5X~WF$2F#{Q;LUzK7Q_V$w@tgz26z-fu(qeFQP$+ZxdJo7al0X ztFZmg90~NwXBOi|2$Y^P;r+rZpar2@1*`A>ImxM_Px!-Hgub<2W!Hu~ujtx+Eh$Gn zO20__%%dG#eZa^D#%!S7jrBI1OIyfBp8L^Z5^{iTRbbk6uD%Uff*n>M1)xHT%|04+ zp!0PxcbbWTs8r9qQ#(>N;d@5$}tWc;48j!D(_2&8l!q;lWDu z5)uYGcBMg4>bLfjjtq;??hRb_T#jewnw|YA4Q|CMvhqxM?CN6FL^ad)1pVK4lYtl6 zMesQ_IJTCqkdGV4pf(n6BeJc;!ptrm)jswozWTr$4YS%TcriVEZk^JmtMv!}H69gB z-f!}+p|l42qO_=KM}go&S~#dE$<%PHobdf>_m0N?cXe(>c)HJy3Xxll#bBk;#r(8a zaYD6@toa%Z3gqxc{cH_&fXdrF7orfu=Zq;8stO{E?LFJEEY|1viz60Me!_zJ{$P65 ziO&WZ5e6hs-ACrx3688Gly}7^ci(6KiWDDUW~8uAw%^4^8{X;7%v^kAY%!8P3vkfJ zJ#3ER?g)HTSEhoJ4h{e%>#^ahTycw{)&D8aKxySZCm5Pi{Zi#4JeB!j+@suFS6}dfkzF)og7Q2f7)KNQQ&n0pxkS1K?Mofd^>IwCoIbdQO zpCm+*r9Z9ZeJETVKO7!ceAY5MkD)XJ5Zq@rNA1WCd3Krw#B4;+I`Ch4J<@7%mR1hs zavAh?SQ#vHHF6r&`Vo~aP#CX{hJ?JlzrT+cs9!iX1c^8@u{=Os-p||}dZ*T}j7{bz zOjAA}&$QR%Urtn=k8~>lNORPI-SfU&E8M(=@dM;~8F z^Y>rmg@67LTn7QzKn!QN{$kkuXv}wf8MfI8w^?V!d7ok8w##E~Nsl$gtLpc1+?jpc z{EgR#REl6udpo8R!wmoNJB(iOV?;BKxb+`!>Gw2Exzh{6ZEq>nb%VY` z-cEDHaw>OR5uSUu*N~sPC3;Ocw!U~lR|jb0Sk>gWH$BDKS3SzYl@$)0t*)h@OFsX$ z-T2s%J96^RZs(qBw_#8BaR6TFKE;5XAU`;h|2t+U01jL*mstZPN{({yuD)*>Cm(N9 z%p2IFt5|vVNioWCa@IR9K92eQx8s~~C6v^QRyOM_`}voGpb=o~);}}&!E>gR|0ShV z4nVHZny2bJA-<4|@8pX}j^iZaIB6z?q$_VQiePokWOh$V0F-7v9nh?W#mQ4FTHw58 zK1K5zeT(MaX{8{yIk~Zr>cj=eef#+o6(N&SOFU^WoaiJEuhiM-;k|#Xe|^A?u?djkQDbQ4xRKW|R$vh1wt}xh{Iz zhMVo9*g4cHloZEFr>e$YOa5A8HIU5;BI)!tD=IZ3R|L~U>IF2K7;Xd8?y!86Z$XY@ z#wLqdC)nvtQQDP};7=Mwg*ZB9+l>(4!fO|3h;W>ultI_YiW_5FC~XnKMBwPU61&ic z<~G&FP0y_0P(7J%`7$%vtbm?lqN%RGyI|0CTu%hf+YpGpw=x8Bd{i#ivgUM11@=|( zvqa6Z%n1>HSuCr-u{l0z;3yq4@jh-(RgF?`2g^Ce8#co=i{66K^1iBD4nh#*9tUHv zA#&V6Ro!3^<~VwRVc=|cM%C=Bi3H;4BoRl2*d!nPFc)pj74|yrTc9eNE)LX=k~-Bg zG^whpL5YL#rLJotMpczk;<}zAmX#NpG_Smeq6FvCa~xd<;hO3?GS250T9(l=27{Xc zpqrx@{WF4H;HUy`e$Hl;ws~c6p8BJ=vGS)M!}-)sxVQab(!f{TeK{ka*p+49KAuIN z+J}kTE>At~So0PZ4OKbe2e$;fi-Dt8udw>!ukrl5w_xOwZy}>WqYzAg_cxgRnctxm zN=k?!?Ovw^>fQ^_@#^OZk9s}phw-@B; zy2jD3-i)HI#$?5W!`1gJqH^bzEd20-mORxoD?E8Ki@tTdD#_xytT^YR)E@g+>==7w z1?G#7PkOHSj~eP;CG#7hRdLk&)6sIOlt84`WT%KD0MenHH!yUa@rK=bA$Gqc$0Pzd zZF3ycXr76HCY!37vUUX2|Ma??(O>(WlcXR_Ejg>+uOhDqQS61jjlBN%r?%pF0gT}` zW7iM_3O=2)Cqn?oKmQ~(=Th5teR{0=cTQIu1P(ew6*E%vGa-(n8L>4#jyT3yImtXf z^QB0)kss%rGPXd5`j*&>cBvGcvE6i~AgJ2a$HzWkhe>DsXD zA>`_31Da}AUT>|t+=4i^nlB(@B-j`J5|mksHSCRtIIbC06)hK*_GxsG=jd;s1m$wl z=Uo~Fw_xaW7j#V94c^DT*{;O2B?TiC#nFPziOw$Rf7v!5jvFlUvh7<7YzB5$ewXQzgs&c}2a-L=@cQLm=4qv(S zG0r(@Kai68bC0m>v^T}YZ~W`&9%Rm^eo6P9VbB#x^Nb(sDVidqJGXw|sOF09ZG>or zGV{N88;z&#Ay?GVX@bx&G*x1yW;x{a)-oK9<8tkNi}~p_k8;C<=}xoI4LfMgAA<9i@4te(zWRPPo7Ka_ZGTGbW9`1bQP?EAmRpNu3FAQP?zQj0YWd(f z5}KjP>P#tcbP<})w5~iWCYr3Sn#}3#0Cmij05PdRj?9Q0rF2!`sOJ%pKq8rYH#Kpb zsZv!Pq|DL6l1{9B!OxP|!Yvu9snzI#ZDS@i_ zyT@w&2K$VDDV?B!?01NC9lD1e#wrftbZyq=LKC_DQsU|bHRcJ$&JfYgKHdMU*-elx z&3YZTtZ1rA%q2u&_2k-^k>i+!IQpB$lOnp(Rcla`s@jP~sj9iM+QjK7aP;%yyA(p1 zk>i*}!5u8;sWj`XsHpkm*QjhH@*9B-;y4j|Ndrg6ak%x?TNxf6W{*AgVE+91FJ(-X4LJpmbrI7J-=jxwadYB{7+SK}XX#)RjL3ut>j4MDWl8=#!X+OYT1P)ka9 zSqd5L-f187fmt{sD_YOl)l1hg|Ht^IdonP627}Y4qm_D?xaKTYU-2{Sq1FIO7&&H~ zbT)eLG?snsO~L1HwQuCB`4jlvtqa+Go2PltmSZgY`4?MCXozAr?rTqC;Mh;nJwxSK z{qyk^eD_ZeQYspJ`nX*(20%|m90RJV5z<+OZcy58Pvp?kDxXVeA?;Ac(Z-QVlV9KP z6z5+1I4`W2NNqZ2XdLtE&G^#cv)L>+)I!TN`e$KnyEi;>8vt95KScAJM`3^VHyG3A zs?X;3vBSI)+b;PhZyURwS#l(Hjxox7BlmW$(|5-e8ikz3I}X^2>+f?obn&IZaRYb$ ziqT{TBMDE{d;Z428$XdghG~{kiX$*R5jaK=$JolC8&jgH;woBIU(sN=W-{Ki8MNI= zYibHjSM~C&dfP(PZY*w${G(Y>bErE(b4`?q@aw5%iQ}Y7?RP>|m8f-}A zh=kK5glH?5?`ny_(O1TFB}{nF^JL{VawQd*%?LONEk zxfUJM_T@NI5XV^Y^QKar^~WeBaP$$!yi{|x={S_L_?=Pn0)-Y~9N%&!MdgIl{6+{8 zM$~4a=j2PPp8BqnuB`%n0IQBGz0EO%s=+KJ(yV#E6@YBJfg>FY&8}gX#w3ViB1OZM zNX;vskU+BhSNO})srS~rhS`L282q%T-ny+Do=FSVoRoZTU5E~-suEZ`sw)24QX5wD zs$Gtxk2n^*z%c`H%nFE0Ii10Bj^UcclCcKe1)Z7Qg%)8PNEgSEb`$j6lN4Y7t&}5|Q-VbUhJ=^%*(_eM?eReQ}(`0vzq%nl|a5GyW;y ziQjmCH#C8T#F3y2Eu>*hDR6Ydd#oE^;nA2P&X`?~0MC;lj`5vfkt-p4=O{4?4{3-6 z)LwpN-ytV5e95=NljREN`@M~sha8Ku$zF_`ISM@m43ApbyZ><%U$Hj}@3|Lo%~{CR zOVpSQ%w*1ozfEc16OfLLF>NmPYUPX`?);0hQ@2H}!{a0igcyEx3(Z2XzHK%TiPYHBW`s8aN6gUJgOab-u$5 z!A6G=f@xiOmX9mcac*A+sAImYf?czgnTHdn1&%bw+l%Y?bJkNT&YE{w6kLEA-37V}1}i3-RGJPovlcwp zrN|~?GS5?54Ons?^vs57n|F4d|0A}}1IJr#xrO78Kc1O0XEJTtG=A}mU!dzcl}d%a zzCM2Plb=wp*E#?E^Ev(W)7gFZ-8t;A!`5{-HrP}w@lq5>Rb)JI*jiCl6RLDAh?oCT zPXsN}M;u=Ys%l2ysQ#YuTiO2W%qH7VRdHn)=V=;9sTO|`O44v zPN=Gh=mOtHnM{l3Kt|DxflZYo=uy}Fs}hmx8aVo6)StYE=2Lgmz1JHlzV%B?)W(qR z7`E5HXzeI+U1++2vG=j$4|y-W4_|{bzKWhLw?>~f-|K=w-@)%>^&eC~TEgUL`*wbl zJCvQw#}W5k9rnG{!^UUdM}F=WjMPn-Fma{ibHBceA71fr>^iT$^92?URoQq(A}A0B zM5(@x1wzB|3af{RqZ-48X=7HKP`9X!HdxgZJh5bqzuog9f4^@rD@L>PiuTxg1}7c9 z18>}OQ}WT`;2AzpTj^c-1mlsyuO&Vp{goWBp(kPH62$o$VC?h-2lrCR7oc_U&sOIEf@?NCKFAYhV4)^ zCi#7_PXz{z(agXxCXN&RB25?vTBxdyltI1IsI?Wx>5!`GmrF=R98-b~k#L$OGuM^4 zdI`;~BONOU;>6fu)lBa^nor(E<+eXEaoc6sD_+PvS7Y(BYU8t=Jtj( zO*yg<%o<{rs%0AHIEIKL09xdo77fz2$idB2Kbuu` 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

X51`9Se*|u3dzhrInR4t2IW$wuY86 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�O26L=gov(Yk`KcQqKcZty_>mFi1 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 z�_tc$);o(~eXq!xnnx#n@nQd`J@;(;yC?mr%2}mc!#&5a6I<7JT0~X9IriGCx_S zz$GNbBOXnrhTWygBx2AX3&%KX`oFzjN@^^~9KrGGc(HtWa~)BpFk2c>c` X!)F8b Date: Mon, 3 Aug 2026 22:47:53 -0500 Subject: [PATCH 54/88] docs: add ROCm Qwen3.5-4B DFlash reference results for MI300X and MI355X Replace the old 0.5B EAGLE3 reference numbers with end-to-end Qwen3.5-4B DFlash results (offline capture + online disaggregated) on MI355X (gfx950) and MI300X (gfx942), covering loss/accuracy curves, throughput, and the truncated-sample data note. Add the ROCm-specific --disable-radix-cache plumbing needed for the hybrid linear-attention/Mamba target: a sglang_disable_radix_cache config field and a --sglang-disable-radix-cache flag for prepare_hidden_states.py. This bypasses SGLang's mamba extra_buffer radix-cache strategy, which asserts CUDA/MUSA/NPU (FLA) at server init and fails on ROCm. --- docs/basic_usage/AMD/amd_rocm.md | 169 ++++++++++-------- .../AMD/imgs/mi300x_eagle3_acceptance.png | Bin 111855 -> 0 bytes .../AMD/imgs/mi300x_eagle3_loss.png | Bin 175594 -> 0 bytes .../AMD/imgs/mi300x_qwen35_4b_dflash_acc.png | Bin 0 -> 119954 bytes .../AMD/imgs/mi300x_qwen35_4b_dflash_loss.png | Bin 0 -> 110574 bytes .../AMD/imgs/mi355x_eagle3_acceptance.png | Bin 71877 -> 0 bytes .../AMD/imgs/mi355x_eagle3_loss.png | Bin 144214 -> 0 bytes .../AMD/imgs/mi355x_qwen35_4b_dflash_acc.png | Bin 0 -> 107539 bytes .../AMD/imgs/mi355x_qwen35_4b_dflash_loss.png | Bin 0 -> 89733 bytes scripts/prepare_hidden_states.py | 10 ++ specforge/config/schema.py | 4 + 11 files changed, 112 insertions(+), 71 deletions(-) delete mode 100644 docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png delete mode 100644 docs/basic_usage/AMD/imgs/mi300x_eagle3_loss.png create mode 100644 docs/basic_usage/AMD/imgs/mi300x_qwen35_4b_dflash_acc.png create mode 100644 docs/basic_usage/AMD/imgs/mi300x_qwen35_4b_dflash_loss.png delete mode 100644 docs/basic_usage/AMD/imgs/mi355x_eagle3_acceptance.png delete mode 100644 docs/basic_usage/AMD/imgs/mi355x_eagle3_loss.png create mode 100644 docs/basic_usage/AMD/imgs/mi355x_qwen35_4b_dflash_acc.png create mode 100644 docs/basic_usage/AMD/imgs/mi355x_qwen35_4b_dflash_loss.png diff --git a/docs/basic_usage/AMD/amd_rocm.md b/docs/basic_usage/AMD/amd_rocm.md index 8c1ca94a6..076575e64 100644 --- a/docs/basic_usage/AMD/amd_rocm.md +++ b/docs/basic_usage/AMD/amd_rocm.md @@ -351,102 +351,129 @@ freshness rules, multi-server capture, and resume, see the ## Reference results on MI355X -The offline and online recipes above were run end-to-end on a single AMD -Instinct **MI355X** (gfx950) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi35x` -container, training a **Qwen2.5-0.5B** EAGLE3 draft head on ShareGPT -(`max_length=512`, `ttt_length=7`, `flex_attention`, `batch_size=1`, -`learning_rate=1e-4`) for 1,500 steps. Offline consumes hidden states captured -to disk by `prepare_hidden_states.py`; online consumes the same features -streamed live from a patched SGLang capture server through Mooncake. Both paths -converge to the same loss and acceptance rate — the online capture path -reproduces offline quality on ROCm. +The offline and online paths were run end-to-end on a single AMD Instinct +**MI355X** (gfx950) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi35x` container, +training a **Qwen3.5-4B DFlash** draft on ShareGPT. Qwen3.5-4B is a hybrid +linear-attention/Mamba target (`Qwen3_5ForConditionalGeneration`); its draft is a +5-layer DFlash head (`hidden_size=2560`, `block_size=16`, +`target_layer_ids=[1, 8, 15, 22, 29]`). Both runs used `max_length=2048`, +`chat_template=qwen3.5`, `batch_size=2`, `accumulation_steps=4`, +`learning_rate=6e-4`, `num_anchors=512`, `loss_decay_gamma=7`, a `flex_attention` +trainer, and ~10 epochs (~680 optimizer steps). + +The SGLang side (offline capture and online capture server) runs the hybrid +Mamba target under **AITER** on ROCm — export +`SGLANG_USE_AITER=1 SGLANG_USE_AITER_UNIFIED_ATTN=1 AITER_FLYDSL_FORCE=1` and use +`--attention-backend aiter`. A ROCm-specific requirement: the target needs +**`--disable-radix-cache`** (offline: `--sglang-disable-radix-cache`; online: +`sglang_disable_radix_cache: true`). SGLang's Mamba radix cache auto-selects the +`extra_buffer` strategy, which asserts CUDA/MUSA/NPU (FLA) at server init and +fails on ROCm; disabling the radix cache bypasses that path. Offline consumes +hidden states captured to disk by `prepare_hidden_states.py`; online consumes the +same features streamed live from the AITER capture server through Mooncake. Both +paths converge together — the online capture path reproduces offline quality on +ROCm. ### Training loss -![EAGLE3 training loss on MI355X](imgs/mi355x_eagle3_loss.png) +![Qwen3.5-4B DFlash training loss on MI355X](imgs/mi355x_qwen35_4b_dflash_loss.png) -Draft-head loss falls from ~25–34 to ~15 over 1,500 steps. Faint lines are raw -per-step values; bold lines are an exponential moving average. (Offline logs a -handful of all-zero steps where a batch is fully truncated at `max_length`; those -degenerate points are filtered from the curve.) +Draft loss falls from ~9 to ~5.6 over ~680 steps. Faint lines are raw per-step +values; bold lines are an exponential moving average. -### Acceptance rate +### Draft accuracy -![EAGLE3 acceptance rate on MI355X](imgs/mi355x_eagle3_acceptance.png) +![Qwen3.5-4B DFlash draft accuracy on MI355X](imgs/mi355x_qwen35_4b_dflash_acc.png) -Mean draft-token acceptance rate — the quantity that translates into speculative -decoding speedup at serving time — rises from ~0.01 to ~0.15 and the two paths -track each other closely. +Top-1 draft-token accuracy (`acc`) — the training-time proxy for serving-time +acceptance — rises from ~0.03 to ~0.12–0.13 and the two paths track each other +closely. ### Summary | Metric (final) | Offline | Online | | --- | --- | --- | -| Draft loss (start → end) | 24.5 → 15.2 | 34.2 → 15.2 | -| Top-1 draft accuracy (`acc_0`) | ~0.24 | ~0.25 | -| Mean acceptance rate | ~0.16 | ~0.15 | -| Training steps | 1,500 | 1,500 | +| Draft loss (start → end) | 8.3 → 5.6 | 9.1 → 5.7 | +| Top-1 draft accuracy (`acc`) | ~0.12 (peak ~0.22) | ~0.13 (peak ~0.17) | +| Epochs / steps | 10 / 687 | 10 / 670 | + +**Throughput** (single MI355X, `batch_size=2`, `max_length=2048`): + +- **Offline** capture: the AITER server generated hidden states for 572 prompts + (286 batches) in ~48 s (~8 batches/s). The GPU-local trainer then ran at + ~0.3 steps/s — sequences up to 2,048 tokens on a 4B target are much heavier + than a small draft at short context. +- **Online** trainer: ~1.3 steps/s end-to-end (670 steps in ~520 s) with the + capture server on GPU 0 and the trainer on GPU 1. A single AITER capture server + produced 5,410 prompts across 10 epochs with **0 failures** (~10 prompts/s); + the single-command `managed_local` stack (Mooncake master + capture server + + trainer) came up and tore down cleanly (`default_kv_lease_ttl_ms=500`). + +> **Data note (offline only):** `prepare_hidden_states.py` truncates each rendered +> conversation at `max_length`. Long-prompt samples whose assistant reply is +> pushed past the cutoff end up with an empty loss region, i.e. fewer than two +> anchorable tokens, which trips DFlash's anchor sampler +> (`ValueError: should preprocess the data.`). Drop those captured samples (any +> with `< 2` loss-mask tokens before the last `block_size` positions) before +> training. The online path never hits this — its producer regenerates full-length +> responses, so every streamed sample has a non-empty loss region. + +These numbers are a functional reference for a 4B DFlash draft on ROCm, not a +tuned performance benchmark — longer sequences and multi-GPU trainers scale +differently. -**Throughput** (single MI355X GCD, `batch_size=1`, sequence length 512): - -- **Offline** trainer: ~3–4 steps/s (pure GPU-local training; no capture server - in the loop). -- **Online** trainer: ~1.7 steps/s end-to-end, bounded by a single capture - server producing ~1.9 prompts/s at ~10k tokens/s. The producer generated - 1,719 prompts with **0 failures**; add more `capture_servers` to raise - producer throughput. - -These numbers are a functional reference for a 0.5B draft on one GCD, not a tuned -performance benchmark — larger targets, longer sequences, and multi-GPU trainers -scale differently. +--- ## Reference results on MI300X -The same offline and online recipes were replicated on a single AMD Instinct -**MI300X** (gfx942) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi30x` container, -with identical hyperparameters (**Qwen2.5-0.5B** EAGLE3 head on ShareGPT, -`max_length=512`, `ttt_length=7`, `flex_attention`, `batch_size=1`, -`learning_rate=1e-4`, 1,500 steps). The capture server used the `triton` -attention backend (flashinfer is CUDA-only). This node was shared with another -tenant, so the capture server and trainer each ran with a small -`sglang_mem_fraction_static` (~0.12) on separate GPUs; on an idle MI300X you can -raise these and expect higher throughput. +The same **Qwen3.5-4B DFlash** recipe was reproduced end-to-end on a single AMD +Instinct **MI300X** (gfx942) inside the `lmsysorg/sglang:v0.5.14-rocm720-mi30x` +container, using identical hyperparameters (offline and online, `max_length=2048`, +`chat_template=qwen3.5`, `batch_size=2`, `accumulation_steps=4`, +`learning_rate=6e-4`, `num_anchors=512`, `loss_decay_gamma=7`, `flex_attention` +trainer, ~10 epochs). The ROCm requirements are the same as on MI355X: run the +hybrid Mamba target under **AITER** +(`SGLANG_USE_AITER=1 SGLANG_USE_AITER_UNIFIED_ATTN=1 AITER_FLYDSL_FORCE=1`, +`--attention-backend aiter`) and **`--disable-radix-cache`** to bypass the +`extra_buffer` Mamba radix-cache FLA assertion. ### Training loss -![EAGLE3 training loss on MI300X](imgs/mi300x_eagle3_loss.png) +![Qwen3.5-4B DFlash training loss on MI300X](imgs/mi300x_qwen35_4b_dflash_loss.png) -Draft-head loss falls from ~25–34 to ~15 over 1,500 steps, matching the MI355X -run. Faint lines are raw per-step values; bold lines are an exponential moving -average. +Draft loss falls from ~8.4 to ~5.4 over ~680 steps; faint lines are raw per-step +values, bold lines an exponential moving average. -### Acceptance rate +### Draft accuracy -![EAGLE3 acceptance rate on MI300X](imgs/mi300x_eagle3_acceptance.png) +![Qwen3.5-4B DFlash draft accuracy on MI300X](imgs/mi300x_qwen35_4b_dflash_acc.png) -Mean draft-token acceptance rate rises from ~0.01 to ~0.15 and the offline and -online paths track each other closely — the online capture path reproduces -offline quality on gfx942 as well. +Top-1 draft-token accuracy (`acc`) climbs from ~0.02 to ~0.14, and the offline and +online paths converge to the same quality — matching the MI355X result. ### Summary | Metric (final) | Offline | Online | | --- | --- | --- | -| Draft loss (start → end) | 24.5 → 15.0 | 34.0 → 15.5 | -| Top-1 draft accuracy (`acc_0`) | ~0.26 | ~0.28 | -| Mean acceptance rate | ~0.16 | ~0.15 | -| Training steps | 1,500 | 1,500 | - -**Throughput** (single MI300X, `batch_size=1`, sequence length 512, GPUs shared -with another tenant at ~0.12 mem fraction): - -- **Offline** trainer: ~5–8 steps/s (pure GPU-local training; no capture server - in the loop). -- **Online** trainer: ~7 steps/s end-to-end (1,500 steps in ~200 s). The managed - `triton` capture server sustained ~38k tokens/s prefill and the producer - generated 1,695 prompts with **0 failures**; the single-command - `managed_local` stack (Mooncake master + capture server + trainer) came up and - tore down cleanly (`default_kv_lease_ttl_ms=500`, no drain failure). - -As with the MI355X figures, these are a functional cross-architecture reference -(gfx942 vs gfx950), not a tuned performance benchmark. +| Draft loss (start → end) | 8.3 → 5.4 | 8.5 → 5.5 | +| Top-1 draft accuracy (`acc`) | ~0.14 (peak ~0.21) | ~0.14 (peak ~0.16) | +| Epochs / steps | 10 / 687 | 10 / 666 | + +**Throughput** (single MI300X, `batch_size=2`, `max_length=2048`): + +- **Offline** capture: the AITER server captured hidden states for all 572 + prompts; 21 truncated samples with an empty loss region were dropped (see the + data note below), leaving 551 for training. +- **Online** trainer: 666 steps in ~858 s (~0.78 steps/s) with the capture server + on GPU 0 and the trainer on GPU 1. A single AITER capture server produced 5,330 + prompts across 10 epochs with **0 failures** (~6 prompts/s) and streamed 15,990 + feature objects through Mooncake; the single-command `managed_local` stack came + up and tore down cleanly (`default_kv_lease_ttl_ms=500`). + +> **Data note (offline only):** identical to the MI355X run — the offline capture +> produced the same 21 empty-loss-region samples (mostly `max_length`-truncated +> conversations), which must be dropped before training or DFlash's anchor sampler +> raises `ValueError: should preprocess the data.`. The online path never hits this. + +Results on MI300X track MI355X closely, confirming the ROCm DFlash flow (AITER + +`--disable-radix-cache`) is portable across gfx942 and gfx950. diff --git a/docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png b/docs/basic_usage/AMD/imgs/mi300x_eagle3_acceptance.png deleted file mode 100644 index 91260917d6fd425da6ad3691e4cdd142ef37cde2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111855 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 u=6@@IpMeVtLqz6Lx~CD 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

Yq?8O8APN2AWSO~uOYX~#e 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

RzpKMgXTgDN2D(&0&L1!`_q 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 diff --git a/docs/basic_usage/AMD/imgs/mi300x_qwen35_4b_dflash_acc.png b/docs/basic_usage/AMD/imgs/mi300x_qwen35_4b_dflash_acc.png new file mode 100644 index 0000000000000000000000000000000000000000..5937cae291dd78e7cdd11559f4d07f916e79044f GIT binary patch literal 119954 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%0ACO7euF}zww89CHIU@+;9eZ6qU9?b?@ zKYT(GOJ-Wj}9D z(CSH{o&_@|RtA0z-ZvS97a8im&T1L=47K__>9H~mx2;l?i#oo`*!U0(3(4$zi$AuFv+H`bRU1posZ*e}s#; zhn@Qmp++*OWP0||rd4n1o%#6UzLT0DS;9*E&a8JEdAccBF8)x6(A#LSO_;u0R#Szj zQpz}KfTfa{MUMMtdqpT~x)Z0!#tGhCLepiKcc%;3^`5;25)T#g{b_(?D)8sv=NDjB zju&5mqB;r1t=YPW`STC~v@bUY6RuGt&n+pCod5;ML0aN(o0^*ChXCX1 zse*XfWJmpx{0nbYfIs6;;!6N&9@bi|PIc5{sAjyeYndS3vw;=P*kxv8%f%ISc4=_1 zH&THe{2Ba|O`kfb7hlH5B8wQ8jOYrPY0#eme{GCiYu!yFSUT!`N!@p4RaHham@*k; z&&)ip|H@Tz?bki9|L~Tu#RGgR`Sf*e*epDfj(Wgn3BFsW#dJ+MX>OVZ`ge+!=fY^dDYTnf1Rwo78Y^n*?WC5 zuDUe49pj9`EOv*!YVrk0tsJq%E+=bqRQ=pPmamqrG4j?|u!OE{5uLlI5=~}W04rVu z6RD~*xB7Cp?bq*BMihw$GNj8Z7jLh*pPwGN1WaAIewP1t0I{A3_6Fv6ZJifD4Gv5s z;-4l^>;GGogl`~I93ZoJoJHpypq`RV5{43!LN7$%T^OC(;3=WJ*|h>LI3ig^eBm#* z)2DL<6q&L8i^fsen^JA(6XqqR+qIn-DC1qpfh6t$l=|bC8BtPh6k8*;qE_ zM2(=D+w{1QU7&%ZM^q{}XugAQj^sf{@WeidzXvud%X0Z}5=Pv<7+`_zR<}c=qcL&C z&(xtJJIqgm%A@S%yYM2YNVE*Da47J-YJlZxhzmepJ*Km!)>)~+=>n^fW(jM*1qsDL zsBZ?GX+~k51vY|nQOo#X2&Fh&bw7@Hh1uJ_kQu%Wb1{1)!?8EPiX+Uvv}|(<*%i0A6O#^8MvE=bfHgHXxdZs%wvh zdtH=iN_%TzuxKD#Dn$lQJOxnbqX6eWSlN>G9^P&hQZL$UFPnGQpLrllLBC$Nzkh06y*A z{+W$l)CzkL!%7An^h_c22{838QBzftYtk|z7Q_uc6!6rXpRQ-o65DZ-MAK3vTQAd_ z+93h)%)2j?1a8C1#X$g8!BKl)h6X2wt0mRjav^HQqeXT^C^#N7&p&PZY|{3>ey=$Z zMH$W{+${~v|I>iXi@1`K*IieuSWj10R+jg_)c1hlxnZ^7;oqZ>K6PO7;2+;)$v%ga zGYt};zMytP2n_A~r=81k)a~szHuy(TsSRlb5I9$-1T#_zOPK9Np*4-QfZ&w3EHL2w z?M`VlAq7bWS^ClC!EDL=5C2(9T5g9HElq_H#M~%_mG~q~DdSaAg>UQ5gZ|EKk{ARo zd!CE}Te^i|fHJ+J(43|$vNRT{-g5rrjl5_oDwVR>vr0SMy21iphhn2C_aWit(!+4^ zoASoS^E+Ck*5c5+&WmSHr+MPPAwU0k%66IN=_y6Ho#muvS!F+|atNi$-m;`0yXy)9 zY+&)~_$ThnU?MTdU46Avt=}c)!(d68l9Ez}u-j`u_)=VKesziJ>P%L>Re-;>fs;*z zeo@!Z=gh#8m7%@HTx#;8rvQy7(^aAHv%!IhGE_a9MLEZOMTifm35eU|CeA=j>))DJ zBHLRr9R(5Ilj;!PDyLYp_`m1;J7lBy=W#_I4CC4Zz0#+F^CQg!s+`q!^U{VFSI^q$ zHr6Mv{o*&jI8@y5HLXA`V%JVzry?Pt*xSG!#H|uBr0~g14y>fZmU)D2_hRcBy6Cra zLe|OG^13KA6{G&tkgD(Q*8d`B6=O}To?C*KnJs9%d^(P*mR}yaz-iC$U+r@$dgL^@ zX>+g?e0aQ1N@MT*dsnP)0Tyz61=QLP0C{UI$8GfI)lKVW-VpDY7~mcxBy{)p8y4nm zd@ulJm1s{DQ-A=F*j(|QBhww!NA{r!$R{as1z_w>NeswV`H)Ty2YKL*^Xx z(6l00LL@#02dNTUFDtWB4HV{L2ynsu`35nVGOLQCggiZU*HTvZl?*xRtB@BjHSB19 zQ5zfmLT>IHOVU5>6piCpO^;CBpJ)je^<_x1{yLt_4P{Ff^!KgVtzbQ?+mDpq)%6UA zkjyk0^im|U$I<(W4O&>#y2vB4vi%kVdV`+jCFr3>vBGL zCgyfL*I3D}*{P@Bmn62T=PF3{Y7bGeor?w7q#k zIInt4Yi@u3;2$%+O4RpC^4@#uTyrDS@W~umI-U7q_vk}Cdy-f|oifBOeAs=ikho=a z%)*S*G*EaoQz67B;0-H%%VJcdDK#CG!>_+K{`QT1W>ut)iIiPVmgTFpNS_B;PF<;Q zp`IPMe3jMaks2k#FH~-|HZscjYJ^u9x&FI6!1s2ma!&se2H4OK8QE6mR4K+cpI$u3Y3|o|r z*{uSupD=m!Gwxi=#ta$_nXlkNVVj=?XwRbBdV)jCqcdUTZqhm_#sgBIc4zI|ztVPO zFA`^umC{RKULJb4U{+QYUv>`AltRj*e{J~vF#1`aG4)f=N1GJR3spz6xZUu)vQ?PR@FeeFP>N144OEYTxBo?94 zTYtPDANzW%TFiVHvHQUW15Dexp|-nI$gV@h2oNYgEx7m_H?u{ll(T;DR~~fLTl`Jl zONBqW(Y``1+{**k*xgO^JAODiZ0Gzah3sp^L|N0R_K!0l5fWEez$~j%&CvD{70IXl z5NehE6GdO&*WW3S06?b;5;8wh$vC$sCha(z>7jCv2LJ91i|RX~FZIe^gU4^K#Y>J} z-2mzG08h=#RTu>=f*Rzjl!Ay7ar9vbVEe~_;6#5WUC9XUkcama2=4s=m6yZrU3413 zpb25wA7QbuSN@)uBWaTlZZ7={GrxOk+*An7n*?x!hb(|75^Q#d{4V#F{)fypSzj0J zskwQx=gK54r_&8bYo+N&>4M!>GIr?AB{Z1MzhV)?*VhksJ4fY`)(C5|@DVb2Iz#nb zzW;O<8ME+tO67#M76D2Sw}p=iY`7W4%y3<~FoD=&G*nytgIq)7Xm#a(1BKmX;o1UzKx#eE8jU@zd+(H5ct%tyuU&nCpq ze#684YYdCy`*lsH`elw1ON))v=Z-nNaAaq=o7ZJ&hn_-OP0ay%g!l*J?y;iUL7DcSuc#? zScQrnJPG8GJ^}P!HlsGcXRYK2&_;;-XSbCi$d^*_9aXehCvH z+lsUe#eIRb#O-OhTl^#j_%lF*>iPz{z6SI1oZF#@L6_C$@cV>(80eIh&s-Se06P&j zyR6+4@@GAOt!qzH^JMN&hyuhbi@p~~n0VsE?>)}H*w=iw%i3U@pOt$r`*k?qe_e%x zMe65i0h)}pgTq$P+jtH2$AfnI)YJlg!3n@=T-)R^n~L5;Uc10;6cm|hm73#I@+xS` zbvK;NyE7^&CNH#TA4CddBaz4e2r?=v>JxANiTDg?8cqQL#M7nq{yqF_y78|k*#i(E zboj>A*%>JbhrV(%Cvq(U!-<1Y50EssX0&g_&TDeZtigGo!ag{nKhtp?5VC zc`bc$oS;|U6Kabzf|F=z%{6DuGek_qQTQ3^Bv@eLiDDyl0=?fmiES4j_TO?deutTd z=e{fwF4jDEC{l0FG7%1)XqRSi4UZv)O(?mXoZJFHvf|_Ce@dML)cUeoT13D&LU4fl zJqtj7*;8O5q+DsSNGy*iI?q|F-1&{>>#7|GPeJOjmkUvYs7HGS&-yoZ;4a(@;PR4L4oy-)`d6-H;bB z%lW-J5V|pj_Fd3lq>IvP@MlxK$6O<@E@*v+4^!tB7VH)sO_SPp?8G_duQv>~Y8AtK z`|!Zzy>C_TK>Wn~W*4!*aibDG91S$6fOe~_tjswl83ejfY{U8ne22~VCG^7hiG3%- zX-*GH&Vl%qyQxIGw@%b>w*KH2B!IW5c6Yu6gL;VCS>jm9LNYu{y)uJU5*SfTT*W`= zB4|7v+rFOl9#=UDAa8KDK88T1PhC~|S+Wu#r=ERWY%3Y8^4JhLycd8lB{{#lDcGZ` zKhm!CWhtr0dk!TH#Wd|%4;A1gAMI9&R&BO9eNQ@@UV3M z>l~g&WCf_u1&Cm$yom$KjM-ulRA#$goL*F5qf=!nGJ%cQrIi2xarv!MQOj|Gm#E_A z@>~OVcaB60mN1wwNjihOAPX<{MvZENkhhBlyQHK$1;A2aJ0X4&Yf9Hf1d*MmCe@w)2FXZNFD1PBrD1EwO^n)DO8~SjufkGj z(21#hP=Emp5j|P4-6#k%UZ`;1wID1elFi`7XRip#BgcAGHAxxPV`C_iFLRE-TqXG9RLqpexO?X{CNnTC>r;- zhUqYNhEd~jetG<2{gGMx5)}PTbi_BFAntW5V&hr6M&k5zxG9-To>7|ia)wg?lm6qa zNu=uc)I5J3old+6aw%dL$a?~#Ze$rjV2JD-PnGL(-Af-|eBBfH?DxZt9dKp=0I_d<6nXKOfyE|Flx;y4hK?K$6oL!9ta1bqgIvx410W%Ps27M7fG(!^3je z#%^887TAjH1)hILR$xE_COZjc3>LYnVA2mRvf15}Jr0DxP9vx^6C4PBSd+XEPUO=t>EpUOrC2`xYiCkG@d5o=>AqxY zyV^WzjWQj;J8lramPFF0^2cs0|EUjGpA>%m>SZ}>`%a`EAY`mx7NI?BkT~c2AfFhc zCgd^*PhoepV3h74@ti23s#g`$D*2lyMDY201oi7o#vq%ziQ^4}4c`jbk?J!4Cc&mQ zn-zp-hR$loYLBbq=XPQnU~>2efX6d7-h5ewDBf zL)wZi6MGnFD_;9}_%Fkzmvc&kAGTw7Kly&)afzqH7DehaFYW?#H~tT@1~C_LSX^Ms zz=f$hQ0j3UZ{nlFwOzh6HvapRU2e_2!W{W5cnmy`1?<^e15Ej`DUwHn?$!9_uUdcI zEs?>(=Vmi}{^+)`W^^+F9N645-(8}2`s{+3xDrC;csuH}k%f(mvHZ|u5zBNUhlM1d zi+qY-1?a=c$dxH!E$SdZ0{wK$0&nM+|6BhON4*kBx0w78xMX@oL<$XNW{Bz+QvBZr zCN{U3hAPYLX8XZVNFG9OMt@UQGEWyBr7+ViY2?7}Hsd!0P$9lHtdp$+%O;F{M78Fx zCw;#}1vmOAEWFwP!HPJ1r(}iw>4t?JPwaDt>#MGa2Y`E1l9PQpoz+_lG)^09p5IV5 zfY(lk;rh;9OCqW?)Y%{QXuBoNf2aIb}X_VjB zNIo3L=K$4{N(L7)fS&+JSNu>QU@QRobksvkOsuY}J7^j~`J+%SW}L$SsHlL^ql}y! zsziSLD_+Z^vx#??$3!AR4BtYF=2Q8UOGSHKVtNRnnE}HV!idt*{~R!4=ZHn+eI0=p z!YkI7b8TU}^SYYZ3Sn6C{0~Pv&EAHsF+$`Tz~s6uvWP}k1Kwl()bEmUOIg)8@ww;z zm_PjXkd_`0H#tQ>VYZqzKl#$sBiUz++ut4|HOyFTVX9KYZY%a^Lon*$lLiF-+ao28 zf}1aQtL>aQTYxc+DUV|>3B@tc>J>G+;DlO*Y?xhq#;;0@f05kv1YZk+O4<)!4BIIk z|76lzivbt9<^q_UydZK-1pO;|Re!1SD#o)B!O-r8}k9?ttms$*(u9JFtpb$M*de0NEA-UYu(nZ*dd>rp}%2;ZFrN;+YO$`%%de zN=z~~{4Vi$=LWoBl7OGl$YH>xsAJpq>og$LO5%2}4^Z?br#aZzm>zasZUCs(+PE_3 zKc~Q3itFp^9M@kgVnD*>^P6E@1E%moUCG$sc^pT-engat1B-5AG&ZnNp@e4mnSGL< z?xCg3a{OYu!B3matp;lUsZ=5Y_A7qF!AE^>Uv+MnH~;BiYT$g>Ah~Rt!agKNRw-Zu zBh8y(TzwMx-M87@9$1=G?OkjH}5)deuoFX!6hYm)k zhIZdWf6v5VjGDY1^vsd%mMljF|7q>3Kn1^Rk&pfM8h7gEhUy+`bsM(zH2Vqy>M^mK zjO!d4b%5ou(KkZuqCyDmF%JRVUMG(t4VhjgX|1#zcHN!;V^jUWeBhN^2zkk90Lj%6VDfO$_GIudzOXP1 zd~sw@*C|KEFyQ)M%c7&i-H}Xn$BEL@_4FA<>M!vBsp($;=}i^@%SH_FF1S1J>Rx+l zD*>LS1t3PeyW5m39ny(B{F{JNE*_r~5NJU4r22Op^}FGY7p)M1PZcojmGEWiyYwHo zIS|0Hfk}wewud60hn)Ah9dD|uc5QrjZd1(p%}7mv9e)G$UK+@yfWB?8Zz+@cD?ndTxIQ!E?Jcp@!BcFH%o85X!OmAtbea;Ww#EGBY?OT+p!$x2 zF|N`u3h0%!r`Y=n+LsK}3T;ISOi`i|`{&Mx%wZwUZp zoc~gpO97bWu6O)EP&p7QDM!7r^!UB@8I)iU5sOb%D4 z^X(k0{TxjNJ!9^|!jtTso1v`fQnFqW1CV~Sw@a0JEwGB95hl?70lb{RrZi$LO<65$ z4Uk*#ankJU1$bP1SY3cnNpx&@tXF4$hn3TNt2bM$^ zFa)t%+w|w+?o@WBlACn)5-COe0{Lx44O81MKJ@7PvEv*0bcSyeTXc4vq=$kC-@9Kk z9-mo2`+^8fQ?_24x8?(w!48&2t8Q3=-@2ym3~wE@dbo**64impm(0Yh`{~HE8>Cx7 zmRuQ70R6Jw`m}~~_Mr^!+Zyzdv^Q9CEING0C@A1j;8h!R?jxpa4`Z^RnMjvW$;x7Y zpV++055PP9APWIJkc1(nL`*FF(@!n(&~LVPNJ@`~9S?!uMndiS(RXV#XF6X@>sP~) zIx=~Y(Sf9>Y{&Ky->R!nqDpu+CuC}Ai2EAk5&GXJ37{!007v(Dpmz!|nn*YHjY>-+ znVX;Y0b=#gQeYxp2Zpj5=FN+q%Dzjx=R6K`d>cSG-k0N2N30J3Wo^-T9;ot$H@Z(Z zpR1dgymN41_uH!l&sps6x;YP~tv>`-uWLg9>65{;pS`sa*{dy5(3MGH5%bp{-53v5 zh1x6-wpfWjhhI~<0MxE<)1%oQU;w7^vDHdsv~@>EeP zWoIkg&$PneG{Stl0XXu!BjB$cf5{OaFnfOs60sWdz+eTXG z)+PPRY$AMLn%_XhQ!v87bT>7F)WW3JFX&*kdp$zplENSxe=V9mCjbZTBgZ29efa0X z!onreU%_o{BEN=)j*G8dUID3fK;}CO!kYM!<`c{A&Q9fd$vnwh2M34z^!+Hn!J-~8 z@+;n4ZA^(S#2-Q61r;)v!J=1-f8xAJd5gqP5=4lKtO`M(AZw$tlgYXlLi?bTZi@Ak zGRV()K)eH7zdsJiJqs$ z+XqSEJMd*)i?F@T!oQKbiC(TAs;UTP3njXcX$$2YTgf7UY-$P~DS^G?M zLw_UG05p+V=%0D$+s|PXGR(Ci6GnE?ZTB4c-J7}_KlD91I?B=KPVWK4ue7~FYMn44=2^ZBH$D1CZ6GKRJl7e^*Qo5`No=4(f(sd0Iq z6}17TuD@9($qOKluD-l&uNCJ0^^46d=kLwA_eaFKb_d!XUUz!3zU#|8PjbYE-#0iK z*w06r@WM~BN`KYqg;cOc79>EPQ{bQwu07us^Wxe*xQktVxh@Vh-Keo!n?H|>ou+uJK_KQRT}v)d)ItWlw=-SUUt9UVd+X5# z&a;4cxZ^kCC^RUBeI;wl5(f9ZzQ7CB^VXn@>c;Xk=Qk$$(PCyVb~=0jqaVJm;}>}{ zxZ5)lJ2;I62;#1{%w#AUvXS!Ky#!l3(i|LWJR{pUG8*cFm1xgV^3&BBp6N~6kgss< zAM-)-@%BGIclFHYBQA@!mERE&O&t1#&x0N~6Z?ueKM?ox-^-$dM$&x>!Pi|gxw+^~ zexn5)8#alN{Ll~nol~rG4z|7MCXi<$GQkg2MNlYoiIkkipuu@~XsD>5fOg+* zMVD2R@@XfQqWnDI_6!mjARW@b`<=d1^4GUNXLlGhams!-tud#9p6ZF`qF?I4RR8 znHKomqf>A>b^6pd0|LW~rRtLTHb)^_QI3a0I;6bowkRS?XlaA_-$WOF*q8oM0xyfN zsTUasLSb)MhnM`5LTmE7BUa}0V|$+^Fm%v^$6YTaxjxzy$d%^HFRil>|IpO9hl3(6 zEQgPr5OmQ{NvKmcp<~&tqhQwf-2u@Q>Aq-?N%uG6=U3JcG`QL^-7QLg;YYD1ml}2R zCH+erN1A2zRu581i-7?Em7kX2rqd8H_QgP|Ee<7kZyZHzHl4V5wVWX+=&X1OieddN@v!)9q8Vz4*da@(`!#VnlUkRcdv8q zWMPG;-@ekw3ZUESe`R*HJO9QSS2nZx6aBuKayjeS3hQ9AL`(_A-A(GhO13h~j|S{u z`}aSXYd+UHQYNvP4RT=jec28Qd^@~Vva?fhG;s6qRc6WUT^5rn9WdBT9Ed@5-IFYj zidq?VNp2y^GM~MPCc6&UB~wgI4W$^-R_4a5Bnk@P>7Q}32Rv^2aj^#{B~SOvP~e07On~} z%=(9Zi^04)opsbW+kk`Tx4UH21`UaC?$vgcb;o3ik zTfj|rgkCwVHGwSdWLnVjzPQuq2}rg?+z4oEghwU+3G{Pw)w8n$$3;c_Uf{%FH$?yJ z1*c@G6SgXmac^f4VKvPgzZ;A21s)qiF6+K^o3bWS8PO4<{lfVgad>k$RjNtURoY(G1;Tgb`(h%U!c(l*tBVs)f zHl0Br0TtVFcSpk`HM4xjegaJtdR?q3nf=XL6|>z8fzJ+0)}w1SqqJC>;9 zx!F@q;iAFw4y{Q$Mt}Atzw`a74gIsB+D&RMj4)=sFmeGwn(YDcYv5TCH2?>in18w- z;_O}rOWtU7&vgIK;p7+?GP(c`Cr4v}56KlZU0D^?ZPEX~!^yludnrQfW~=CFX7F_J z2?&fYEjixbZxzMAX1{IjzK>I2=zW5s0L|#nrwjNTQoVZzSYN8@U%cZQt zGkzHjZugME=$FqOIhIH#f2`~KR#jCQnA8>gKbp=mDyycA!l0B0DBazubcb{|DDi-F zmo!L7H-dC`hjfFq64K2B(%s$P@UHd!z@IKybIzQZeeZp38Zi5egDN!v@Z*M?T-0IL z*n5;EJCjQu5|6)Y_h1_8*EXl`zI=5BO3_MAcy0PKG6gK1~M~99X=ksB)yD zL8b&5FOWswySRLMOx#42KO<;S-cjmdUFtxG)qcEqHyBxSA1n9$^uc$|l{HES!(nFf zUos!IZ>uw7F-p=8yovrx-9n93SI5!%_(sn7&+0~hT~U$r-Zk4Emu|J!Q~aXcXA1)s z=j&lrc>#n@*9?WV*UhFm@6#oPt!L+Hn>Ru9Z9JTTM{8+<1 z2c^jUa_9VW+c-8nk~w&z?##!4=|v*X=nz@BiVM(rZ=E0nu69Le0_a@M2`l96W@9V;4c0G*p1Jn@AiOna{@G} z(}~Q-xQ}}7@T}o;jn7ALPy?Uw{1X&}Fh4(E)AyJO{c zuhMKhPv$ECGwi2*L4U`{xCZ*`R~px%)<~bO5dpS|7r5&MfW(>R^LX>05=okDx=r-0 zYU6vH&}1se`G(x2@t{ln$kX#e5dT=VUDYx785nq-jVz@?lQpebC@e35!K-INdrcHw zWXdV0q;vcht#s%9!-A&D+_djJM$1a{-67svNKK!%{x_w~R>6KPpQp=wyV$;I-2LL; zCwsbg;qRPvDo&#K%{@1h^Eh}Y2U9KY>Q`0X*Hy(teUzUdeD+Ibx|G!Es_(IkzDDC4 z^*GGBIHUjL$XeC4ymo0U^Y7B-LKmZ$THq`J&zXw%;H*LeB_8qI57)0CYUfQ^ub zhi{}+5Ve^V?KpGqaD667 zog$)ri-gknT`iWX0s!Q|&n`)#j?rCq%~?D16cwTAa7r&28K+KF=SoKI;?x1k%9~W=vP2~{ z16^96Il6aajk+!~oY;41{ds9WUEOCp}{ zT7CZ!yS`BN>5i1n&o8(MD#;GM1p#dl7%^_8-}QwVK}jJj9ruMYTEr(0`w8$pNk{|2 z$08|*J}~O>FK0#yPa4@hook(CC`@ihOx3rlj3FV-KmYoZmTxXy4gUdDk4DQ&F3m~_ zJNbX%w%i9ma7>}^WAwhoT(7PM%k=MrtWg&WU>j|e1Ov0_fE0`u{}GaW!RJ9Cluh|t z6*QcDr|QX}0C~7`WjsM5MU1=Ro;#cu(i^X6mBDxR;?L9A{fG~Z!Yc5%gPEH6Tp;dh zSpM4M=-UVSt|~GHP01R!J&V?~8YI(`grw;K9AQ?Z1^ z-_qp^^8!ThxN`N^`p_RY31M>fW+<_7*FtUX(zJ{Kf)_A5oA%l^i1=-&a^-HP(HQ9u z#tvwX=eEpQ%@m*Z=YIE-gwMP;Z#uKf+MCpK$JUsE1bgm(ijXD^G%9OnZMDYl{3H_z z_-|oRLVOmyAP{o7{YGwXIOvfe#zsZ z%ZuhcjXur!0mdR@M`5uoXEt?sXoapxsP@RZOkA3`N2ZXO0CJMgFWB0SQ)J2a(qKrb z>@pT8v&r~C-M$R3o$4Jg-H1K&@h7`D=0c1!>e!oZ`j}ae_gN2=R z{=P>MS6N>PM&w>z0`X&MC0y;!gFYlFq7|N@ojsan@6!#e9*S`2S5NqF zCCj?YX5?gz3M+J*8!AqdF|~ph9Ke1FUOk1m=*LQ>J=xw6$#V0ri`%r=O^r#{DpT|G zY{-Ij`Mm?bzYh+wazrZlw8l?=^2C~LhBJCr-5$JaqZH4T--=Ddw&(8PxfGXhT^UiP zh43I7F6(UbSbHNtcu2qi6#&A}fW=@e+Pg;r7t&LvRTG;4{EzqdUrqm}C08kB`}>Q% z5OhXRk(+$`%9cBTQqkBl36M$gTs1l0Pv|zRl7150L6{0BLu%}geksPSCbb4$KJ~## z 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