Skip to content

feat(dspark): Inkling-era trainer v2 — new-target adaptation + config-level backbone - #4

Open
Boreas618 wants to merge 26 commits into
dspark-trainerfrom
inkling-dspark-v2
Open

feat(dspark): Inkling-era trainer v2 — new-target adaptation + config-level backbone#4
Boreas618 wants to merge 26 commits into
dspark-trainerfrom
inkling-dspark-v2

Conversation

@Boreas618

@Boreas618 Boreas618 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Item 14 (core port) — intentionally based on dspark-trainer, NOT main (owner decision). main's online mode is the disaggregated runtime; this stack IS the colocated in-process-engine topology, and main's dspark modeling files have evolved separately. Reconciliation with main happens when the branches merge.

The DSpark trainer as evolved for a second target family (42-layer hybrid-SWA MoE, NVFP4, eos-less tokenizer, 1M context):

  • target: frozen embed/head at padded_vocab_size; per-arch embed/head key remapping
  • data: assistant-pattern + ThinkingParser, Inkling chat template, seeded node-file sampler + content-hash dedup, tokenized-cache key includes the train-data content hash
  • in-loop eval: stop-token resolution falls back to the draft config's eos_token_id when the tokenizer defines no eos; task auto-discovery from eval-datasets-dir
  • resume: world-consistent resume guard — fixes a deterministic post-checkpoint rendezvous deadlock
  • backbone v2 is config-level: uniform taps, target-matched GQA, block-size-adaptive loss decay all in the draft config; capture-parity probe included

Stacked: #5 (evals-off), #6 (drains), launcher+probe fix. On main instead: #3, #7, #9, #10, #12, #13.

🤖 Generated with Claude Code

@Boreas618

Copy link
Copy Markdown
Owner Author

Cross-reference: upstream sgl-project#734 (Inkling managed capture, disaggregated).

Both PRs solve the same logits_mup_width_multiplier double-application problem, with opposite strategies:

  • this PR (colocated path): target_utils.py folds the multiplier into the frozen head weights (lm_head.weight.div_(mup), tied-embeddings guarded) — capture stores the raw post-norm hidden state, folded head applies the scale once.
  • upstream Support Inkling managed capture sgl-project/SpecForge#734 (disaggregated path): the SGLang capture patch multiplies the stored last-hidden-state back to the pre-head-scale representation — raw state + upstream's folded head, applied once.

⚠️ When migrating this trainer onto upstream's patched capture: the strategies MUST NOT compose. Folded head + restore-before-capture together is correct (both normalize to raw-H × folded-head). Folded head + a capture that stores the divided state double-divides. Our scale-aware probe P1 (probe_nda_inkling_capture.py) is the regression check for exactly this.

Also relevant: upstream #735 adds a constant LR schedule (specforge/lr_scheduler.py) with checkpoint-compat tests — a native replacement for the --fixed-lr feature deliberately excluded from this port.

maocheng23 and others added 26 commits August 1, 2026 17:33
…ion)

Port of TorchSpec PR sgl-project#129 to SpecForge. Adds:
- specforge/modeling/draft/dspark.py: DSparkConfig, VanillaMarkov,
  AcceptRatePredictor, DSparkDraftModel (subclass of DFlashDraftModel)
- specforge/core/dspark.py: OnlineDSparkModel (subclass of OnlineDFlashModel)
  with Markov-biased logits + CE + L1 distribution distillation + confidence BCE
  and a pooled global-mean loss
- scripts/train_dspark.py: training driver (clone of train_dflash.py)
- configs/qwen3-8b-dspark.json, examples/run_qwen3_8b_dspark_online.sh
- last_hidden_states surfaced from the DFlash target backends (HF + sglang)
- tests/test_utils/test_dspark.py: 11 CPU unit tests
… eval)

Train a dense 5-layer DSpark drafter for zai-org/GLM-5.2-FP8 (glm_moe_dsa),
matching the RedHat/UCloud community consensus; validated end-to-end on a
single-node tp=4 smoke against the real 753B target (loss 3.90->3.59,
checkpoint save+reload).

