Skip to content

[lora] declarative layouts, support-status registry - #2072

Merged
Zhichenzzz merged 15 commits into
yusheng/lora-plugin-refactorfrom
zhichen/lora-plugin-redesign
Aug 2, 2026
Merged

[lora] declarative layouts, support-status registry#2072
Zhichenzzz merged 15 commits into
yusheng/lora-plugin-refactorfrom
zhichen/lora-plugin-redesign

Conversation

@Zhichenzzz

Copy link
Copy Markdown
Contributor

Stacked on #2017 (base: yusheng/lora-plugin-refactor). Keeps that PR's package layout; redesigns the function-level contracts so supporting a NEW model natively is a one-declaration exercise, plus the fixes found while validating four models end-to-end.

Redesign

Classes carry the taxonomy; every fact is declared once and derived everywhere else.

  • Declarative layouts (spec/attach.py, spec/dims.py): an architecture spec is a class holding a ModuleLayout table — which projections exist, on which physical linear, with which shard geometry. One shared attach_layout() implements the existence-check → target-filter → guard → build → hook walk; spec/dims.py is the only spec-side reader of MCore attribute names. MLA's attach went from ~90 imperative lines to a table; a new layout is ~15 lines of facts.
  • Spec hierarchy (LayoutSpecAttentionSpecBaseGQA/MLA/GDN, HybridGQAGDN(GQAAttentionSpec), SharedOuterExpertMoESpec(GeneralExpertMoESpec)): supported_targets, the canonical --target-modules order, and SGLang's fused-family expansion all derive from the layout declaration. registry.serving_fused_families() feeds expand_sglang_target_modules, so a new architecture's fused groups reach serving-side expansion automatically. ShardLayout/AttentionFamily str-enums replace the bare "column"/"gqa" strings; the parallel name tables that used to live in codec/sglang.py and modules/linear.py are gone (split adapters take member projections from their FusedAttach; MoE specs get MLP target names by injection).
  • ModelEntry(spec, status, reason) with VALIDATED / STRUCTURAL / UNSTABLE: support status becomes one first-class field instead of _RAW_MODE_BACKWARD_UNSTABLE plus comments. Entries record this branch's e2e evidence.
  • Public export descriptors: adapters expose exports() -> ProjectionExport (with SGLangFusedGroup metadata); codec/sglang.py is rewritten on that API — no more _active/_rows private access or isinstance ladders; the MLA fused_qkv_a special case is a ServingGroup declaration in the MLA table.
  • Launcher helpers: default_target_modules(hf_checkpoint) and preflight_native_lora(...) (registry ∧ mbridge bridge ∧ model-args audit, no GPU) — run_lora_native.py stops re-declaring per-family target strings and fails in seconds instead of deep inside torchrun when a conversion bridge is missing.
  • Consolidation: LoRASplitQKV/LoRASplitFC1 collapse into a shared LoRASplitAdapter; the marked-grad cache moves onto the model chunk (the id-keyed module global could outlive rebuilt models); MoE's module-level warn flag becomes a per-run report.

