You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The policy-only trainer (GRPO/GSPO) spends its per-step CPU time between rollout and train building and repacking Python lists. For each prompt group the current data flow materializes response data three times as Python lists before it reaches the CUDA training tensors:
rollout logprobs are converted GPU→CPU per sequence (engine/worker.py, then backend.pyrollout.logprobs[index, : len(tokens)].tolist());
PolicyOnlyTrainer._materialize_train_batch decodes each completion individually, allocates per-sample [0.0] * prefix + resp logprob/loss-mask lists inside make_reward_record, and builds a TrainSequence with four freshly allocated per-token lists (prompt_mask, tokens, logprobs, advantages);
make_train_pack re-iterates those lists field by field (pad_rows, _make_prompt_mask, _make_advantages) into right-padded tensors.
With long-CoT RL (large max_new_tokens, batch_size × n_samples completions), this is O(response tokens) of Python list allocation and iteration executed serially while the GPUs sit idle between rollout and the next backend.train call. It is also the unit of work the experimental async trainer (#487) will queue, so reducing this overhead benefits both the synchronous and the asynchronous paths.
This is roughly 12 µs of pure Python list work per response token. The per-step materialization cost is linear in response tokens, so it scales to the workloads RL post-training is increasingly dominated by: at ~1M response tokens per step (e.g. 8 GPUs, batch 32 × n 8 × max-new-tokens 4096), the same path would spend an estimated 12–13 s/step in serial CPU list building while all GPUs idle — a double-digit share of the step and growing with CoT length.
Also observed on the same runs: ratio_mean=1.0, ratio_std=0.0 for the current unit-valued GRPO surrogate, consistent with the discussion in #68.
Environment: 8×A100-40GB, torch 2.13.0+cu130, AReno main @ 9218129.
Proposed optimization
A behavior-preserving refactor of the rollout→train materialization path, scoped to the CUDA policy-only pipeline:
replace per-completion tokenizer.decode calls with one batched decode per prompt batch where the tokenizer supports it;
build the training pack directly from rollout tensors where they are still on-device, or from a single flattened CPU pass, instead of allocating per-token TrainSequence lists and repacking them a second time;
compute group-relative advantages (compute_group_advantages) with one vectorized pass over the whole batch using prompt-group boundaries, instead of one numpy call per group;
drop or amortize the per-step float(np.mean(rollout_logprobs)) over the full response-token set (keep the metric, avoid the full-token reduction when it only feeds a log line);
keep the public TrainSequence model and all loss-function contracts unchanged; only internal layout/repack helpers may change.
CPU tests asserting the refactor is behavior-preserving: identical loss and gradient values for GRPO and GSPO on packed and padded batches before/after (reuse the existing tests/ -k cpu fixtures). Baseline already collected: tests/test_algorithms_cpu.py tests/test_losses_rewards_cpu.py tests/test_more_losses_cpu.py tests/test_agentic_cpu.py tests/test_config_data_cpu.py tests/test_parallel_partition_cpu.py → 135 passed;
a bounded GPU end-to-end run on a long-CoT workload (max-new-tokens ≥ 1024) comparing per-step wall time, rollout→train gap, and GPU utilization before/after; report both numbers in the PR. The pre-change baseline above (small + mid workloads on 8×A100) is reproducible from areno train --algo grpo --ckpt Qwen/Qwen3-0.6B --dataset-path gsm8k:main --dataset-loader-fn examples/math/dataset_loader.py --reward-fn-path examples/math/math_verify_reward.py --attn-backend native;
a short profile excerpt (e.g. py-spy / cProfile) showing where the CPU time moves after the change.
I have access to a single-node 8×A100 host and can run the bounded GPU end-to-end validation for this change.
Questions for maintainers
Is a TrainSequence-level layout change (e.g. carrying flat tensors instead of per-token lists) acceptable inside the backend, as long as the public model and loss contracts stay unchanged?
Should the group-advantage vectorization live in areno/api/rewards.py (shared with PPO/DPO paths) or stay trainer-local?
Would maintainers prefer this as one cohesive refactor PR, or split the logging amortization into a separate small PR?
Alternatives considered
Eliminating the GPU→CPU transfer entirely would require scoring rewards on-device, which is impossible for arbitrary Python reward_fns and is the separate concern of feature: add experimental async policy trainer #487 (async overlap).
Leaving the path as-is keeps the code simple but forfeits measurable per-step latency on long-CoT workloads, which is the workload RL post-training is increasingly dominated by.
Motivation
The policy-only trainer (GRPO/GSPO) spends its per-step CPU time between rollout and train building and repacking Python lists. For each prompt group the current data flow materializes response data three times as Python lists before it reaches the CUDA training tensors:
engine/worker.py, thenbackend.pyrollout.logprobs[index, : len(tokens)].tolist());PolicyOnlyTrainer._materialize_train_batchdecodes each completion individually, allocates per-sample[0.0] * prefix + resplogprob/loss-mask lists insidemake_reward_record, and builds aTrainSequencewith four freshly allocated per-token lists (prompt_mask,tokens,logprobs,advantages);make_train_packre-iterates those lists field by field (pad_rows,_make_prompt_mask,_make_advantages) into right-padded tensors.With long-CoT RL (large
max_new_tokens,batch_size × n_samplescompletions), this is O(response tokens) of Python list allocation and iteration executed serially while the GPUs sit idle between rollout and the nextbackend.traincall. It is also the unit of work the experimental async trainer (#487) will queue, so reducing this overhead benefits both the synchronous and the asynchronous paths.Measured baseline (single-node 8×A100, GRPO + GSM8K + Qwen3-0.6B,
--attn-backend native)Per-step phase timing from
train_stats(step_e2e_time_s = step_rollout_time_s + step_train_time_s + materialization):max-new-tokens 512, batch 4 × n 4)max-new-tokens 1024, batch 8 × n 4)This is roughly 12 µs of pure Python list work per response token. The per-step materialization cost is linear in response tokens, so it scales to the workloads RL post-training is increasingly dominated by: at ~1M response tokens per step (e.g. 8 GPUs, batch 32 × n 8 ×
max-new-tokens4096), the same path would spend an estimated 12–13 s/step in serial CPU list building while all GPUs idle — a double-digit share of the step and growing with CoT length.Also observed on the same runs:
ratio_mean=1.0, ratio_std=0.0for the current unit-valued GRPO surrogate, consistent with the discussion in #68.Environment: 8×A100-40GB, torch 2.13.0+cu130, AReno main @
9218129.Proposed optimization
A behavior-preserving refactor of the rollout→train materialization path, scoped to the CUDA policy-only pipeline:
tokenizer.decodecalls with one batched decode per prompt batch where the tokenizer supports it;TrainSequencelists and repacking them a second time;compute_group_advantages) with one vectorized pass over the whole batch using prompt-group boundaries, instead of one numpy call per group;float(np.mean(rollout_logprobs))over the full response-token set (keep the metric, avoid the full-token reduction when it only feeds a log line);TrainSequencemodel and all loss-function contracts unchanged; only internal layout/repack helpers may change.The CLI surface is unchanged:
Validation plan
tests/ -k cpufixtures). Baseline already collected:tests/test_algorithms_cpu.py tests/test_losses_rewards_cpu.py tests/test_more_losses_cpu.py tests/test_agentic_cpu.py tests/test_config_data_cpu.py tests/test_parallel_partition_cpu.py→ 135 passed;max-new-tokens≥ 1024) comparing per-step wall time, rollout→train gap, and GPU utilization before/after; report both numbers in the PR. The pre-change baseline above (small + mid workloads on 8×A100) is reproducible fromareno train --algo grpo --ckpt Qwen/Qwen3-0.6B --dataset-path gsm8k:main --dataset-loader-fn examples/math/dataset_loader.py --reward-fn-path examples/math/math_verify_reward.py --attn-backend native;py-spy/cProfile) showing where the CPU time moves after the change.I have access to a single-node 8×A100 host and can run the bounded GPU end-to-end validation for this change.
Questions for maintainers
TrainSequence-level layout change (e.g. carrying flat tensors instead of per-token lists) acceptable inside the backend, as long as the public model and loss contracts stay unchanged?areno/api/rewards.py(shared with PPO/DPO paths) or stay trainer-local?Alternatives considered
reward_fns and is the separate concern of feature: add experimental async policy trainer #487 (async overlap).