Skip to content

Commit 8740508

Browse files
hubert-marekclaude
andcommitted
feat(rl): thread seq_lens through transport, packer, and trainer forward
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>
1 parent d5c7363 commit 8740508

5 files changed

Lines changed: 23 additions & 0 deletions

File tree

src/prime_rl/trainer/batch.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def prepare_sample(training_example: TrainingSample, seq_len: int) -> MicroBatch
214214
rl_weights=rl_weights,
215215
ce_weights=ce_weights,
216216
ref_kl_weights=ref_kl_weights,
217+
seq_lens=[len(input_ids)],
217218
)
218219

219220

@@ -291,6 +292,7 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch:
291292
ref_logprobs: list[float] | None = [] if has_ref_logprobs else None
292293
mm_token_type_ids: list[int] | None = [] if has_mm_token_type_ids else None
293294
streams: dict[str, list[float] | None] = {name: ([] if has_stream[name] else None) for name in STREAM_FILL}
295+
seq_lens: list[int] = []
294296
routed_experts: RoutedExperts | None = None
295297
lora_num_tokens = [0] * num_loras
296298

@@ -322,10 +324,12 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch:
322324
assert routed_experts.shape[1:] == sample.routed_experts.shape[1:]
323325
routed_experts.data += sample.routed_experts.data
324326
routed_experts.shape[0] += sample.routed_experts.shape[0]
327+
seq_lens.extend(sample.seq_lens if sample.seq_lens is not None else [sample_len])
325328
lora_num_tokens[lora_idx] += sample_len
326329

327330
sequence_lengths = [len(sample.input_ids) for _, sample in bin_content.samples]
328331
assert sum(sequence_lengths) == len(input_ids), (sequence_lengths, len(input_ids))
332+
assert sum(seq_lens) == len(input_ids), (seq_lens, len(input_ids))
329333
first_sample = bin_content.first_sample
330334

331335
return MicroBatch(
@@ -345,6 +349,7 @@ def _materialize_bin(bin_content: _MicroBatchBin, num_loras: int) -> MicroBatch:
345349
rl_weights=streams["rl_weights"],
346350
ce_weights=streams["ce_weights"],
347351
ref_kl_weights=streams["ref_kl_weights"],
352+
seq_lens=seq_lens,
348353
)
349354

350355

@@ -464,6 +469,8 @@ def pad_micro_batch(micro_batch: MicroBatch, pad_to_multiple_of: int) -> MicroBa
464469
)
465470
if micro_batch.mm_token_type_ids is not None:
466471
micro_batch.mm_token_type_ids.extend([0] * padding_size)
472+
if micro_batch.seq_lens is not None:
473+
micro_batch.seq_lens.append(padding_size)
467474
if micro_batch.routed_experts is not None:
468475
_pad_routed_experts(micro_batch, padding_size)
469476
micro_batch.env_names.extend([""] * padding_size)

src/prime_rl/trainer/rl/data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class TensorMicroBatch(TypedDict):
3434

3535
# Batch level
3636
lora_num_tokens: Int[Tensor, "n_loras"]
37+
seq_lens: Int[Tensor, "segments"] | None
3738

3839
# MoE router replay
3940
routed_experts: Int[Tensor, "batch seq layers topk"] | None
@@ -129,6 +130,7 @@ def _get_sample_micro_batch(self, generator: torch.Generator) -> TensorMicroBatc
129130
"sequence_lengths": sequence_lengths,
130131
"loss_mask": loss_mask.unsqueeze(0),
131132
"lora_num_tokens": lora_num_tokens,
133+
"seq_lens": torch.tensor(sequence_lengths, dtype=torch.long),
132134
"routed_experts": None,
133135
"mm_kwargs": None,
134136
"mm_token_type_ids": None,
@@ -161,6 +163,7 @@ def _get_micro_batch(self, generator: torch.Generator) -> TensorMicroBatch:
161163
"sequence_lengths": [self.seq_len],
162164
"loss_mask": torch.ones(self.seq_len, dtype=torch.bool).unsqueeze(0),
163165
"lora_num_tokens": lora_num_tokens,
166+
"seq_lens": torch.tensor([self.seq_len], dtype=torch.long),
164167
"routed_experts": None,
165168
"mm_kwargs": None,
166169
"mm_token_type_ids": None,
@@ -256,6 +259,7 @@ def _micro_batch_to_tensor(self, micro_batch: MicroBatch) -> TensorMicroBatch:
256259
env_names=micro_batch.env_names,
257260
sequence_lengths=micro_batch.sequence_lengths,
258261
lora_num_tokens=torch.tensor(micro_batch.lora_num_tokens, dtype=torch.int32),
262+
seq_lens=torch.tensor(micro_batch.seq_lens, dtype=torch.long) if micro_batch.seq_lens is not None else None,
259263
mm_kwargs=mm_kwargs,
260264
mm_token_type_ids=torch.tensor(micro_batch.mm_token_type_ids, dtype=torch.long).unsqueeze(0)
261265
if micro_batch.mm_token_type_ids is not None

