Skip to content

feat(experience): pack microbatches at collate time via AutoModel's THD helper - #80

Draft
HuiyingLi wants to merge 3 commits into
mainfrom
huiyingl/refactor/datum-thd-packing
Draft

feat(experience): pack microbatches at collate time via AutoModel's THD helper#80
HuiyingLi wants to merge 3 commits into
mainfrom
huiyingl/refactor/datum-thd-packing

Conversation

@HuiyingLi

@HuiyingLi HuiyingLi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

Packs each microbatch once, at collate time, instead of padding it and then
un-padding it again inside the model forward.

The replay buffer already stores unpadded per-sample records. Today
make_experience_batch re-pads them to [B, T], pack_padded_batch strips that
padding back out inside _forward_backbone, and unpack_to_padded re-pads the
outputs before the loss. make_experience_batch(packed=True) produces the flat
[1, total_tokens] THD batch directly, and the forward consumes it.

The token schema comes from AutoModel's pack_features_for_thd +
packed_sequence_thd_collater rather than being re-derived here, so the pack is
identical to the one AutoModel's own THD/CP pipeline builds. That matters more
than it looks: the load-bearing field is position_ids restarting per sequence,
which is what the flash-attention backend uses to infer sequence boundaries.

Net effect

12 files changed, 124 insertions(+), 164 deletions(-)

  • pack_padded_batch (74 lines) → packed_attn_kwargs (~35): it no longer
    removes padding, builds positions, or rolls targets — the collate does.
  • unpack_to_padded deleted outright. A packed batch needs no inverse: per-token
    outputs keep the flat axis their inputs already share, so
    _restore_full_sequence handles only the cp>1 gather.
  • The packer's index map disappears with it — routing targets are already in
    packed order, and _forward_backbone returns a 5-tuple instead of threading it out.
  • The per-sample shift disappears: each sequence gets a trailing masked slot at
    collate time, so log_probs / entropy / values stay elementwise-aligned
    with action_mask, and the [:, :-1] slice now applies to padded batches only.

What did not disappear is the EP token-count equalization: its length is a
collective, so it stays a forward-time step that extends an existing pack.

Validation

  • GPU, one node, tiny Qwen3 (logs/actor_parity.py): the Actor's
    action_log_probs for padded vs packed input agree to 0.0 on every scored
    position. A control (logs/parity_control.py) shows the same comparison fails
    by 1.13 when position_ids are dropped, i.e. the test can fail.
  • 218 CPU unit tests pass; compileall clean.
  • Not yet exercised: a full RL training run, CP>1 (unchanged path, but untested
    here), and VLM (packing_samples is already force-disabled for VLM).

Dependency

Blocked on NVIDIA-NeMo/Automodel#3514, which adds
pack_features_for_thd. CI stays red until that merges and requirements.txt
bumps the nemo-automodel pin.

…HD helper

The replay buffer already stores unpadded per-sample records, so the padded
batch make_experience_batch rebuilds exists only to be un-padded again by
pack_padded_batch inside the forward and re-padded by unpack_to_padded after
it. make_packed_experience_batch skips that round trip: it packs the loose
records once into the flat [1, total_tokens] THD layout the model consumes.

Token packing delegates to AutoModel's pack_features_for_thd +
packed_sequence_thd_collater instead of re-deriving the schema here, so the
packed batch is identical to the one AutoModel's own THD/CP pipeline builds.
Action-side [T-1] fields get one trailing masked slot so every per-token
tensor rides the same [T] axis, which is what makes a token-mean loss
layout-invariant (asserted in the tests).

Not wired into collate_fn yet: R3 routed_experts ([layers, topk, T]) and VLM
media are rejected, and the EP token-count equalization is a distributed
step that stays in the forward. The padded path is untouched.

Tests: parity with pack_padded_batch on ids / positions / cu_seqlens /
max_seqlen and on every scored target position.

Signed-off-by: HuiyingLi <willwin.lee@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Replaces the standalone make_packed_experience_batch with a packed= flag on
make_experience_batch, so the metadata/episode-field handling is shared
instead of duplicated and there is no second entry point.

Packing now covers routed_experts too: it is stored sequence-last
([layers, topk, T]), so it concatenates on that axis like the token fields
rather than being rejected. position_ids and packed_seq_lens become
Experience fields, which is what lets the forward consume a pack without
re-deriving the layout.

Signed-off-by: HuiyingLi <willwin.lee@gmail.com>
… packer

The forward no longer packs: a packed Experience arrives with its flat
[1, total] tokens, per-sequence position_ids and packed_seq_lens, so
_forward_backbone only describes the boundaries to the attention kernel and
adds the EP-equalized suffix, whose length is a collective and therefore
cannot be known at collate time.

Deleted:
- pack_padded_batch (74 lines) -> packed_attn_kwargs (~35), which no longer
  removes padding, builds positions, or rolls targets; the collate does.
- unpack_to_padded entirely. A packed batch needs no inverse: per-token
  outputs keep the flat axis their inputs already share, so
  _restore_full_sequence only handles the cp>1 gather, and the routing
  targets no longer need the packer's  to reorder.
- _forward_backbone returns a 5-tuple instead of threading  out.

The per-sample shift also disappears: the collate gives each sequence a
trailing masked slot, so log_probs/entropy/values stay elementwise-aligned
with action_mask and the [:, :-1] slice applies to padded batches only.

Verified on one GPU with a tiny Qwen3: padded vs packed action_log_probs
agree to 0.0 (logs/actor_parity.py), with a control showing the comparison
fails by 1.13 when position_ids are dropped. 218 CPU tests pass.

Signed-off-by: HuiyingLi <willwin.lee@gmail.com>
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