Adds reproducible scripts under scripts/benchmarks/ that compare vLLM colocated rollouts against the transformers continuous batching API (model.generate_batch, paged attention) for GRPO training on Qwen3-4B. Contents: - unsloth_grpo_common.py: shared dataset, reward functions, and GRPO hyperparameters so the two backends differ only in the rollout engine. - qwen3_grpo_vllm.py: baseline training entry using fast_inference=True and TRL use_vllm=True, vllm_mode=colocate. - qwen3_grpo_tpaged.py: candidate using a vanilla HF Qwen3 + PEFT LoRA with TRL use_transformers_paged=True. - cb_vs_vllm_generation.py: standalone generation microbenchmark. - README.md: integration notes, reproduction steps, and observed numbers. On a single B200 with Unsloth Qwen3-4B-Base at LoRA rank 32 bf16, transformers continuous batching reaches 7-9 percent of vLLM throughput on this workload. The README documents the integration sharp edges (top_k=-1, PagedAttentionCache default upper bounds, Unsloth's Qwen3Attention_fast_forward bypassing the functional attention interface, TRL importing GuidedDecodingParams from a newer vLLM that no longer exports it, and UnslothGRPOTrainer expecting for_training / for_inference hooks on the model). The scripts are intentionally self-contained so they are easy to rerun after either upstream change that could close the throughput gap (flash-attn availability, CUDA graphs in ContinuousBatchingManager, persistent paged caches across generate_batch calls, or a paged-compatible Unsloth attention forward).
5.1 KiB
Qwen3-4B GRPO rollout engine benchmarks
This directory holds the reproducible scripts behind the experiment
documented in the accompanying PR: can Hugging Face transformers'
continuous-batching API (model.generate_batch, backed by
PagedAttentionCache) serve as a drop-in replacement for vLLM during GRPO
rollouts on the Qwen3-4B notebook?
The short answer on a single NVIDIA B200 with Unsloth Qwen3-4B-Base, LoRA
rank 32, bf16: transformers continuous batching is functionally correct and
integrates with TRL's use_transformers_paged=True path, but end-to-end
throughput lands at around 7-9 percent of vLLM colocated. Full numbers and
per-step timings are in the PR description.
Files
| File | Purpose |
|---|---|
unsloth_grpo_common.py |
Shared dataset loading, reward functions, and GRPO hyperparameters |
qwen3_grpo_vllm.py |
vLLM baseline training entry (fast_inference=True, use_vllm=True, vllm_mode="colocate") |
qwen3_grpo_tpaged.py |
Continuous-batching candidate (fast_inference=False, use_transformers_paged=True, vanilla HF + PEFT) |
cb_vs_vllm_generation.py |
Standalone generation microbenchmark across both engines |
Reproduce
pip install unsloth "transformers>=4.57" "trl>=0.25" peft vllm
# Generation microbenchmark (32 prompts, 512 new tokens each)
CUDA_VISIBLE_DEVICES=2 python scripts/benchmarks/cb_vs_vllm_generation.py \
--backend vllm --stats_path logs/vllm_gen.json \
--n_prompts 32 --n_rounds 2 --max_new_tokens 512 \
--gpu_memory_utilization 0.6
CUDA_VISIBLE_DEVICES=2 python scripts/benchmarks/cb_vs_vllm_generation.py \
--backend tpaged --stats_path logs/cb_gen.json \
--n_prompts 32 --n_rounds 2 --max_new_tokens 512
# Full GRPO training (20 steps)
CUDA_VISIBLE_DEVICES=2 python scripts/benchmarks/qwen3_grpo_vllm.py \
--max_steps 20 --num_generations 2 --per_device_train_batch_size 2 \
--output_dir outputs/grpo_vllm --stats_path logs/vllm_stats.json \
--gpu_memory_utilization 0.6
CUDA_VISIBLE_DEVICES=2 python scripts/benchmarks/qwen3_grpo_tpaged.py \
--max_steps 20 --num_generations 2 --per_device_train_batch_size 2 \
--output_dir outputs/grpo_tpaged --stats_path logs/tpaged_stats.json \
--max_batch_tokens 16384 --num_blocks 16384
Known integration notes for transformers continuous batching + TRL + Unsloth
These are the sharp edges you hit going down the continuous-batching path and
how qwen3_grpo_tpaged.py handles them:
-
top_k=-1is not a valid value for transformers. vLLM treats-1as "disabled", butTopKLogitsWarperraisesValueError: top_k has to be a strictly positive integer. The script rewritestop_k=Noneon the shared GRPOConfig before handing it toGRPOConfig(use_transformers_paged=True, ...). -
PagedAttentionCachedefault upper bounds are extremely conservative._upper_bound_max_batch_tokens=256and_upper_bound_num_blocks=4096choke decode throughput. The script passesgeneration_kwargs={"max_batch_tokens": 16384, "num_blocks": 16384}which TRL forwards toGenerationConfig, and the CB manager reads them when sizing the paged cache. -
Unsloth's
Qwen3Attention_fast_forwardbypasses the functional attention interface. Callingmodel.generate_batchon an Unsloth-patched Qwen3 model fails insideunsloth.utils.attention_dispatch.run_attentionbecause Unsloth routes through its own dispatcher rather than readingconfig._attn_implementation. The benchmark script works around this by loading a vanilla HF Qwen3 with PEFT LoRA for the tpaged path. This costs the Unsloth training kernels but keeps the comparison clean. A proper upstream fix is to detectconfig._attn_implementationending in_pagedand delegate to the stock transformers forward. -
TRL imports
GuidedDecodingParamsfromvllm.sampling_params. Newer vLLM releases (>= 0.13) have moved or removed that symbol, sotrl.trainer.grpo_trainerfails to import on a fresh vLLM install even if you are not using vLLM.qwen3_grpo_tpaged.pyinstalls a minimal shim before importing TRL. -
UnslothGRPOTrainercallsmodel.for_training()/for_inference(). Importingunslothreplacestrl.GRPOTrainerwithUnslothGRPOTrainer, which assumes the model has these hooks. A vanilla HF model does not, soqwen3_grpo_tpaged.pydoes notimport unslothat all.
Why continuous batching is slower than vLLM on this workload
flash_attnis hard to install on this box (CUDA 13.1 detected, torch compiled against CUDA 12.8), sopaged|flash_attention_2falls back tosdpa_paged. vLLM uses FlashInfer and TRTLLM kernels.- CB re-allocates a fresh
PagedAttentionCacheon everygenerate_batchcall. For GRPO that is once per step. ContinuousBatchingManagerdoes not yet implement CUDA graphs (use_cuda_graph=TrueraisesNotImplementedError). vLLM captures 100+ mixed prefill-decode and decode graphs during warmup.
These are all upstream transformers issues, not Unsloth issues. The scripts in this directory are intentionally simple so they are easy to port into a future upstream fix.