feat(api): add length-bucketed batch sampling to reduce padding (#204) - #361
Open
GarfieldFine wants to merge 3 commits into
Open
feat(api): add length-bucketed batch sampling to reduce padding (#204)#361GarfieldFine wants to merge 3 commits into
GarfieldFine wants to merge 3 commits into
Conversation
…AI#204) Add an optional seeded length-bucketing sampler that groups similar-length items into the same batch, reducing wasted padding tokens during training. Changes: - New module areno/api/length_bucketing.py with bucketed_batch_indices() - Trainer.load_prompt_batches() gains length_bucket_seed parameter (None = sequential, backward compatible; int = bucketed mode with pre-scan) - SFTTrainer._iter_train_batches() gains bucketed path using full prompt+target length as the sort key - TrainerConfig gains length_bucket_seed field with negative-value validation - CLI --length-bucket-seed option in the Rollout group, wired through all 4 config constructors (SFT/DPO/GSPO+GRPO/PPO) - PolicyOnlyTrainer passes the seed to load_prompt_batches() - docs/cli/training.rst documents the flag with a copyable example and observable output description - 22 CPU-only tests covering core logic, integration, config validation, cross-module CLI flow, backward compatibility, and boundary cases - Existing test fixtures updated to include the new field default Verified with 500 real prompts + Qwen3-0.6B tokenizer: padding reduced 77.7% (12149 -> 2713 tokens), compute efficiency 45.2% -> 78.5%. All 149 tests pass (127 existing + 22 new).
Add 3 integration tests for SFTTrainer._iter_train_batches() bucketed path: - test_sft_bucketed_each_sample_once: verify all rows appear exactly once - test_sft_bucketed_reduces_padding: verify bucketed mode produces less padding - test_sft_seed_none_preserves_sequential: verify seed=None keeps original order Uses a mock char-level tokenizer (chat_template=None) so encode_generation_prompt falls back to tokenizer.encode() directly, avoiding the chat template path. Total: 25 tests in test_length_bucketing_cpu.py, 152 total pass.
- Fix missing newline at end of length_bucketing.py and test file - Apply ruff lint fixes (2 errors auto-fixed) - Apply ruff format (reformat test file for line length and spacing) - No functional changes, all 152 tests still pass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Add an optional seeded length-bucketing sampler that groups similar-length items into the same batch, reducing wasted padding tokens during training.
Motivation:
Trainer.load_prompt_batches()(areno/api/trainer.py:207) fills batches in dataset order. When_make_train_pack()(areno/api/backend/areno/backend.py:457) right-pads all sequences to the batch max length, mixing short and long prompts in the same batch wastes compute on padded positions. SFT has the same issue in_iter_train_batches()(areno/api/trainers/sft.py:97).Changes:
areno/api/length_bucketing.pywithbucketed_batch_indices()— a pure-Python, CPU-only function that sorts indices by length, groups into buckets, shuffles within and across buckets, then chunks into batchesTrainer.load_prompt_batches()gainslength_bucket_seedparameter (None= sequential, backward compatible;int= bucketed mode with pre-scan)SFTTrainer._iter_train_batches()gains bucketed path using full prompt+target length as the sort keyTrainerConfiggainslength_bucket_seedfield with negative-value validation in__post_init__--length-bucket-seedoption in the Rollout group, wired through all 4 config constructors (SFT/DPO/GSPO+GRPO/PPO)PolicyOnlyTrainerpasses the seed toload_prompt_batches()docs/cli/training.rstdocuments the flag with a copyable example and observable output descriptionareno/api/length_bucketing.pybucketed_batch_indices()function — sort, bucket, shuffle, chunkareno/api/trainer.pyload_prompt_batches()gainslength_bucket_seedparam; split into sequential + bucketed pathsareno/api/trainer_config.pyTrainerConfiggainslength_bucket_seedfield with negative-value validationareno/api/trainers/policy_only.pylength_bucket_seedtoload_prompt_batches()areno/api/trainers/sft.py_iter_train_batches()gains bucketed path using full prompt+target lengthareno/cli/train.py--length-bucket-seedCLI option, 4 config constructors, summary displaytests/test_length_bucketing_cpu.pytests/test_config_data_cpu.pylength_bucket_seeddefaulttests/test_train_cli_config_cpu.pylength_bucket_seeddefaultdocs/cli/training.rst--length-bucket-seedwith example and observable outputDesign decisions:
load_prompt_batches()and is DP-unaware. The existingsplit_list_by_dpround-robin DP sharding operates on already-formed batches and is not affected.len(item.input_tokens)); SFT path buckets by full sequence length (len(seq.tokens), i.e. prompt + target).num_bucketsdefaults tomin(len(items) // batch_size, 128)so each bucket holds multiple batches, giving intra-bucket shuffle real effect.input_tokensheld in memory). Acceptable for typical post-training datasets; documented in CLI help.Related issue
Fixes #204
Type of change
How was it tested?
CPU test suite (no GPU required):
pytest tests/test_length_bucketing_cpu.py tests/test_trainer_api_cpu.py tests/test_config_data_cpu.py tests/test_train_cli_config_cpu.py -v
Result (macOS arm64, Python 3.12.13): 152 passed, 0 failed.
Result (Google Colab Tesla T4, Python 3.12.13): 151 passed, 1 failed. The 1 failure (
test_training_config_summary_shows_resolved_values_and_warning) is a pre-existing test that hardcodesattn_backend=flash, which auto-falls back tonativeon Colab's Tesla T4 (cc 7.5). This failure is unrelated to this PR's changes and does not occur on GPUs that support flash-attn.GPU smoke training (Google Colab, Tesla T4):
Result: smoke training completed successfully.
--length-bucket-seed 42works end-to-end through the full GPU training pipeline (rollout → reward → train).Real tokenizer validation (CPU + GPU):
Verified with 500 and 2000 real prompts + Qwen3-0.6B tokenizer (loaded from ModelScope):
seed=Nonepreserves original sequential behavior exactlyChecklist
pytest tests/ -k cpu).Breaking change details
No breaking changes.
length_bucket_seeddefaults toNone, which preserves the existing sequential batching behavior exactly. The new CLI option--length-bucket-seedis optional and defaults to disabled.