Skip to content

[9/10] Version draft settings and separate serving serialization - #1045

Draft
jiapingW wants to merge 1 commit into
split/config-layer-specfrom
split/config-04-format
Draft

jiapingW wants to merge 1 commit into
split/config-layer-specfrom
split/config-04-format

Conversation

@jiapingW

@jiapingW jiapingW commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Add schema_version and the canonical layer format, while decoding legacy fields at the boundary. Group format handling, the HF save adapter and compatibility helpers under config/draft/serialization/. Save canonical draft settings for training checkpoints and generate separate HF/SGLang serving configs. Reject unsupported heterogeneous serving exports.

Depends on #1052.

Validation: 658 CPU-suite tests completed, 48 skipped.

Stack: #1042 → #1043 → #1058 → #1059 → #1044 → #1060 → #1061 → #1052 → #1045 → #1046.
Independent CPU CI: #1053.

@maocheng23

Copy link
Copy Markdown
Collaborator

Reviewed the stack at this PR's head (6aadf9e), CPU only. Three things:

1. save_pretrained now writes a layout SGLang can't read

DraftSerializationMixin.to_json_string moves block_size, target_layer_ids, mask_token_id, conv_* and selector_* out of dflash_config (and the root) into spec_config. Only to_hf / to_sglang rewrite config.json back to the legacy fields. SGLang's parse_dflash_draft_config only reads dflash_config.* and the root block_size, so a draft saved with a plain save_pretrained (user script, notebook, external eval kit) is served with block_size=16 and default target layers for DFlash/DSpark, with no error. DFlash2 at least fails on the missing selector_rank.

In-tree this is fine today: save_pretrained is only called from export, and training checkpoints are torch state. It is still an easy trap. Options: dual-write (legacy fields plus spec_config) until SGLang reads the new schema, or keep config.json legacy and write the canonical form only to specforge_config.json.

2. Schema: settle per-layer structure before #1046 migrates the recipes

attention_config / ffn_config / residual_config each hold one value for the whole model. Attention settings also live in three places:

  • head counts and head_dim at the root
  • SWA in root layer_types / sliding_window / use_sliding_window / max_window_layers
  • type, is_causal and MLA dims in attention_config

The work already in flight needs different structures per layer: KDA+GQA hybrids (#836), per-layer attention modes and SWA-MLA (#767), MoE on some layers (#1003), GR/mHC (#1027), DSA/QSA (#1030). Each of these would force a schema_version: 2 and a second migration of all 51 recipes.

Proposal, following the HF target-config convention (global dims at the root plus a per-layer type list):

{
  "spec_config": {"algorithm": "dflash2", "block_size": 8, "target_layer_ids": [5, 19, 33, 47, 61],
                  "mask_token_id": 248070, "selector_rank": 256, "selector_top_k": 16},
  "hidden_size": 5120, "num_hidden_layers": 5, "vocab_size": 248320, "rms_norm_eps": 1e-6,
  "layer_config": {
    "attention": {
      "swa": {"type": "gqa", "num_heads": 32, "num_kv_heads": 8, "head_dim": 128,
              "sliding_window": 2048, "is_causal": false},
      "kda": {"type": "kda", "num_heads": 32, "head_dim": 128}
    },
    "ffn": {
      "dense": {"type": "dense", "intermediate_size": 17408},
      "moe": {"type": "moe", "num_experts": 16, "top_k": 2}
    },
    "conv": {"g16": {"type": "grouped_causal", "kernel_size": 2, "group_size": 16}},
    "layers": [
      {"attention": "kda", "ffn": "dense", "conv": "g16"},
      {"attention": "swa", "ffn": "moe", "conv": "g16"}
    ]
  }
}
  • spec_config.algorithm is the only selector. architectures comes from the registry and is written at export.
  • layers is either one object (applied to every layer) or a list of length num_hidden_layers that references named presets.
  • When layer_config is absent, build it from the HF root fields. Every existing config then stays valid as the uniform case.
  • Boundary rule:
    • Anything that changes parameters inside a decoder layer goes in layer_config. That moves DFlash2's grouped conv out of spec_config.
    • Anything that changes the draft's input/output contract stays in spec_config: block size, target layers, mask token, selector, Markov/confidence heads, Domino GRU, DSpine injection.
  • The execution backend (sdpa / flex / FA, ring / USP) stays in the training config, as [5/10] Move draft models and layers to canonical paths #1044 already does.
  • Each component type registers a typed config class that validates its fields and rejects unknown keys. build_dflash_attention / build_ffn from [5/10] Move draft models and layers to canonical paths #1044 take the per-layer spec instead of the global config.
  • Legacy fields cannot describe different per-layer structures at all, so export translation alone won't cover them. A small torch-free schema/parser module that SGLang can also import would let train and serve parse the same thing. Until then, export should fail loudly for these layouts instead of silently degrading.

#1042–#1044 look compatible with this direction (typed options and component factories), so the change should stay within this PR and #1046.

3. Smaller items

  • Across the stack: tests/test_modeling/test_draft/test_llama3.py::TestLlamaForCausalLMEagle3Loading::test_config_validation fails from [2/10] Add immutable draft configuration types #1043 onward. It expects AttributeError, but Eagle3Config now raises ValueError("EAGLE3 requires num_hidden_layers=1") first. Stacked PRs only run lint, so CI won't show this until the PR is retargeted to main.
  • The exported config.json also carries root-level attention_config / residual_config / ffn_config (from the runtime to_dict()). SGLang ignores them today.
  • Checked and fine:
    • SGLang treats is_causal: null as the legacy default.
    • Dropping confidence_head_alpha is safe, because the loss weight comes from training.dspark_confidence_head_alpha.
    • All 51 configs/*.json load and finalize.

@jiapingW jiapingW changed the title [4/5] Add canonical draft settings and serving serialization [5/6] Version draft settings and separate serving serialization Oct 30, 2026
@jiapingW
jiapingW changed the base branch from split/config-03-components to split/config-layer-spec October 30, 2026 13:58
@jiapingW jiapingW changed the title [5/6] Version draft settings and separate serving serialization [9/10] Version draft settings and separate serving serialization Nov 1, 2026
@jiapingW
jiapingW force-pushed the split/config-layer-spec branch from 6379350 to a0ea9b0 Compare November 1, 2026 06:11
@jiapingW
jiapingW force-pushed the split/config-04-format branch from c774859 to 795cc1a Compare November 1, 2026 06:11
@maocheng23

Copy link
Copy Markdown
Collaborator

Schema v1 persists model_type: "qwen3" for every DFlash-family draft:

  • encode_format dumps DraftModelConfig.to_dict() (format.py#L56).
  • inline_values sets model_type from the registered HF config class (model.py#L190).

At the stack tip (92cda43), the LFM2.5 DFlash recipe encodes to {"schema_version": 1, "model_type": "qwen3", ...}.

v1 is the format we'll have to keep reading. I'd keep HF's model_type out of it, or write a draft-owned value, and have legacy.py add model_type: qwen3 only when it writes the SGLang config.json.

Related: DraftSaveConfig is already a generic PretrainedConfig, but it is still built from the Qwen3Config adapter (hf.py#L16). Building it directly from the canonical dict would take Qwen3Config out of the canonical save path.

More context in my #1043 comment.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants