Skip to content

refactor(models): make seq_lens a universal custom-model contract#2971

Closed
hubert-marek wants to merge 11 commits into
feat/rl-multimodal-cpfrom
feat/seq-lens-universal
Closed

refactor(models): make seq_lens a universal custom-model contract#2971
hubert-marek wants to merge 11 commits into
feat/rl-multimodal-cpfrom
feat/seq-lens-universal

Conversation

@hubert-marek

Copy link
Copy Markdown
Contributor

Summary

Part 8 of the #2485 split stack. Based on #2948 (feat/rl-multimodal-cp) — needs the seq_lens boundary contract from #2944 and the CP-global semantics from #2946.

Makes seq_lens a universal custom-model contract: every custom ForCausalLM declares the typed seq_lens / seq_lens_are_global forward parameters, the trainer passes them unconditionally to any PreTrainedModelPrimeRL, and the prime_forward_kwargs hook is deleted. Previously only Qwen3.5 dense/MoE received the packed boundaries (via the hook); every other custom family silently never saw them.

Per-family behavior:

  • Standard attention families (llama, qwen3, qwen3_moe, glm4_moe, laguna, afmoe, minimax_m2, nemotron_h): the flash path keeps deriving cu_seqlens from position_ids (boundary-equivalent — the packer resets positions per document), so numerics are unchanged. Non-flash paths now reject packed rows loudly (Packed <Family> batches require flash attention) instead of silently attending across document boundaries — SDPA/eager have no varlen support.
  • gpt_oss / glm_moe_dsa: boundaries already honored via position_ids on every path (HF packed-sequence masks / sparse MLA varlen indices); they accept the params to satisfy the contract, documented in the signature.
  • Qwen3.5 dense + MoE: consume seq_lens exactly as before (varlen cu_seqlens, fla state resets, CP-global handling), minus the hook indirection.

New-model guidance in docs/development.md updated: declaring and honoring the two parameters is now part of the custom-model checklist.

Validation

  • Full tests/unit/train/ failure set byte-identical to the base tip's pre-existing environment baseline (59, CUDA-dependent) — no new failures; forward-contract tests updated and passing.
  • CPU smoke: migrated Llama root forward accepts seq_lens=[8] single-doc under sdpa and raises Packed Llama batches require flash attention for seq_lens=[5, 3].
  • Custom-model imports are clean — the new parameters are documented for @auto_docstring-decorated forwards.

🤖 Generated with Claude Code

hubert-marek and others added 11 commits July 6, 2026 19:50
CatDataset packs now carry explicit per-sample boundaries instead of
relying on position_ids resets:

- _finalize_pack pads every pack to seq_len (pad tokens form their own
  loss-masked document) and yields the tail pack instead of dropping it;
  overflow samples start the next pack rather than being truncated.
- Sample/Batch gain seq_lens; cat_collate forwards it for batch-size-1
  packs, stack bucketing yields seq_lens=None.
- trainer forward() gains seq_lens and hands it to custom models via a
  new PreTrainedModelPrimeRL.prime_forward_kwargs hook, so PrimeRL-only
  kwargs never leak into generic HF model signatures.
- Dense + MoE Qwen3.5 build cu_seqlens from seq_lens on the non-flash
  path (previously None: fla's GatedDeltaNet carried conv/SSM state
  across packed samples and SDPA saw one causal document). Packed
  hybrid batches with full_attention layers require flash and raise
  otherwise; a config validator rejects pack_function='stack' for
  Qwen3.5 custom flash attention (its varlen kernel assumes batch 1).
- Under CP the trainer passes seq_lens=None for now: boundaries span
  the pre-shard pack and models only have local semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror of the SFT-side contract: packed RL micro-batches carry explicit
per-sample boundaries end to end — prepare_sample stamps them,
_materialize_bin accumulates them across the bin (asserting they sum to
the packed length), pad_micro_batch records padding as its own segment,
and the RL dataloader tensorizes them into the micro-batch for
forward(). Under CP the trainer passes seq_lens=None for now, matching
the SFT side, until global boundary semantics land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renderer-produced multimodal samples flow end to end through SFT:

- SFTDataset keeps the renderer's MultiModalData: image-safe truncation
  (never cuts inside a placeholder run, drops truncated-out images, and
  skips samples whose EOS was truncated away), mm_token_type_ids kept
  aligned through EOS append and causal shift, video inputs rejected
  explicitly. _flatten_mm_items folds per-image processor outputs into
  model-forward kwargs (pixel_values, image_grid_thw, ...).
- Cat packing concatenates mm_kwargs across samples in a pack and
  represents text-only spans as zeros in mm_token_type_ids; stack
  bucketing emits multimodal samples solo. Collates move mm tensors to
  CUDA and keep packed rows batch-size 1.
- Local data_files normalization wraps string message content into
  typed content blocks under a multimodal flag so Arrow can unify
  text and image rows.
- setup_processor loads the AutoProcessor and attaches it to the
  renderer; VLM SFT fails fast without one; weight checkpoints save
  the processor alongside the tokenizer.
- Dense Qwen3.5 gains the composite VLM body (HF vision encoder +
  custom text model) with MRoPE positions from mm_token_type_ids and
  registers in _CUSTOM_VLM_MAPPING; VLM training requires a custom
  PrimeRL implementation. Both dense and MoE VLM bodies always run
  the vision encoder (dummy input on text-only microbatches, kept in
  the backward graph) for FSDP/EP collective symmetry.
- Config: VLM SFT requires micro_batch_size=1; unfreezing the vision
  encoder is incompatible with LoRA; the renderer-vs-VLM rejection is
  gone (renderers own multimodal tokenization now).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
processor.save_pretrained rewrites tokenizer files from the processor's
separately loaded tokenizer, clobbering the configured pad token and any
custom chat template. Save the tokenizer last so it wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without [model.vlm] the custom VLM class still loads and runs the vision
tower on dummy input every microbatch, so its params carry zero — not
None — grads and AdamW weight decay silently shrinks them. Freeze the
tower when no VLM training is configured; mixed image/text batches
cannot occur without [model.vlm], so collective symmetry is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l.vlm]

The processor auto-loads for VLM checkpoints and mm samples would flow
through packing and forward unvalidated (no mbs/attn/CP checks, no
freeze policy). Require the explicit [model.vlm] declaration instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unify model CP setup and make packed-boundary metadata CP-aware:

- setup_model_cp replaces setup_hybrid_cp/setup_nemotron_h_cp: models
  owning linear-attention/Mamba layers expose
  set_context_parallel_attributes (dense + MoE Qwen3.5, NemotronH) and
  wire their own layers; hybrid models without the hook are rejected
  instead of silently misconfigured.
- seq_lens stays global under CP: documents can straddle the shard
  cut, so input_ids/position_ids shard per rank while seq_lens keeps
  the full pre-shard boundaries, flagged via seq_lens_are_global
  through forward() / prime_forward_kwargs into both Qwen3.5 models.
  A cu_seqlens_are_global provenance flag rides along to fla's
  GatedDeltaNet instead of reading cu_seqlens back (which cost one
  CPU-GPU sync per linear-attention layer per microbatch); the CP
  conv path switches to fla's causal_conv1d with a CP context so conv
  state passes between ranks with document-boundary resets.
- VLM CP defers sharding to the model: the vision encoder and the
  image-embed merge see the full sequence, then the root forward
  shards embeds/positions (shard_position_ids_for_cp handles 3D
  MRoPE) and updates ulysses attention params from global seq_lens.
  The SFT trainer skips pre-sharding for MRoPE batches; the vision
  encoder is pinned to SDPA under CP.
- shard_for_cp validates divisibility (uneven shards deadlock ulysses
  all-to-all) and CatDataset's seq_len padding guarantees it; the
  LoRA token count follows the sharded loss_mask.
- Qwen3.5 CP requires the custom implementation (the HF impl has no
  set_context_parallel_attributes); rejected in get_model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multimodal RL samples no longer force one-sample micro-batches: eager
image samples pack with text spans and compatible mm_kwargs samples from
the same run/LoRA, with per-sample boundaries kept in seq_lens. Packing
is always on — VLM training is custom-implementation-only and the custom
Qwen3.5 models advertise supports_packed_multimodal_training; a config
validator requires varlen flash attention for VLM training and
validate_multi_modal_pack fails loudly at startup for models without
packed-MM support.

Ported from #2889 minus its gating (pack_multimodal knob, resolve_pack_multimodal)
and its tokenizer packer params (removed on main since).

(cherry picked from commit 027edc2)
…[model.vlm]

Mirrors the SFT-side check: transported mm samples would otherwise flow
into forward unvalidated (no packability, attn, or freeze policy checks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the SFT VLM CP path in the RL trainer, replacing the blanket
NotImplementedError:

- MRoPE multimodal batches under ulysses defer sharding to the model:
  input_ids/position_ids stay global so the vision encoder and
  image-embed merge see the full sequence; the model root forward
  shards embeds/positions after merge (and routed_experts with them).
  Text batches keep the existing pre-shard path.
- seq_lens passes with seq_lens_are_global=True under CP — micro-batch
  boundaries span the pre-shard sequence and pad_to_multiple_of=cp
  already guarantees shard divisibility (padding is its own seq_lens
  segment).
- The LoRA token-count CP adjustment derives chunk_size from the
  sharded length even when input_ids stays global.
- Config gate relaxed: VLM + CP now requires cp_style='ulysses' and a
  custom model implementation instead of being rejected outright;
  resolve_pack_multimodal drops its CP rejection (packed MM rows meet
  the same divisibility guarantee).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every custom ForCausalLM now declares the typed seq_lens /
seq_lens_are_global forward parameters and the trainer passes them
unconditionally; the prime_forward_kwargs hook is gone.

- Standard attention families (llama, qwen3, qwen3_moe, glm4_moe,
  laguna, afmoe, minimax_m2, nemotron_h): flash path keeps deriving
  cu_seqlens from position_ids; non-flash paths now reject packed rows
  loudly instead of silently attending across document boundaries.
- gpt_oss / glm_moe_dsa honor boundaries via position_ids (HF
  packed-sequence masks / sparse MLA varlen indices); they accept the
  params to satisfy the contract.
- Qwen3.5 dense + MoE consume seq_lens as before, minus the hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hubert-marek hubert-marek force-pushed the feat/rl-multimodal-cp branch from dd21849 to 3e99cac Compare July 7, 2026 21:32
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