- configs/glm-5.2-dspark.json: dense 5L draft (hidden 6144, head_dim 64,
  block_size 7, aux target layers [1,19,38,57,76], markov 256 + confidence).
- data/template.py + parse.py + preprocessing.py: glm-5.2 chat template +
  GLMParser (forces enable_thinking=False; loss mask = assistant content).
- modeling/target/dflash_target_model.py: set_dflash_layers_to_capture +
  append final post-norm hidden so DSpark L1/confidence get last_hidden_states.
- modeling/draft/dflash.py + core/dflash.py: optional FA4 flex path (gated off).
- scripts/train_dspark.py: 4-node hardening + guarded accept-length eval hook.
- scripts/eval_dspark_deepspec.py: DeepSpec mean-accepted-length eval (9 tasks).
- scripts/prepare_glm52_dspark_data.py + examples/run_glm5.2_dspark_4node.sh:
  mixed-corpus prep + 4-node WORLD=16 DP-attention launcher.
…cept-length eval

- train_dspark.py: torch.compile(dspark_model) after FSDP (SPECFORGE_COMPILE_DRAFT,
  on by default); flex attention + dual-source mask construction excluded via
  @torch.compiler.disable (they fail inductor lowering nested in an outer dynamic
  compile). Validated 2-GPU FSDP: ~55s warm, ~0.1s/step, dynamic shapes, finite grads.
- eval_dspark_deepspec.py: rewrite the accept-length core to DeepSpec's EXACT
  rejection-sampling verify (logits_to_probs / sample_from_probs / sample_residual /
  gather_token_probs ported verbatim; accept_prob=clamp(p_t/p_d,max=1) -> rand<ap ->
  cumprod; +1 bonus unless EOS; accept_rate@k; per-sample seed_all(seed+idx)). Draft
  proposal now returns per-position draft_probs. Bit-exact vs DeepSpec-greedy at temp 0,
  faithful at any temperature. Default task = gsm8k; standalone defaults to DeepSpec's
  gsm8k protocol (500 prompts, 2048 tokens, seed 980406). Fixes a BatchEncoding.shape
  crash in _encode_prompt (GLM tokenizer returns a dict).
- train_dspark.py: --evals-per-epoch (10 evals/epoch); in-loop accept-length eval hook
  now LOGS failures with full traceback (no silent swallow).
- run script: num_anchors 1024 (RedHat), evals-per-epoch 10, EVAL_DATASETS_DIR default
  + gsm8k.jsonl build in `prepare`, SPECFORGE_COMPILE_DRAFT=1.
- core/dflash.py: @torch.compiler.disable on create_dflash_block_mask; draft/dflash.py:
  @torch.compiler.disable on the dual-source attention (keeps block-sparse flex outside
  the outer compile). FA4-flex path retained (gated, off) with the SM100 findings.
- .gitignore: ignore *.local.sh (local launch cheatsheets w/ credentials).
…, no IB/DeepEP)

The devbox container exposes no /dev/infiniband verbs (HCAs are ACTIVE in sysfs but
the char devices aren't mapped in), so NVSHMEM/DeepEP cannot init any cross-node IB
transport (IBGDA fails; IBRC would too) -> Approach 1 (DP-attention+DeepEP) can't run
cross-node. Add APPROACH=2: one per-node tp=NUM_GPUS engine (intra-node NVLink target,
no DeepEP) + cross-node data-parallelism via the draft FSDP over NCCL (TCP when
NCCL_IB_DISABLE=1). DATA_STREAMS/ACC and MEM_FRAC adjust per approach; global batch 512
preserved (A2: dp=4, acc=128). Loss-correct (objective pooled-mean is valid for the
4-replicated tp-groups). Use when IB is unavailable; prefer A1 once /dev/infiniband is exposed.
…crashes with FSDP)

torch.compile(dspark_model) crashes under real varying-length data: dynamo recompiles
on a new shape after a backward has populated grads and hits "assign a gradient of size
[s0] to a tensor of size [s19]" (FSDP SHARD_GRAD_OP sharded-grad vs full-param) inside
Qwen3RMSNorm. The 2-GPU test passed only because its shapes never forced a post-grad
recompile. Eager is validated + stable and the run is target-bound, so default compile
off; re-enable (=1) once the FSDP-recompile interaction is fixed.
Two DSpark training fixes surfaced by the 4-node run that showed loss=inf:

1. Resume silently loaded a RANDOM draft. Checkpoints saved while
   torch.compile was enabled carry an `_orig_mod.` key prefix that neither
   save_checkpoint nor the resume path stripped, so from_pretrained matched
   0 keys and re-initialized the whole draft. Now:
   - save_checkpoint strips both `_orig_mod.` and `draft_model.`;
   - resume loads weights straight from the checkpoint safetensors (stripping
     both prefixes) and RAISES if it matches nothing, instead of training a
     random draft. Verified the existing epoch_0_step_2 checkpoint is 100%
     finite — the inf is a forward-pass issue, not NaN weights.

2. loss=inf diagnostics + opt-in sanitizer in OnlineDSparkModel.forward /
   _dspark_objective:
   - [NONFINITE-INPUT] logs when the captured target hidden (context or
     final) is non-finite — the likely inf source (fc(inf) -> inf logits).
   - [NONFINITE-LOSS] fires only when the loss is already broken (zero cost
     on the healthy path) and reports which numerator (ce/l1/conf), the
     denominators, and whether draft_hidden/base_logits/last_hidden are the
     source.
   - SPECFORGE_SANITIZE_NONFINITE=1 (default OFF) drops non-finite tokens
     from supervision and zeros their hidden so one bad sample cannot
     NaN-poison all ranks via the gradient all-reduce under genuine DP.

Diagnostics default ON (SPECFORGE_DEBUG_NONFINITE=1); sanitizer default OFF
so the first run shows the raw source before we mask anything.
The GLM-5.2 launcher now defaults to the validated topology + robustness so
the real run needs no extra flags:
  - APPROACH=${APPROACH:-2}  (per-node tp=4 + cross-node draft DP over NVLink;
    the DeepEP/IB Approach-1 path is opt-in, pending RDMA in-container).
  - SPECFORGE_SANITIZE_NONFINITE=1 (was opt-in) — drop non-finite tokens from
    supervision so one rare bad FP8-target-capture sample can't NaN all ranks
    over a 1.5M x 10-epoch run; [NONFINITE-*]/[SANITIZE] still log the rate.
  - SPECFORGE_DEBUG_NONFINITE=1, SPECFORGE_COMPILE_DRAFT=0 (already 0) made
    explicit in the launcher.
All remain overridable via env (e.g. APPROACH=1, SANITIZE=0 to crash-on-bad-
sample for diagnosis). Fixed a stale trainer comment (run script sets compile
=0, not =1). The 40-step 4-node smoke was clean, so this is standing insurance.
The 4-node run hung at step ~47: rank 0 blocked in the DataLoader while ranks
1-3 spun in the target's tp=4 collective waiting for it. py-spy showed a fork()ed
DataLoader worker deadlocked — mid-batch tensorize triggered an import, and while
holding the import lock a GC'd wandb public-API object's weakref finalizer fired
and blocked forever on api_cleanup_request (the wandb service socket doesn't exist
in the fork). That worker never yielded its batch.

This is exactly why the REPORT_TO=none smoke ran clean but the REPORT_TO=wandb
real run hung. Fix: force num_workers=0 (load in-process, no fork, no inherited
wandb finalizer). spawn workers avoid the inherit but re-exec this heavy module
per worker and are fragile here; the workload is target-prefill-bound and data is
pre-tokenised (Arrow mmap), so in-process loading costs ~1-2%/step. Launcher now
passes --dataloader-num-workers 0 to match (the trainer forces 0 regardless).
…~2.5x)

Fixes the step-46 CUDA OOM and cuts per-epoch wall clock ~2.5-3x under the
Approach-2 (no-RDMA) topology. Recipe untouched: num_anchors stays 1024,
global batch stays 512, optimizer steps/epoch stay 1445, same LR schedule.

1. Chunked DSpark objective (SPECFORGE_OBJECTIVE_CHUNK_BLOCKS=128, 0=legacy).
   The objective materialized a [nb,7,154880] f32 logits/probs stack — 24.91 GiB
   measured at nb=1024 — inside the ~45 GB left beside the sglang static pool;
   backward pushed it over. Now the block dim is processed in slices under
   torch.utils.checkpoint (recompute-in-backward): peak 3.93 GiB, +46 ms on the
   worst-case sample. Validated exactly grad-equivalent to the legacy path on
   CPU (loss, all metrics, draft/markov/confidence grads) and loss-delta 2.8e-5
   in bf16 at real dims. Includes a cuBLAS fix: vocab linears on sliced 4D
   tensors hit a degenerate batched-GEMM kernel (37.8 ms vs 1.1 ms flattened) —
   all vocab GEMMs now flatten to 2D first.

2. TP-batch scatter (SPECFORGE_TP_BATCH_SCATTER=1, tp-replicated target only).
   Without DP-attention every rank in the node's tp group holds the same batch
   and identical target hiddens; training the draft on all of it just computes
   tp_size identical gradient copies. Now the node batch (default 4 under
   APPROACH=2) is prefilled once by the tp=4 target, then each rank trains the
   draft on a distinct 1/tp slice trimmed to its true length -> 16 unique data
   streams, draft compute deduplicated, ACC 128->32.

3. generate_dflash_data strips right-padding per request (the 750B target no
   longer prefills pad tokens at batch>1) and re-pads the returned hiddens;
   pad positions are never read (loss_mask=0, blocks attend <= anchor).

4. Launcher: BATCH_SIZE defaults to 4 under APPROACH=2, plus
   PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True (the OOM report showed
   12.8 GB reserved-but-unallocated fragmentation; safe — the in-process
   engine runs disable_cuda_graph=True).

Validated end-to-end: single-node 4-GPU smoke (batch 4, scatter, chunked)
ran 40 steps clean — no OOM/non-finite, losses match the pre-change runs,
~1.1 s/iter for 4 samples vs ~0.7 s/iter for 1 before (~2.5x throughput);
batched prefill confirmed pad-stripped (hs_shape 5748 tokens for 4 samples).
anchor sampler, tau_probabilistic

Full training-flow audit against deepseek-ai/DeepSpec@005e03b. Already
exact (verified line-by-line against their source): label convention
(slot k -> anchor+k+1, all slots supervised), eval mask (cumprod prefix),
noise embed (slot0 = anchor token), position ids, attention mask (strict
kv < anchor, bidirectional intra-block), backbone (hidden_norm(fc(.)),
k_norm over ctx+noise concat, RoPE cos[-q_len:]), VanillaMarkov bias on
teacher-forced prev ids applied before CE AND L1, confidence BCE on
[hidden, prev_emb] vs detached accept_rate, loss 0.1*CE + 0.9*L1 + 1.0*conf
with exp(-k/4) decay and pooled global-mean reduction (their 3 all-reduced
denominators are all loss_weight_mask.sum() == our shared denominator),
frozen target embed+head, BF16Optimizer/AdamW wd=0/cosine+warmup 0.04/
lr 6e-4/bf16/epochs 10/global batch 512/max_len 4096/seed 42.

Three mismatches found and fixed:

1. Grad clipping was LOCAL-SHARD norm (BF16Optimizer clips fp32 master
   shards) vs DeepSpec's global-norm FSDP.clip_grad_norm_ — under 16-way
   sharding that is a ~sqrt(world)x looser, non-uniform clip. Now clip the
   FSDP model by the global norm before optimizer.step() (DeepSpec order);
   the optimizer's internal local clip provably no-ops after that.

2. Anchor sampling deviated 3 ways from DeepSpec sample_anchor_positions:
   no first-target-valid rule (loss_mask[p+1]) -> anchors wasted on
   zero-supervision blocks at turn boundaries; candidates capped at
   seq_len - block_size (DeepSpec allows to seq_len-2 with eval-mask
   truncation); off-by-one in the keep cap (batch_max_valid - 1).
   OnlineDSparkModel now overrides the DFlash sampler with a DeepSpec-exact
   port — validated bit-identical anchors + keep masks vs DeepSpec's own
   function under shared RNG across 200 randomized trials. One disclosed
   deviation kept: anchor tensor width caps at the batch max keep count
   instead of always padding to num_anchors (identical supervision; dummy
   columns only burn draft-MLP compute).

3. Added DeepSpec's tau_probabilistic train metric (expected accepted
   drafts per block + 1, over valid blocks) to both objective paths;
   chunked-vs-legacy equivalence re-verified incl. the new metric.

Intentional deviations (owner-locked / infra): num_anchors 1024 (RedHat)
vs DeepSpec reference 512; online sglang FP8 teacher vs offline bf16 HF
cache (same tensors: layer-output hooks + post-norm final hidden);
shard_grad_op vs no_shard; no no_sync during accumulation (math-identical,
~4% comm); compile off (FSDP-recompile crash); chunked objective and
TP-batch scatter (both proven equivalence-preserving).
…odel rescue)

The reshard-safe resume resets Adam moments; the first optimizer steps are
then sign-like kicks of ~lr per coordinate (m_hat/sqrt(v_hat) = +-1 at t=1).
On a converged model at full LR this is destructive — observed live: rollback
to a healthy checkpoint (tau 4.06) degraded to tau ~1.1 within ~5 optimizer
steps of resuming at lr 5.76e-4.

Fix: linearly re-warm the LR over the first N optimizer steps after a
moment-reset resume (SPECFORGE_RESUME_LR_REWARM_OPT_STEPS, default 64). The
second moments re-estimate on real gradients while the weights barely move;
identical factor on every rank keeps sharded updates consistent.

Implementation note: the damp must be applied inside BF16Optimizer.step()
(new optional lr_scale arg) and the original lr restored BEFORE
scheduler.step() — torch schedulers like CosineAnnealingLR are recurrences on
param_group["lr"], so a damped value left in place permanently drags the
remaining schedule down (validated: naive one-shot damp collapsed the
schedule 1e-3 -> 9.4e-5; the fixed path keeps the schedule exact while the
effective LR ramps 0.25x -> 1x).
edge-of-stability collapses

The converged drafter (tau ~4+, gsm8k accept_len 4.76) collapsed twice at the
cosine schedule's peak region (5.76e-4): ce 1.8 -> 40+ with onset within ~3
optimizer steps of running at full schedule LR, at two different data
positions (micro steps ~75.9k and ~77.1k), and was stable through the entire
damped post-resume re-warm both times. Classic sharp-minimum instability; the
cosine stays above half-peak until ~55% of training, so waiting it out is not
an option.

SPECFORGE_LR_SCALE (default 1.0 = DeepSpec-parity recipe; GLM launcher sets
0.5) multiplies the effective LR while leaving the schedule shape exact —
applied through optimizer.step(lr_scale=...), which restores the group lr
before the recurrent scheduler advances. Composes with the post-resume
re-warm ramp. A one-time cross-rank all-reduce check aborts startup if ranks
disagree on the scale (a silent mismatch would diverge the sharded updates).
Distinguishes gradient explosion (grad_norm >> max_norm at collapse onset)
from curvature/step-size instability (collapse with flat grad_norm — Adam's
update magnitude is ~lr per coordinate regardless of gradient scale, which
clipping cannot bound).
(DeepSpec-style incremental verify on the sglang bridge)