Fixes found by running four models natively

  • miles_plugins/mbridge/kimi_k25.py (new): mbridge has no Kimi bridge, so raw-mode K2.5 conversion was impossible (bridge mode uses megatron.bridge's own KimiK25VL bridge and never converts). Thin DeepseekV3Bridge subclass: text_config descent + language_model. prefix remap; vision weights are skipped by pull-based loading.
  • megatron_to_hf kimi dispatch: _convert_to_hf_core matched only kimi_k25, but the direct weight iterator passes the config class name kimik25config (dead branch); the converter also lacked the raw text-only provider's unprefixed embedding/output/final-layernorm names.
  • convert_kimi_int4_to_bf16.py strips quantization_config: a surviving one sends SGLang through its CompressedTensors path, which serves the BF16 checkpoint with a degenerate context-free forward (identical next-token distribution at every position) — rollouts are garbage and train/rollout logprob_abs_diff sits at ~2.2. Verified by a megatron-vs-HF-vs-SGLang three-way logprob comparison; with the config stripped all three agree.
  • torch_dsa_topk clamps k to the key length: gsm8k-length sequences (< index_topk=2048 keys) crashed torch.topk in raw-mode log-prob compute; pads with -1, the existing invalid-index convention.
  • run_glm5_2_744b_a40b_lora_native.py (new): raw-mode GLM-5.2 launcher (the existing LoRA script is bridge-only). Its toy conversion must be single-rank: the convert tool pipeline-shards across its ranks, and any PP split of the 5-layer toy starts a stage on a DSA skip layer.

Validation

20-rollout native GRPO runs (wandb ch271828n-team/miles_lora_native), acceptance ≈ 0.01 train/rollout logprob_abs_diff:

model logprob_abs_diff
Qwen3-8B 0.0099
Qwen3.5-35B-A3B 0.0105 (grad_norm ~1e-2 throughout; the recorded GDN raw-backward divergence did not reproduce → qwen3_5_moe flipped to VALIDATED)
GLM-5.2 5-layer 0.0096
Kimi-K2.5 2-layer 0.0124 (after the quantization_config fix)

tests/fast/miles_plugins/lora: 47 passed at every step (baseline 47). Behavior-preservation smoke: a 2-rollout Qwen3-8B run on this branch reproduces the baseline's first-rollout logprob_abs_diff (0.00967 vs 0.00978). Adapter attribute names are pinned, so checkpoint keys are unchanged.

🤖 Generated with Claude Code

…scriptors

Function-level redesign of miles_plugins/lora on top of the #2017 layout,
aimed at one-declaration support for new models:

- spec/attach.py + spec/dims.py: architecture layouts become ProjectionBinding/
  FusedAttach tables walked by one shared attach_layout(); dims.py is the only
  spec-side reader of MCore attribute names. GQA/MLA/fused-MLP specs are now
  tables; adapter attribute names are pinned (checkpoint keys unchanged).
- registry.py: ModelEntry(spec, status, reason) with VALIDATED/STRUCTURAL/
  UNSTABLE folds _RAW_MODE_BACKWARD_UNSTABLE and validation tribal knowledge
  into one field; adds default_target_modules() and preflight_native_lora()
  so launchers stop re-declaring family facts (preflight catches
  registered-but-unconvertible gaps like kimi_k25-without-a-bridge upfront).
- modules/linear.py: adapters expose exports() -> ProjectionExport (with
  SGLangFusedGroup metadata); codec/sglang.py rewritten on the public
  descriptor - no more _active/_rows private access or isinstance ladders;
  the MLA fused_qkv_a special case is now a ServingGroup declaration in the
  MLA table.
- spec/moe.py: validate_layer returns skipped targets; the orchestrator logs
  once per run (kills the module-global warn flag).
- distributed.py: marked-grad cache moves onto the model chunk (id-keyed
  module-global could outlive rebuilt models).
- miles_plugins/mbridge/kimi_k25.py: Kimi-K2.5 conversion bridge
  (DeepseekV3Bridge + text_config descent + language_model. prefix).
- scripts/run_glm5_2_744b_a40b_lora_native.py: GLM-5.2 native (raw-mode)
  launcher; both native launchers now preflight before GPU work.

tests/fast/miles_plugins/lora: 47 passed (baseline 47).
…names

_convert_to_hf_core matched only the underscore spelling kimi_k25, but the
direct HF weight iterator passes the config class name (kimik25config), so
raw-mode K2.5 runs died at the first weight sync. Also accept the raw
text-only provider's unprefixed embedding/output/final_layernorm names in
convert_language_model_to_hf (the VL trainer's language_model. prefix is
absent there; HF names keep the multimodal shell prefix either way).
gsm8k-length sequences (< index_topk=2048 keys) crash torch.topk with
'selected index k out of range' in raw-mode log-prob compute; the serving
indexer clamps to context length. Pad the clamped result with -1, the
existing invalid-index convention.
qwen3_5_moe, glm_moe_dsa -> VALIDATED (20-rollout native runs, logprob_abs_diff
0.0105 / 0.0096). kimi_k25 -> UNSTABLE with the measured raw-mode forward
divergence vs SGLang (weights proven bit-identical; suspect mcore-MLA runtime
numerics).
A surviving quantization_config sends SGLang through its CompressedTensors
path, which serves the BF16 checkpoint with a degenerate context-free forward
(identical next-token distribution at every position). Verified: overriding
quantization_config to null restores per-position agreement with both the
megatron raw forward and an HF-transformers reference (~0.01-0.05 logprob).
…0124 after the quantization_config strip fix
Per review feedback: SCREAMING-case module constants (QKV_PROJECTIONS +
GQA_TARGETS pairs, _QKV_NAMES/_FC1_NAMES/_MLA_A_NAMES copies in two codecs,
GQA/MLA family strings, singleton spec instances, _CANONICAL_ATTENTION_TARGETS)
each duplicated a fact that some class already knew. Now:

- specs are a hierarchy (LayoutSpec -> AttentionSpecBase -> GQA/MLA/GDN,
  HybridGQAGDN(GQAAttentionSpec), SharedOuterExpertMoESpec(GeneralExpert...));
  each class declares its projections once, inline in its layout class attr.
- supported_targets, canonical --target-modules order, and SGLang fused
  families all DERIVE from that declaration (registry.serving_fused_families
  feeds expand_sglang_target_modules, so a new layout extends serving
  expansion automatically).
- split adapters take member projections from their FusedAttach instead of
  hardcoded name maps; MoE specs get MLP target names by injection.
- ShardLayout / AttentionFamily str-enums replace bare 'column'/'gqa' strings.
- no module-level spec singletons: instances are assembled in
  registry._build_model_specs().

tests/fast/miles_plugins/lora: 47 passed (baseline 47).
… generics

- drop the COLUMN/ROW/REPLICATED alias line; call sites use ShardLayout.*
  (the enum definition is now the only place the strings exist).
- MLA's _GENERIC_QKV_TARGETS derives from GQAAttentionSpec.layout.fused_targets
  (the parser's all-linear names ARE the GQA split-QKV names) instead of
  restating them.
- inline the single-use HF-naming fallback defaults into resolve_hf_naming.

Kept deliberately: _MLA_A_SERVING_GROUP (a named part of MLA's own layout
declaration, referenced by two bindings) and codec/checkpoint.py's
_MODEL_CHUNK_PREFIX (the checkpoint key format's single source of truth,
private to its module) - neither duplicates a fact owned elsewhere.

tests/fast/miles_plugins/lora: 47 passed.
…se comments

- LoRASplitQKV / LoRASplitFC1 collapse into a LoRASplitAdapter base that owns
  the parameter layout, fused-drain forward, absent-slot zero-fill, and export
  descriptor; subclasses keep only their slot geometry and output packing
  (~90 duplicated lines gone). Per-projection export emission is shared in
  NativeLoRAAdapter._export_projections.
- delete unused attach_layouts; fold single-caller resolve_registered_model_spec
  into _resolve_registered_entry.
- trim registry evidence comments to one-liners and other commentary noise.

tests/fast/miles_plugins/lora: 47 passed.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@Zhichenzzz Zhichenzzz changed the title [lora] declarative layouts, support-status registry, and the fixes from validating 4 models natively [lora] declarative layouts, support-status registry Aug 2, 2026
… package

- spec/layout.py now holds the whole declarative-layout mechanism: dimension
  resolvers (still the only spec-side reader of MCore attribute names),
  binding/group dataclasses, the attach walk, and the spec base classes.
- codec/ was an uncommon name for three files that share no code; they move to
  the plugin root under conventional names: hf.py -> hf_adapter.py (HF/PEFT
  naming + import/export), sglang.py -> serving.py, checkpoint.py ->
  checkpointing.py.
@Zhichenzzz
Zhichenzzz merged commit dd8d196 into yusheng/lora-plugin-refactor Aug 2, 2026
17 checks passed
@Zhichenzzz
Zhichenzzz deleted the zhichen/lora-plugin-redesign branch August 2, 2026 22:33
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.

1 participant