Skip to content

feat(api): add length-bucketed batch sampling to reduce padding (#204) - #361

Open
GarfieldFine wants to merge 3 commits into
inclusionAI:mainfrom
GarfieldFine:feat/issue-204/length-bucketing-sampler
Open

feat(api): add length-bucketed batch sampling to reduce padding (#204)#361
GarfieldFine wants to merge 3 commits into
inclusionAI:mainfrom
GarfieldFine:feat/issue-204/length-bucketing-sampler

Conversation

@GarfieldFine

@GarfieldFine GarfieldFine commented Jul 29, 2026

Copy link
Copy Markdown

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:

  • New module areno/api/length_bucketing.py with bucketed_batch_indices() — a pure-Python, CPU-only function that sorts indices by length, groups into buckets, shuffles within and across buckets, then chunks into batches
  • 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 in __post_init__
  • 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
  • 25 CPU-only test 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
File Type Description
areno/api/length_bucketing.py New Core bucketed_batch_indices() function — sort, bucket, shuffle, chunk
areno/api/trainer.py Modified load_prompt_batches() gains length_bucket_seed param; split into sequential + bucketed paths
areno/api/trainer_config.py Modified TrainerConfig gains length_bucket_seed field with negative-value validation
areno/api/trainers/policy_only.py Modified Passes length_bucket_seed to load_prompt_batches()
areno/api/trainers/sft.py Modified _iter_train_batches() gains bucketed path using full prompt+target length
areno/cli/train.py Modified --length-bucket-seed CLI option, 4 config constructors, summary display
tests/test_length_bucketing_cpu.py New 25 CPU-only tests: core logic, integration, config, CLI flow, boundary
tests/test_config_data_cpu.py Modified Test fixture updated with length_bucket_seed default
tests/test_train_cli_config_cpu.py Modified Test fixture updated with length_bucket_seed default
docs/cli/training.rst Modified Documents --length-bucket-seed with example and observable output

Design decisions:

  • Bucketing is entirely internal to load_prompt_batches() and is DP-unaware. The existing split_list_by_dp round-robin DP sharding operates on already-formed batches and is not affected.
  • RL path buckets by prompt length only (len(item.input_tokens)); SFT path buckets by full sequence length (len(seq.tokens), i.e. prompt + target).
  • num_buckets defaults to min(len(items) // batch_size, 128) so each bucket holds multiple batches, giving intra-bucket shuffle real effect.
  • Bucketed mode pre-tokenizes the full dataset (all input_tokens held in memory). Acceptable for typical post-training datasets; documented in CLI help.

Related issue

Fixes #204

Type of change

  • ✨ New feature

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 hardcodes attn_backend=flash, which auto-falls back to native on 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):

areno train --ckpt Qwen/Qwen3-0.6B --dataset-path gsm8k:main \
  --reward-fn-path examples/math/math_verify_reward.py \
  --algo gspo --tp-size 1 --world-size 1 \
  --batch-size 4 --n-samples 2 --mini-bs 1 \
  --length-bucket-seed 42 --smoke-train \
  --attn-backend native

Result: smoke training completed successfully. --length-bucket-seed 42 works 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):

Dataset Without bucketing With bucketing Padding reduction Compute efficiency
500 prompts 2379 padding 555 padding 76.7% 45.5% → 78.3%
2000 prompts 57947 padding 13371 padding 76.9% 45.5% → 78.3%
  • All samples appear exactly once per epoch (verified by record id)
  • Same seed reproduces identical batch order; different seeds produce different order
  • seed=None preserves original sequential behavior exactly

Checklist

  • The PR title summarizes the contribution.
  • Linked the related issue in the description (if any).
  • Existing tests pass (pytest tests/ -k cpu).
  • New behavior is covered by tests.
  • Described the test commands run and any hardware limitations.
  • Public API / CLI changes are additive and backward-compatible (see CONTRIBUTING.md).

Breaking change details

No breaking changes. length_bucket_seed defaults to None, which preserves the existing sequential batching behavior exactly. The new CLI option --length-bucket-seed is optional and defaults to disabled.

昭航 and others added 3 commits July 28, 2026 17:42
…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
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.

Group examples by token length to reduce padding

1 participant