The DeepSpec eval re-prefilled the whole growing sequence every verify step
(the training bridge only exposed stateless prefill) -> O(n^2) target compute
per sample, ~50-100x DeepSpec's KV-cached incremental verify (DynamicCache +
crop). New: start_cache_session()/end_cache_session() give the bridge a
persistent RadixCache; each generate_dflash_data call prefix-matches against
it (init_next_round_input), computes only the unmatched suffix, and inserts
the COMMITTED tokens back (cache_finished_req with cache_commit_len). Only
committed tokens are ever cached and drafted-block KV is freed, so a future
match can never overrun the positions the verifier reads. Hidden states are
returned suffix-only with a prefix_len offset (DFlashTargetOutput.prefix_len);
outputs split by extend_input_len (identical to the old full-length split
when no session is active). Training path untouched.

Two traps fixed along the way:
- Paged-allocator double free: free() frees WHOLE pages (unique(idx // page));
  cache_finished_req already frees the page holding the unaligned commit
  boundary, so the manual tail free must start at the next page boundary.
- eval cmd: multi-node dp x tp sweep entry (cmd_eval) in the 4-node launcher.

Eval loop: unified relative indexing (prefix_len=0 without a session), context
maintained incrementally, session released in a finally wrapper,
SPECFORGE_EVAL_KV_REUSE=0 forces the legacy path. Validated: 4-rank lockstep
breadcrumbs across cached+control phases, accept-length parity with the
legacy path (4.88 vs 5.08 on n=4, within greedy near-tie noise from different
kernel shapes), and per-64-page prefix matching observed live. Rows now flush
immediately and partial JSON is checkpointed after each task.
… eval default

Root cause of the ~50% training-data loss: the glm-5.2 chat template hardcoded
assistant_header="<|assistant|><think></think>" (the EMPTY think block). GLM
renders a thinking-ON assistant turn as "<|assistant|><think>{reasoning}</think>
{answer}", so the literal-empty-block header never matched -> those turns got a
zero loss mask and were dropped by the min_loss_tokens filter. That is exactly
the 1,498,000 -> 739,583 (49.4%) filter seen in training: 100% of the dropped
half were thinking-ON regens (the mgoin open-perfectblend regenerations). The
drafter therefore trained thinking-OFF-only and was evaluated thinking-OFF.

Owner decision: target GLM-5.2's default THINKING-ON mode; supervise reasoning
+ answer. Fixes:

- template.py: assistant_header -> "<|assistant|><think>" (the always-scaffolding
  prefix) + assistant_pattern_type="glm".
- parse.py: new "glm" assistant_pattern
  <\|assistant\|><think>(?:</think>)?([\s\S]*?(?:<\|user\|>|$))
  -> supervises {reasoning}</think>{answer} for thinking-ON and just {answer}
  for thinking-OFF (the empty-block </think> scaffold is skipped, unsupervised).
  Validated on CPU: retention 49.4% -> 99.8%; thinking-ON span starts at the
  reasoning and includes the generated </think>; thinking-OFF span excludes the
  scaffold </think>.
- train_dspark.py: (1) hard guard — abort if <90% of samples survive the
  loss-mask filter (SPECFORGE_MIN_RETENTION), so a template/mask mismatch can
  never again silently halve the corpus; (2) cache key versioned
  (maskv2-glm-thinkhybrid) — the processed-dataset cache keyed on template NAME
  would otherwise reuse the stale 49.4%-masked tensors after this content change;
  (3) in-loop --eval-max-new-tokens default 256 -> 1024 so thinking-ON reasoning
  isn't fully truncated.
- eval: SPECFORGE_EVAL_ENABLE_THINKING default OFF -> ON (matches deployment +
  training target); launcher eval max_new 1024 -> 2048 (room for the reasoning
  chain); EVAL_TAG records the thinking mode.

Recipe implication (training deferred): retention ~2x -> ~1.5M samples/epoch,
so steps/epoch and the cosine schedule length ~double vs the prior run. Stale
processed_dataset cache cleared.
… fixed

The training path accreted ~10 SPECFORGE_* env knobs, two import-time
monkeypatches, a broken resume branch, and one-off debug prints during bring-up.
The flow is now settled, so most knobs have a single correct value. Net: the
training path exposes ONE env var (SPECFORGE_DATA_NUM_PROC) down from ~10, with
zero change to training numerics (verified: chunked==legacy objective loss+grads
bit-parity; retention still 100%; defaults equal the old env values).

scripts/train_dspark.py
- Delete the torch.compile path (SPECFORGE_COMPILE_DRAFT) + its per-rank
  inductor/triton cache monkeypatch — compile crashes with FSDP on the
  data-dependent num_anchors shape; eager is the validated path.
- Delete SPECFORGE_OFFLOAD_MASTER (pass offload_master=False) and the
  SPECFORGE_RESUME_FULL_OPTIM branch (impossible multi-rank — only rank-0's optim
  shard is saved; reshard-safe is the sole correct path).
- Hardcode FSDP shard_grad_op (SPECFORGE_FSDP_STRATEGY) and the retention guard
  (MIN_RETENTION=0.9 module constant).
- Promote the two genuine recipe hyperparameters to real CLI args: --lr-scale
  (0.5) and --resume-lr-rewarm-steps (64), keeping the cross-rank consistency check.
- De-magic the PG-timeout monkeypatch (PG_TIMEOUT_MINUTES=45, now setdefault so it
  no longer silently clobbers callers) and delete the dead --dist-timeout arg it
  had been overriding.
- TP-batch scatter: drop the env gate; auto-enable from topology
  (tp-replicated + batch divisible by tp).
- Resume-path raw print() -> print_on_rank0 (stop duplicating across ranks).

specforge/core/dspark.py
- Convert the three objective env knobs to OnlineDSparkModel constructor params:
  objective_chunk_blocks=128, sanitize_nonfinite=True, debug_nonfinite=True.
  Legacy _full_numerators stays reachable via objective_chunk_blocks=0 (kept as
  the equivalence reference). Drop the now-unused `import os`.

specforge/modeling/target/dflash_target_model.py
- Remove the [capture] and one-shot [DEBUG _extend] scaffolding prints. Keep the
  ServerArgs-drop info line, the [KV] debug (gated), and the sglang serial-load /
  moe-runner-backend escape hatches (shared by all entrypoints).

examples/run_glm5.2_dspark_4node.sh
- Drop the exports for the removed/hardcoded knobs + redundant unsets; pass
  --lr-scale / --resume-lr-rewarm-steps in the torchrun call; drop --dist-timeout.

Deliberately untouched (blast radius): optimizer.py (shared lr_scale default used
by 7 other trainers), train_dspark_v4/moe/dflash (own knob copies), the shared
sglang-load knobs, the FA4 flex gate in shared core/dflash.py, and Approach-1.
…pproved)

Force the DeepSpec recipe values where they can apply to the GLM-5.2 target,
per owner decision (with the known trade-offs accepted):

Training
- num_anchors 1024 -> 512. DeepSpec canonical (all their dspark configs use 512).
  Halves supervision density/step; also the likely reason DeepSpec is stable at
  unscaled 6e-4 (our earlier collapse was at 1024).
- Effective LR 0.5x -> 1.0x (unscaled 6e-4) = DeepSpec. --lr-scale default is now
  1.0 (launcher + argparse); the 0.5 escape hatch remains (set LR_SCALE=0.5) if
  the run re-collapses at the schedule peak. grad-norm telemetry + sanitizer stay
  as the safety net.

Evaluation (DeepSpec-exact protocol)
- temperature 0.0 -> 1.0 (DeepSpec's stochastic rejection-sampling default).
- per-task caps: EVAL_LIMIT 128 -> 0, which now maps bare --tasks names to the
  DeepSpec DEFAULT_TASKS caps (gsm8k 500, math500 500, aime25 30, humaneval 164,
  mbpp 256, livecodebench 500, mt-bench 80, alpaca 500, arena-hard-v2 500) via a
  fallback added to _normalize_tasks. max_new 2048 + seed 980406 already matched.

Already identical (verified earlier, unchanged): objective/loss math, block_size
7, 5 layers, markov 256 + vanilla, confidence + with_markov, α 0.1/0.9/1.0,
decay 4.0, base lr 6e-4, warmup 0.04, wd 0, bf16, global_batch 512, epochs 10,
grad-clip 1.0, max_len 4096.

NOT changed (target-dependent, cannot be identical to a Qwen3/Gemma config):
target=GLM-5.2-FP8, target_layer_ids [1,19,38,57,76] (78 layers), mask_token
154856, chat_template glm-5.2, vocab/hidden. NOT forced (owner left unchecked;
would reintroduce known failures): torch_compile (FSDP crash), dataloader
num_workers=4 (wandb+fork deadlock). Kept for safety (non-numerics): FSDP
shard_grad_op (vs no_shard), save-interval 500 (vs 3000) for rollback granularity.
…igned in-loop eval

Fresh-run reset of the GLM-5.2 DSpark pipeline:

- Data: corpus = mgoin/open-perfectblend-glm5.2-regen ONLY (codealpaca mix
  removed everywhere); whole corpus trains, no eval carve (the dedicated
  DeepSpec accept-length suite is the eval; --eval-data-path was dead code
  in this flow and is no longer passed).
- Thinking-ON only: GLMParser renders enable_thinking=True (training prompt
  = deployment prompt byte-for-byte, incl. the "Reasoning Effort: Max"
  header, outside the loss mask); eval _encode_prompt has no thinking-OFF
  path; SPECFORGE_EVAL_ENABLE_THINKING removed. Cache key bumped to
  maskv3-glm-thinkon (train + launcher warm cache kept in sync).
  Validated on 500 real regen rows: retention 100%, header excluded from
  the mask, span = {reasoning}</think>{answer}.
- In-loop eval: gsm8k + mbpp + mt-bench @ 32 prompts each (--eval-tasks),
  temp 1.0 / max_new 2048 / seed 980406 == the final-eval protocol; the
  in-loop subsets are seed-matched prefixes of the final runs. prepare now
  builds all three jsonls per node (byte-identical to the reference files;
  a jsonl present on some replicas but not others deadlocks the eval's dp
  all-reduce).
- Launcher: Approach 2 only (per-node tp engine + cross-node FSDP DP);
  DP-attention/DeepEP path removed (no /dev/infiniband in this container).
  MEM_FRAC default 0.78.
- Trainer defaults resolved to the DeepSpec/owner recipe: block 7, 5 draft
  layers, 10 epochs, max_length 4096.
- Docs: REGEN_OPEN_PERFECTBLEND.md regen+verification guide; execution doc
  updated to match all of the above.
…-level backbone

Ports the DSpark trainer as evolved for a second target family (Inkling,
42-layer hybrid-SWA MoE, NVFP4, eos-less tokenizer):

- target: frozen embed/head allocated at padded_vocab_size (201024), not
  vocab_size; embed/head key remapping per target arch
- data: assistant-pattern + ThinkingParser support, nda_inkling_thinking
  chat template, seeded node-file sampler + content-hash dedup in prepare,
  tokenized-cache key includes train-data content hash (prepare and train
  derive identical keys)
- eval (in-loop): stop-token resolution falls back to the draft config's
  eos_token_id list when the tokenizer defines no eos (raises if neither
  exists); task list auto-discovers <task>.jsonl in eval-datasets-dir
- resume: world-consistent resume guard — all ranks agree on the newest
  checkpoint present on every node, falling back to the newest common one
  (fixes a deterministic post-checkpoint rendezvous deadlock)
- backbone v2 is config-level: KV injection + block-local attention already
  in the modeling stack; uniform target taps, target-matched GQA geometry,
  and block-size-adaptive loss decay are all expressed in the draft config
- capture-parity probe (probe_nda_inkling_capture.py): scale-aware P1 +
  stream-distinctness P2b checks before first smoke

Deliberately NOT included (companion PRs): native-list tools fix
(fix/tools-native-list, based on main), evals-off guard, engine-stream
drains, dual-rope-schema save. Excluded by owner selection: --fixed-lr,
slim checkpoints, dual-node saves, watchdog machinery.
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