src/prime_rl/trainer/rl/train.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,13 +421,18 @@ def load_run_checkpoint(_optimizer, idx: int) -> None:
421421
else None
422422
)
423423

424+
seq_lens = micro_batch["seq_lens"].to("cuda") if micro_batch.get("seq_lens") is not None else None
425+
424426
labels = shift_tensor_left(input_ids)
425427

426428
# VLM + CP is not supported: MRoPE requires global positions but CP shards the sequence
427429
if cp_enabled and mm_kwargs is not None:
428430
raise NotImplementedError("Context parallelism is not supported with VLM/multimodal training")
429431

430432
if cp_enabled:
433+
# seq_lens spans the pre-shard sequence; consuming it under CP needs
434+
# global-boundary handling that isn't wired up here yet.
435+
seq_lens = None
431436
input_ids, forward_position_ids = setup_cp_params(
432437
input_ids, position_ids, cp_rank, cp_size, cp_group, cp_style=config.model.cp_style
433438
)
@@ -465,6 +470,7 @@ def load_run_checkpoint(_optimizer, idx: int) -> None:
465470
temperature=temperatures,
466471
mm_kwargs=mm_kwargs,
467472
mm_token_type_ids=mm_token_type_ids,
473+
seq_lens=seq_lens,
468474
routed_experts=routed_experts,
469475
)
470476

src/prime_rl/transport/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,6 @@ class MicroBatch(msgspec.Struct, array_like=True, gc=False, omit_defaults=True):
108108
# Packer-derived metadata used for run-local token exports.
109109
run_id: str | None = None
110110
run_step: int | None = None
111+
112+
# Packed sample boundaries. Sum equals len(input_ids) when present.
113+
seq_lens: list[int] | None = None

tests/unit/orchestrator/test_batch.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def test_randomized_packing_invariants():
161161
for batch in flat_batches:
162162
assert len(batch.input_ids) <= seq_len
163163
assert sum(batch.sequence_lengths) == len(batch.input_ids)
164+
assert batch.seq_lens == batch.sequence_lengths
164165
assert sum(batch.lora_num_tokens) == len(batch.input_ids)
165166
assert len(batch.env_names) == len(batch.input_ids)
166167

@@ -176,6 +177,7 @@ def test_pad_micro_batch_preserves_explicit_sequence_lengths():
176177

177178
assert len(padded.input_ids) == 6
178179
assert padded.sequence_lengths == [4, 2]
180+
assert padded.seq_lens == [4, 2]
179181
assert sum(padded.sequence_lengths) == len(padded.input_ids)
180182
assert padded.loss_mask[-2:] == [False, False]
181183

@@ -271,6 +273,7 @@ def test_prepare_batch_packs_different_temperatures(make_training_example):
271273
# Second sample (4 tokens): all get temp 1.1
272274
assert flat_batches[0].temperatures[4:8] == [1.1, 1.1, 1.1, 1.1]
273275
assert flat_batches[0].env_names == ["env-a"] * 4 + ["env-b"] * 4
276+
assert flat_batches[0].seq_lens == [4, 4]
274277

275278

276279
def test_prepare_sample_propagates_weight_streams(make_training_example):

0 commit comments

Comments
 (0)