From ed57f771923703998a17ad656536ffb460447a2c Mon Sep 17 00:00:00 2001 From: JiangWeixiang <854746559@qq.com> Date: Tue, 28 Apr 2026 13:39:23 +0800 Subject: [PATCH 001/237] [Bugfix ] fix bailing_moe_linear (#40859) Signed-off-by: ghphotoframe <854746559@qq.com> --- .../layers/mamba/mamba_utils.py | 3 -- .../models/bailing_moe_linear.py | 28 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/vllm/model_executor/layers/mamba/mamba_utils.py b/vllm/model_executor/layers/mamba/mamba_utils.py index a5a30502b218..c1fd81e40e34 100644 --- a/vllm/model_executor/layers/mamba/mamba_utils.py +++ b/vllm/model_executor/layers/mamba/mamba_utils.py @@ -55,9 +55,6 @@ def linear_attention_state_dtype( model_dtype: ModelDType | torch.dtype, mamba_cache_dtype: MambaDType, ) -> tuple[torch.dtype, ...]: - # TODO (tdoublep) requires testing - if mamba_cache_dtype == "float32": - raise ValueError("fp32 state for minimax is not yet supported") state_dtype = get_kv_cache_torch_dtype(mamba_cache_dtype, model_dtype) return (state_dtype,) diff --git a/vllm/model_executor/models/bailing_moe_linear.py b/vllm/model_executor/models/bailing_moe_linear.py index e26adc17430e..55ea1bad44db 100644 --- a/vllm/model_executor/models/bailing_moe_linear.py +++ b/vllm/model_executor/models/bailing_moe_linear.py @@ -17,6 +17,7 @@ ) from vllm.forward_context import get_forward_context from vllm.logger import init_logger +from vllm.model_executor.custom_op import PluggableLayer from vllm.model_executor.layers.fla.ops.layernorm_guard import ( RMSNormGated, layernorm_fn, @@ -211,7 +212,6 @@ def __init__( max_position=max_position, is_neox_style=False, rope_parameters=rope_parameters or None, - dtype=torch.float32, ) # Build MLAModules for MultiHeadLatentAttentionWrapper @@ -425,14 +425,18 @@ def _weight_loader(param: torch.nn.Parameter, loaded_weight: torch.Tensor) -> No param.data.copy_(loaded_weight[shard].contiguous()) -class BailingMoELinearAttention(nn.Module, MambaBase): - """ - Bailing MoE Linear Attention implementation using minimax backend. +# --8<-- [start:bailing_moe_linear_attention] +@PluggableLayer.register("bailing_moe_linear_attention") +class BailingMoELinearAttention(PluggableLayer, MambaBase): + """Pluggable Bailing MoE Linear Attention layer which allows OOT backends + to add custom implementations. - This implements the linear attention mechanism from sglang, adapted for vLLM's - v1 engine with MambaBase interface support. + This implements the linear attention mechanism from sglang, adapted for + vLLM's v1 engine with MambaBase interface support. """ + # --8<-- [end:bailing_moe_linear_attention] + @property def mamba_type(self) -> str: return "linear_attention" @@ -569,7 +573,6 @@ def __init__( self.head_dim, max_position=self.max_position_embeddings, is_neox_style=True, - dtype=torch.float32, rope_parameters=rope_parameters or None, ) @@ -754,8 +757,6 @@ def _prefill_and_mix_infer( def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, attn_metadata): """Handle decode (single token per sequence).""" - num_prefill_tokens = attn_metadata.num_prefill_tokens - num_prefills = attn_metadata.num_prefills hidden = linear_attention_decode( q, k, @@ -763,10 +764,10 @@ def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, attn_metadata): kv_cache, self.tp_slope, state_indices_tensor, - q_start=num_prefill_tokens, - q_end=None, - slot_start=num_prefills, - slot_end=None, + q_start=0, + q_end=attn_metadata.num_decode_tokens, + slot_start=0, + slot_end=attn_metadata.num_decodes, block_size=32, ) return hidden @@ -1149,6 +1150,7 @@ def __init__( config.vocab_size, config.hidden_size, quant_config=quant_config, + prefix=maybe_prefix(prefix, "lm_head"), ) self.logits_processor = LogitsProcessor(config.vocab_size) else: From 76c9cccc368fde7f1b8d8a546e3638f4f434c8fd Mon Sep 17 00:00:00 2001 From: anthonsu <50185138+anthonsu@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:42:47 -0700 Subject: [PATCH 002/237] [Core] Fix redundant None append in StepPool.forward for chunked prefill (#41049) Signed-off-by: Anthony Su --- vllm/model_executor/layers/pooler/tokwise/methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vllm/model_executor/layers/pooler/tokwise/methods.py b/vllm/model_executor/layers/pooler/tokwise/methods.py index 9ee6e8527c9a..523e8e4c9a9b 100644 --- a/vllm/model_executor/layers/pooler/tokwise/methods.py +++ b/vllm/model_executor/layers/pooler/tokwise/methods.py @@ -111,7 +111,7 @@ def forward( if step_tag_id is not None: data = data[token_id == step_tag_id] - pooled_data.append(data) + pooled_data.append(data) return pooled_data From a8208e6a81befd781b2a9a8b6b29fd61f5333c66 Mon Sep 17 00:00:00 2001 From: "wang.yuqi" Date: Tue, 28 Apr 2026 15:33:41 +0800 Subject: [PATCH 003/237] [Examples] Resettle features examples. (#40995) Signed-off-by: wang.yuqi --- .buildkite/test-amd.yaml | 34 +-- .buildkite/test_areas/distributed.yaml | 15 +- .buildkite/test_areas/misc.yaml | 6 +- .buildkite/test_areas/model_runner_v2.yaml | 9 +- .github/mergify.yml | 5 +- docs/cli/README.md | 4 +- docs/configuration/conserving_memory.md | 2 +- docs/contributing/profiling.md | 2 +- docs/features/automatic_prefix_caching.md | 2 +- docs/features/context_extension.md | 4 +- docs/features/lora.md | 2 +- docs/features/prompt_embeds.md | 4 +- docs/features/speculative_decoding/README.md | 2 +- docs/features/speculative_decoding/eagle.md | 2 +- docs/features/structured_outputs.md | 8 +- .../models/extensions/runai_model_streamer.md | 2 +- docs/serving/data_parallel_deployment.md | 2 +- docs/usage/reproducibility.md | 2 +- .../automatic_prefix_caching_offline.py} | 2 +- .../prefix_caching_offline.py} | 0 .../reproducibility_offline.py} | 0 .../context_extension_offline.py} | 2 +- .../data_parallel/data_parallel_offline.py} | 6 +- .../multi_instance_data_parallel.py | 2 +- .../kv_events}/kv_events_subscriber.py | 0 .../logits_processor/README.md | 6 +- .../logits_processor/custom.py | 0 .../logits_processor/custom_req.py | 0 .../logits_processor/custom_req_init.py | 0 .../lora/lora_with_quantization_offline.py} | 0 .../lora/multilora_offline.py} | 0 .../openai_batch/README.md | 22 +- .../openai_batch/openai_example_batch.jsonl | 0 .../data_parallel_pause_resume.py | 270 +++++++++--------- .../pause_resume/pause_resume_offline.py} | 0 .../profiling/run_one_batch_offline.py} | 0 .../profiling/simple_profiling_offline.py} | 0 ...ompt_embed_inference_with_openai_client.py | 2 +- .../prompt_embed/prompt_embed_offline.py} | 2 +- .../reset_kv/reset_kv_offline.py} | 0 .../load_sharded_state_offline.py} | 6 +- .../save_sharded_state_offline.py} | 2 +- .../extract_hidden_states_offline.py} | 0 .../mlpspeculator_offline.py} | 0 .../spec_decode_offline.py} | 0 .../structured_outputs/README.md | 10 +- .../structured_outputs/pyproject.toml | 0 .../structured_outputs_client.py} | 0 .../structured_outputs_offline.py} | 0 .../torchrun/torchrun_dp_example_offline.py} | 6 +- .../torchrun/torchrun_example_offline.py} | 2 +- .../routed_experts_e2e.py | 2 +- .../skip_loading_weights_in_engine_init.py | 0 tests/distributed/test_torchrun_example.py | 2 +- .../distributed/test_torchrun_example_moe.py | 2 +- .../v1/spec_decode/test_acceptance_length.py | 3 +- vllm/entrypoints/llm.py | 2 +- .../model_loader/sharded_state_loader.py | 4 +- vllm/model_executor/models/config.py | 2 +- vllm/v1/engine/utils.py | 2 +- vllm/v1/executor/uniproc_executor.py | 2 +- 61 files changed, 234 insertions(+), 234 deletions(-) rename examples/{offline_inference/automatic_prefix_caching.py => features/automatic_prefix_caching/automatic_prefix_caching_offline.py} (98%) rename examples/{offline_inference/prefix_caching.py => features/automatic_prefix_caching/prefix_caching_offline.py} (100%) rename examples/{offline_inference/reproducibility.py => features/batch_invariance/reproducibility_offline.py} (100%) rename examples/{offline_inference/context_extension.py => features/context_extension/context_extension_offline.py} (96%) rename examples/{offline_inference/data_parallel.py => features/data_parallel/data_parallel_offline.py} (96%) rename examples/{online_serving => features/data_parallel}/multi_instance_data_parallel.py (97%) rename examples/{online_serving => features/kv_events}/kv_events_subscriber.py (100%) rename examples/{offline_inference => features}/logits_processor/README.md (90%) rename examples/{offline_inference => features}/logits_processor/custom.py (100%) rename examples/{offline_inference => features}/logits_processor/custom_req.py (100%) rename examples/{offline_inference => features}/logits_processor/custom_req_init.py (100%) rename examples/{offline_inference/lora_with_quantization_inference.py => features/lora/lora_with_quantization_offline.py} (100%) rename examples/{offline_inference/multilora_inference.py => features/lora/multilora_offline.py} (100%) rename examples/{offline_inference => features}/openai_batch/README.md (94%) rename examples/{offline_inference => features}/openai_batch/openai_example_batch.jsonl (100%) rename examples/{online_serving => features/pause_resume}/data_parallel_pause_resume.py (96%) rename examples/{offline_inference/pause_resume.py => features/pause_resume/pause_resume_offline.py} (100%) rename examples/{offline_inference/run_one_batch.py => features/profiling/run_one_batch_offline.py} (100%) rename examples/{offline_inference/simple_profiling.py => features/profiling/simple_profiling_offline.py} (100%) rename examples/{online_serving => features/prompt_embed}/prompt_embed_inference_with_openai_client.py (96%) rename examples/{offline_inference/prompt_embed_inference.py => features/prompt_embed/prompt_embed_offline.py} (97%) rename examples/{offline_inference/llm_engine_reset_kv.py => features/reset_kv/reset_kv_offline.py} (100%) rename examples/{offline_inference/load_sharded_state.py => features/sharded_state/load_sharded_state_offline.py} (94%) rename examples/{offline_inference/save_sharded_state.py => features/sharded_state/save_sharded_state_offline.py} (98%) rename examples/{offline_inference/extract_hidden_states.py => features/speculative_decoding/extract_hidden_states_offline.py} (100%) rename examples/{offline_inference/mlpspeculator.py => features/speculative_decoding/mlpspeculator_offline.py} (100%) rename examples/{offline_inference/spec_decode.py => features/speculative_decoding/spec_decode_offline.py} (100%) rename examples/{online_serving => features}/structured_outputs/README.md (85%) rename examples/{online_serving => features}/structured_outputs/pyproject.toml (100%) rename examples/{online_serving/structured_outputs/structured_outputs.py => features/structured_outputs/structured_outputs_client.py} (100%) rename examples/{offline_inference/structured_outputs.py => features/structured_outputs/structured_outputs_offline.py} (100%) rename examples/{offline_inference/torchrun_dp_example.py => features/torchrun/torchrun_dp_example_offline.py} (95%) rename examples/{offline_inference/torchrun_example.py => features/torchrun/torchrun_example_offline.py} (99%) rename examples/{offline_inference => rl}/routed_experts_e2e.py (99%) rename examples/{offline_inference => rl}/skip_loading_weights_in_engine_init.py (100%) diff --git a/.buildkite/test-amd.yaml b/.buildkite/test-amd.yaml index 68179dcb68cd..3ff5413f707e 100644 --- a/.buildkite/test-amd.yaml +++ b/.buildkite/test-amd.yaml @@ -395,11 +395,11 @@ steps: # Pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 # Features demo - - python3 offline_inference/prefix_caching.py + - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 offline_inference/llm_engine_example.py - python3 others/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 others/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 offline_inference/spec_decode.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 - - python3 offline_inference/spec_decode.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 #---------------------------------------------------------- mi250 · kernels ----------------------------------------------------------# @@ -1168,13 +1168,13 @@ steps: - vllm/v1/attention/backends/ - vllm/v1/attention/selector.py - tests/distributed/test_context_parallel.py - - examples/offline_inference/data_parallel.py + - examples/features/data_parallel/data_parallel_offline.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py commands: - export TORCH_NCCL_BLOCKING_WAIT=1 - pytest -v -s tests/distributed/test_context_parallel.py - - VLLM_LOGGING_LEVEL=DEBUG python3 examples/offline_inference/data_parallel.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=allgather_reducescatter --disable-nccl-for-dp-synchronization + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=allgather_reducescatter --disable-nccl-for-dp-synchronization - label: Distributed Tests (4xA100-4xMI300) # TBD timeout_in_minutes: 180 @@ -1203,7 +1203,7 @@ steps: - tests/distributed/test_torchrun_example.py - tests/distributed/test_torchrun_example_moe.py - examples/rl/ - - tests/examples/offline_inference/data_parallel.py + - tests/examples/features/data_parallel/data_parallel_offline.py - vllm/platforms/rocm.py commands: - export TORCH_NCCL_BLOCKING_WAIT=1 @@ -1213,7 +1213,7 @@ steps: - PP_SIZE=2 TP_SIZE=2 torchrun --nproc-per-node=4 distributed/test_torchrun_example_moe.py - DP_SIZE=4 ENABLE_EP=1 torchrun --nproc-per-node=4 distributed/test_torchrun_example_moe.py - TP_SIZE=2 DP_SIZE=2 ENABLE_EP=1 torchrun --nproc-per-node=4 distributed/test_torchrun_example_moe.py - - python3 ../examples/offline_inference/data_parallel.py --enforce-eager + - python3 ../examples/features/data_parallel/data_parallel_offline.py --enforce-eager # rlhf examples - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 ../examples/rl/rlhf_nccl.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 ../examples/rl/rlhf_ipc.py @@ -1266,7 +1266,7 @@ steps: optional: true working_dir: "/vllm-workspace/tests" source_file_dependencies: - - examples/offline_inference/torchrun_dp_example.py + - examples/features/torchrun/torchrun_dp_example_offline.py - vllm/config/parallel.py - vllm/distributed/ - vllm/v1/engine/llm_engine.py @@ -1275,7 +1275,7 @@ steps: - vllm/platforms/rocm.py commands: - export TORCH_NCCL_BLOCKING_WAIT=1 - - torchrun --nproc-per-node=8 ../examples/offline_inference/torchrun_dp_example.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep + - torchrun --nproc-per-node=8 ../examples/features/torchrun/torchrun_dp_example_offline.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep #-------------------------------------------------------- mi300 · entrypoints --------------------------------------------------------# @@ -1654,11 +1654,11 @@ steps: # Pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 # Features demo - - python3 offline_inference/prefix_caching.py + - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 offline_inference/llm_engine_example.py - python3 others/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 others/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 offline_inference/spec_decode.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 - - python3 offline_inference/spec_decode.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 #---------------------------------------------------------- mi300 · kernels ----------------------------------------------------------# @@ -2302,7 +2302,7 @@ steps: commands: - export TORCH_NCCL_BLOCKING_WAIT=1 - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py - - VLLM_LOGGING_LEVEL=DEBUG python3 examples/offline_inference/data_parallel.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput + - VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput - pytest -v -s tests/v1/distributed/test_dbo.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py - pytest -v -s tests/distributed/test_packed_tensor.py @@ -2713,7 +2713,7 @@ steps: - vllm/v1/attention/selector.py - tests/distributed/test_context_parallel.py - tests/v1/distributed/test_dbo.py - - examples/offline_inference/data_parallel.py + - examples/features/data_parallel/data_parallel_offline.py - vllm/_aiter_ops.py - vllm/platforms/rocm.py commands: @@ -2937,11 +2937,11 @@ steps: # Pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 # Features demo - - python3 offline_inference/prefix_caching.py + - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 offline_inference/llm_engine_example.py - python3 others/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 others/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 offline_inference/spec_decode.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 - - python3 offline_inference/spec_decode.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 #---------------------------------------------------------- mi355 · kernels ----------------------------------------------------------# diff --git a/.buildkite/test_areas/distributed.yaml b/.buildkite/test_areas/distributed.yaml index 093f3ab4fe1f..e1d6e2039c59 100644 --- a/.buildkite/test_areas/distributed.yaml +++ b/.buildkite/test_areas/distributed.yaml @@ -88,9 +88,8 @@ steps: - vllm/distributed/ - tests/distributed/test_torchrun_example.py - tests/distributed/test_torchrun_example_moe.py - - examples/offline_inference/rlhf_colocate.py - examples/rl/ - - tests/examples/offline_inference/data_parallel.py + - tests/examples/features/data_parallel/data_parallel_offline.py commands: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 @@ -107,7 +106,7 @@ steps: # test with torchrun tp=2 and dp=2 with ep - TP_SIZE=2 DP_SIZE=2 ENABLE_EP=1 torchrun --nproc-per-node=4 tests/distributed/test_torchrun_example_moe.py # test with internal dp - - python3 examples/offline_inference/data_parallel.py --enforce-eager + - python3 examples/features/data_parallel/data_parallel_offline.py --enforce-eager # rlhf examples - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_nccl.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_ipc.py @@ -159,7 +158,7 @@ steps: num_devices: 8 working_dir: "/vllm-workspace/tests" source_file_dependencies: - - examples/offline_inference/torchrun_dp_example.py + - examples/features/torchrun/torchrun_dp_example_offline.py - vllm/config/parallel.py - vllm/distributed/ - vllm/v1/engine/llm_engine.py @@ -169,7 +168,7 @@ steps: # https://github.com/NVIDIA/nccl/issues/1838 - export NCCL_CUMEM_HOST_ENABLE=0 # test with torchrun tp=2 and dp=4 with ep - - torchrun --nproc-per-node=8 ../examples/offline_inference/torchrun_dp_example.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep + - torchrun --nproc-per-node=8 ../examples/features/torchrun/torchrun_dp_example_offline.py --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep - label: Distributed Tests (4 GPUs)(A100) device: a100 @@ -194,7 +193,7 @@ steps: commands: - pytest -v -s tests/distributed/test_context_parallel.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 python3 examples/rl/rlhf_async_new_apis.py - - VLLM_USE_DEEP_GEMM=1 VLLM_LOGGING_LEVEL=DEBUG python3 examples/offline_inference/data_parallel.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput + - VLLM_USE_DEEP_GEMM=1 VLLM_LOGGING_LEVEL=DEBUG python3 examples/features/data_parallel/data_parallel_offline.py --model=Qwen/Qwen1.5-MoE-A2.7B -tp=1 -dp=2 --max-model-len=2048 --all2all-backend=deepep_high_throughput - pytest -v -s tests/v1/distributed/test_dbo.py - VLLM_ALLOW_INSECURE_SERIALIZATION=1 pytest -v -s tests/distributed/test_weight_transfer.py - pytest -v -s tests/distributed/test_packed_tensor.py @@ -222,9 +221,9 @@ steps: - vllm/executor/ - vllm/model_executor/models/ - tests/distributed/ - - tests/examples/offline_inference/data_parallel.py + - tests/examples/features/data_parallel/data_parallel_offline.py commands: - - ./.buildkite/scripts/run-multi-node-test.sh /vllm-workspace/tests 2 2 $IMAGE_TAG "VLLM_TEST_SAME_HOST=0 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_same_node.py | grep 'Same node test passed' && NUM_NODES=2 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_node_count.py | grep 'Node count test passed' && python3 ../examples/offline_inference/data_parallel.py -dp=2 -tp=1 --dp-num-nodes=2 --dp-node-rank=0 --dp-master-addr=192.168.10.10 --dp-master-port=12345 --enforce-eager --trust-remote-code && VLLM_MULTI_NODE=1 pytest -v -s distributed/test_multi_node_assignment.py && VLLM_MULTI_NODE=1 pytest -v -s distributed/test_pipeline_parallel.py" "VLLM_TEST_SAME_HOST=0 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_same_node.py | grep 'Same node test passed' && NUM_NODES=2 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_node_count.py | grep 'Node count test passed' && python3 ../examples/offline_inference/data_parallel.py -dp=2 -tp=1 --dp-num-nodes=2 --dp-node-rank=1 --dp-master-addr=192.168.10.10 --dp-master-port=12345 --enforce-eager --trust-remote-code" + - ./.buildkite/scripts/run-multi-node-test.sh /vllm-workspace/tests 2 2 $IMAGE_TAG "VLLM_TEST_SAME_HOST=0 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_same_node.py | grep 'Same node test passed' && NUM_NODES=2 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_node_count.py | grep 'Node count test passed' && python3 ../examples/features/data_parallel/data_parallel_offline.py -dp=2 -tp=1 --dp-num-nodes=2 --dp-node-rank=0 --dp-master-addr=192.168.10.10 --dp-master-port=12345 --enforce-eager --trust-remote-code && VLLM_MULTI_NODE=1 pytest -v -s distributed/test_multi_node_assignment.py && VLLM_MULTI_NODE=1 pytest -v -s distributed/test_pipeline_parallel.py" "VLLM_TEST_SAME_HOST=0 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_same_node.py | grep 'Same node test passed' && NUM_NODES=2 torchrun --nnodes 2 --nproc-per-node=2 --rdzv_backend=c10d --rdzv_endpoint=192.168.10.10 distributed/test_node_count.py | grep 'Node count test passed' && python3 ../examples/features/data_parallel/data_parallel_offline.py -dp=2 -tp=1 --dp-num-nodes=2 --dp-node-rank=1 --dp-master-addr=192.168.10.10 --dp-master-port=12345 --enforce-eager --trust-remote-code" - label: Pipeline + Context Parallelism (4 GPUs) timeout_in_minutes: 60 diff --git a/.buildkite/test_areas/misc.yaml b/.buildkite/test_areas/misc.yaml index d0930be156d2..1552aceab4ab 100644 --- a/.buildkite/test_areas/misc.yaml +++ b/.buildkite/test_areas/misc.yaml @@ -120,12 +120,12 @@ steps: # for pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 # for features demo - - python3 offline_inference/prefix_caching.py + - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 offline_inference/llm_engine_example.py - python3 others/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 others/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 offline_inference/spec_decode.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 # https://github.com/vllm-project/vllm/pull/26682 uses slightly more memory in PyTorch 2.9+ causing this test to OOM in 1xL4 GPU - - python3 offline_inference/spec_decode.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 - label: Metrics, Tracing (2 GPUs) timeout_in_minutes: 20 diff --git a/.buildkite/test_areas/model_runner_v2.yaml b/.buildkite/test_areas/model_runner_v2.yaml index 2b88c00d6b77..74025d34f8b7 100644 --- a/.buildkite/test_areas/model_runner_v2.yaml +++ b/.buildkite/test_areas/model_runner_v2.yaml @@ -31,8 +31,9 @@ steps: - vllm/v1/worker/gpu/ - vllm/v1/core/sched/ - vllm/v1/worker/gpu_worker.py - - examples/offline_inference/ - examples/basic/offline_inference/ + - examples/generate/multimodal/ + - examples/features/ - examples/pooling/embed/vision_embedding_offline.py - examples/others/tensorize_vllm_model.py commands: @@ -51,12 +52,12 @@ steps: # for pooling models - python3 pooling/embed/vision_embedding_offline.py --seed 0 # for features demo - - python3 offline_inference/prefix_caching.py + - python3 features/automatic_prefix_caching/prefix_caching_offline.py - python3 offline_inference/llm_engine_example.py - python3 others/tensorize_vllm_model.py --model facebook/opt-125m serialize --serialized-directory /tmp/ --suffix v1 && python3 others/tensorize_vllm_model.py --model facebook/opt-125m deserialize --path-to-tensors /tmp/vllm/facebook/opt-125m/v1/model.tensors - - python3 offline_inference/spec_decode.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 2048 # https://github.com/vllm-project/vllm/pull/26682 uses slightly more memory in PyTorch 2.9+ causing this test to OOM in 1xL4 GPU - - python3 offline_inference/spec_decode.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 + - python3 features/speculative_decoding/spec_decode_offline.py --test --method eagle3 --num_spec_tokens 3 --dataset-name hf --dataset-path philschmid/mt-bench --num-prompts 80 --temp 0 --top-p 1.0 --top-k -1 --tp 1 --enable-chunked-prefill --max-model-len 1536 - label: Model Runner V2 Distributed (2 GPUs) timeout_in_minutes: 45 diff --git a/.github/mergify.yml b/.github/mergify.yml index 8ca00d6e7d2d..de3c76fd458b 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -308,8 +308,7 @@ pull_request_rules: - files=benchmarks/benchmark_serving_structured_output.py - files=benchmarks/run_structured_output_benchmark.sh - files=docs/features/structured_outputs.md - - files=examples/offline_inference/structured_outputs.py - - files=examples/online_serving/structured_outputs/structured_outputs.py + - files=^examples/features/structured_outputs/ - files~=^tests/v1/structured_output/ - files=tests/entrypoints/llm/test_struct_output_generate.py - files~=^vllm/v1/structured_output/ @@ -325,7 +324,7 @@ pull_request_rules: - or: - files~=^vllm/v1/spec_decode/ - files~=^tests/v1/spec_decode/ - - files~=^examples/.*(spec_decode|mlpspeculator|eagle|speculation).*\.py + - files=^examples/features/speculative_decoding/ - files~=^vllm/model_executor/models/.*eagle.*\.py - files=vllm/model_executor/models/mlp_speculator.py - files~=^vllm/transformers_utils/configs/(eagle|medusa|mlp_speculator)\.py diff --git a/docs/cli/README.md b/docs/cli/README.md index c708eb795898..b27bd3b647b5 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -163,7 +163,7 @@ Running with a local file: ```bash vllm run-batch \ - -i offline_inference/openai_batch/openai_example_batch.jsonl \ + -i features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` @@ -172,7 +172,7 @@ Using remote file: ```bash vllm run-batch \ - -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl \ + -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` diff --git a/docs/configuration/conserving_memory.md b/docs/configuration/conserving_memory.md index 8ea241c582e5..2c098118dbb1 100644 --- a/docs/configuration/conserving_memory.md +++ b/docs/configuration/conserving_memory.md @@ -23,7 +23,7 @@ llm = LLM(model="ibm-granite/granite-3.1-8b-instruct", tensor_parallel_size=2) !!! note With tensor parallelism enabled, each process will read the whole model and split it into chunks, which makes the disk reading time even longer (proportional to the size of tensor parallelism). - You can convert the model checkpoint to a sharded checkpoint using [examples/offline_inference/save_sharded_state.py](../../examples/offline_inference/save_sharded_state.py). The conversion process might take some time, but later you can load the sharded checkpoint much faster. The model loading time should remain constant regardless of the size of tensor parallelism. + You can convert the model checkpoint to a sharded checkpoint using [examples/features/sharded_state/load_sharded_state_offline.py](../../examples/features/sharded_state/load_sharded_state_offline.py). The conversion process might take some time, but later you can load the sharded checkpoint much faster. The model loading time should remain constant regardless of the size of tensor parallelism. ## Quantization diff --git a/docs/contributing/profiling.md b/docs/contributing/profiling.md index addda300d020..91757c40e4f8 100644 --- a/docs/contributing/profiling.md +++ b/docs/contributing/profiling.md @@ -42,7 +42,7 @@ Traces can be visualized using . #### Offline Inference -Refer to [examples/offline_inference/simple_profiling.py](../../examples/offline_inference/simple_profiling.py) for an example. +Refer to [examples/features/profiling/simple_profiling_offline.py](../../examples/features/profiling/simple_profiling_offline.py) for an example. #### OpenAI Server diff --git a/docs/features/automatic_prefix_caching.md b/docs/features/automatic_prefix_caching.md index 3718a4b74eb2..fe7977ee23d0 100644 --- a/docs/features/automatic_prefix_caching.md +++ b/docs/features/automatic_prefix_caching.md @@ -11,7 +11,7 @@ Automatic Prefix Caching (APC in short) caches the KV cache of existing queries, Set `enable_prefix_caching=True` in vLLM engine to enable APC. Here is an example: -[examples/offline_inference/automatic_prefix_caching.py](../../examples/offline_inference/automatic_prefix_caching.py) +[examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py](../../examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py) ## Example workloads diff --git a/docs/features/context_extension.md b/docs/features/context_extension.md index f622191aebc6..f96340c3183f 100644 --- a/docs/features/context_extension.md +++ b/docs/features/context_extension.md @@ -6,12 +6,12 @@ This directory contains examples for extending the context length of models usin ## Offline Inference Example -The [`context_extension.py`](../../examples/offline_inference/context_extension) script demonstrates how to extend the context length of a Qwen model using the YARN method (rope_parameters) and run a simple chat example. +The [`context_extension.py`](../../examples/features/context_extension/context_extension_offline.py) script demonstrates how to extend the context length of a Qwen model using the YARN method (rope_parameters) and run a simple chat example. ### Usage ```bash -python examples/offline_inference/context_extension.py +python examples/features/context_extension/context_extension_offline.py ``` ## OpenAI Online Method diff --git a/docs/features/lora.md b/docs/features/lora.md index 2e7b36545d46..d78fdc05792e 100644 --- a/docs/features/lora.md +++ b/docs/features/lora.md @@ -47,7 +47,7 @@ the third parameter is the path to the LoRA adapter. ) ``` -Check out [examples/offline_inference/multilora_inference.py](../../examples/offline_inference/multilora_inference.py) for an example of how to use LoRA adapters with the async engine and how to use more advanced configuration options. +Check out [examples/features/lora/multilora_offline.py](../../examples/features/lora/multilora_offline.py) for an example of how to use LoRA adapters with the async engine and how to use more advanced configuration options. ## Serving LoRA Adapters diff --git a/docs/features/prompt_embeds.md b/docs/features/prompt_embeds.md index b81d2f28e3b9..3d68b07a3ace 100644 --- a/docs/features/prompt_embeds.md +++ b/docs/features/prompt_embeds.md @@ -16,7 +16,7 @@ To input multi-modal data, follow this schema in [vllm.inputs.EmbedsPrompt][]: You can pass prompt embeddings from Hugging Face Transformers models to the `'prompt_embeds'` field of the prompt embedding dictionary, as shown in the following examples: -[examples/offline_inference/prompt_embed_inference.py](../../examples/offline_inference/prompt_embed_inference.py) +[examples/features/prompt_embed/prompt_embed_offline.py](../../examples/features/prompt_embed/prompt_embed_offline.py) ## Online Serving @@ -41,4 +41,4 @@ vllm serve meta-llama/Llama-3.2-1B-Instruct --runner generate \ Then, you can use the OpenAI client as follows: -[examples/online_serving/prompt_embed_inference_with_openai_client.py](../../examples/online_serving/prompt_embed_inference_with_openai_client.py) +[examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py](../../examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py) diff --git a/docs/features/speculative_decoding/README.md b/docs/features/speculative_decoding/README.md index 25cda8059b24..bef71a4f5a37 100644 --- a/docs/features/speculative_decoding/README.md +++ b/docs/features/speculative_decoding/README.md @@ -32,7 +32,7 @@ depend on your model family, traffic pattern, hardware, and sampling settings. | Suffix decoding | Low to medium gain | Medium gain | No extra draft model; dynamic speculation depth. | For reproducible measurements in your environment, use -[`examples/offline_inference/spec_decode.py`](../../../examples/offline_inference/spec_decode.py) +[`examples/features/speculative_decoding/spec_decode_offline.py`](../../../examples/features/speculative_decoding/spec_decode_offline.py) or the [benchmark CLI guide](../../benchmarking/cli.md). ## `--speculative-config` schema diff --git a/docs/features/speculative_decoding/eagle.md b/docs/features/speculative_decoding/eagle.md index 3e0f3add416e..cc9e4fd4c0c1 100644 --- a/docs/features/speculative_decoding/eagle.md +++ b/docs/features/speculative_decoding/eagle.md @@ -1,6 +1,6 @@ # EAGLE Draft Models -The following code configures vLLM to use speculative decoding where proposals are generated by an [EAGLE (Extrapolation Algorithm for Greater Language-model Efficiency)](https://arxiv.org/pdf/2401.15077) based draft model. A more detailed example for offline mode, including how to extract request level acceptance rate, can be found in [examples/offline_inference/spec_decode.py](../../../examples/offline_inference/spec_decode.py) +The following code configures vLLM to use speculative decoding where proposals are generated by an [EAGLE (Extrapolation Algorithm for Greater Language-model Efficiency)](https://arxiv.org/pdf/2401.15077) based draft model. A more detailed example for offline mode, including how to extract request level acceptance rate, can be found in [examples/features/speculative_decoding/spec_decode_offline.py](../../../examples/features/speculative_decoding/spec_decode_offline.py) ## Eagle Drafter Example diff --git a/docs/features/structured_outputs.md b/docs/features/structured_outputs.md index 41cf7be89291..fa39f7ae6e48 100644 --- a/docs/features/structured_outputs.md +++ b/docs/features/structured_outputs.md @@ -165,7 +165,7 @@ As an example, we can use to define a specific format of simplified SQL queries: print(completion.choices[0].message.content) ``` -See also: [full example](../examples/online_serving/structured_outputs.md) +See also: [full example](../../examples/features/structured_outputs/README.md) ## Reasoning Outputs @@ -208,7 +208,7 @@ Note that you can use reasoning with any provided structured outputs feature. Th print("content: ", completion.choices[0].message.content) ``` -See also: [full example](../examples/online_serving/structured_outputs.md) +See also: [full example](../../examples/features/structured_outputs/README.md) !!! note When using Qwen3 Coder models with reasoning enabled, structured outputs might become disabled if the reasoning content does not get parsed into the `reasoning` field separately (v0.11.2+). @@ -304,7 +304,7 @@ Step #2: explanation="Next, let's isolate 'x' by dividing both sides of the equa Answer: x = -29/8 ``` -An example of using `structural_tag` can be found here: [examples/online_serving/structured_outputs](../../examples/online_serving/structured_outputs) +An example of using `structural_tag` can be found here: [examples/features/structured_outputs](../../examples/features/structured_outputs/README.md) ## Offline Inference @@ -339,4 +339,4 @@ shown below: print(outputs[0].outputs[0].text) ``` -See also: [full example](../examples/online_serving/structured_outputs.md) +See also: [full example](../../examples/features/structured_outputs/structured_outputs_offline.py) diff --git a/docs/models/extensions/runai_model_streamer.md b/docs/models/extensions/runai_model_streamer.md index 38c603b46e10..965b2932ffaa 100644 --- a/docs/models/extensions/runai_model_streamer.md +++ b/docs/models/extensions/runai_model_streamer.md @@ -101,7 +101,7 @@ vllm serve /path/to/sharded/model \ --model-loader-extra-config '{"pattern":"custom-model-rank-{rank}-part-{part}.safetensors"}' ``` -To create sharded model files, you can use the script provided in [examples/offline_inference/save_sharded_state.py](../../../examples/offline_inference/save_sharded_state.py). This script demonstrates how to save a model in the sharded format that is compatible with the Run:ai Model Streamer sharded loader. +To create sharded model files, you can use the script provided in [examples/features/sharded_state/save_sharded_state_offline.py](../../../examples/features/sharded_state/save_sharded_state_offline.py). This script demonstrates how to save a model in the sharded format that is compatible with the Run:ai Model Streamer sharded loader. The sharded loader supports all the same tunable parameters as the regular Run:ai Model Streamer, including `concurrency` and `memory_limit`. These can be configured in the same way: diff --git a/docs/serving/data_parallel_deployment.md b/docs/serving/data_parallel_deployment.md index f0946eaf407a..7b963b99d565 100644 --- a/docs/serving/data_parallel_deployment.md +++ b/docs/serving/data_parallel_deployment.md @@ -16,7 +16,7 @@ For MoE models, when any requests are in progress in any rank, we must ensure th In all cases, it is beneficial to load-balance requests between DP ranks. For online deployments, this balancing can be optimized by taking into account the state of each DP engine - in particular its currently scheduled and waiting (queued) requests, and KV cache state. Each DP engine has an independent KV cache, and the benefit of prefix caching can be maximized by directing prompts intelligently. -This document focuses on online deployments (with the API server). DP + EP is also supported for offline usage (via the LLM class), for an example see [examples/offline_inference/data_parallel.py](../../examples/offline_inference/data_parallel.py). +This document focuses on online deployments (with the API server). DP + EP is also supported for offline usage (via the LLM class), for an example see [examples/features/data_parallel/data_parallel_offline.py](../../examples/features/data_parallel/data_parallel_offline.py). There are two distinct modes supported for online deployments - self-contained with internal load balancing, or externally per-rank process deployment and load balancing. diff --git a/docs/usage/reproducibility.md b/docs/usage/reproducibility.md index a8e49d0a3398..680791bbe24a 100644 --- a/docs/usage/reproducibility.md +++ b/docs/usage/reproducibility.md @@ -7,7 +7,7 @@ reproducible results: or enable [batch invariance](../features/batch_invariance.md) to make the outputs insensitive to scheduling. - In online mode, you can only enable [batch invariance](../features/batch_invariance.md). -Example: [examples/offline_inference/reproducibility.py](../../examples/offline_inference/reproducibility.py) +Example: [examples/features/batch_invariance/reproducibility_offline.py](../../examples/features/batch_invariance/reproducibility_offline.py) !!! warning diff --git a/examples/offline_inference/automatic_prefix_caching.py b/examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py similarity index 98% rename from examples/offline_inference/automatic_prefix_caching.py rename to examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py index 2d3c28d9dd4f..801b4b769792 100644 --- a/examples/offline_inference/automatic_prefix_caching.py +++ b/examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py @@ -15,7 +15,7 @@ but ask different questions. Run: -python examples/offline_inference/automatic_prefix_caching.py +python examples/features/automatic_prefix_caching/automatic_prefix_caching_offline.py """ import time diff --git a/examples/offline_inference/prefix_caching.py b/examples/features/automatic_prefix_caching/prefix_caching_offline.py similarity index 100% rename from examples/offline_inference/prefix_caching.py rename to examples/features/automatic_prefix_caching/prefix_caching_offline.py diff --git a/examples/offline_inference/reproducibility.py b/examples/features/batch_invariance/reproducibility_offline.py similarity index 100% rename from examples/offline_inference/reproducibility.py rename to examples/features/batch_invariance/reproducibility_offline.py diff --git a/examples/offline_inference/context_extension.py b/examples/features/context_extension/context_extension_offline.py similarity index 96% rename from examples/offline_inference/context_extension.py rename to examples/features/context_extension/context_extension_offline.py index fae8590f914e..3874288b5e11 100644 --- a/examples/offline_inference/context_extension.py +++ b/examples/features/context_extension/context_extension_offline.py @@ -6,7 +6,7 @@ and run a simple chat example. Usage: - python examples/offline_inference/context_extension.py + python examples/features/context_extension/context_extension_offline.py """ from vllm import LLM, RequestOutput, SamplingParams diff --git a/examples/offline_inference/data_parallel.py b/examples/features/data_parallel/data_parallel_offline.py similarity index 96% rename from examples/offline_inference/data_parallel.py rename to examples/features/data_parallel/data_parallel_offline.py index 287409fa2b5c..c38ff7297afc 100644 --- a/examples/offline_inference/data_parallel.py +++ b/examples/features/data_parallel/data_parallel_offline.py @@ -3,14 +3,14 @@ """ Usage: Single node: - python examples/offline_inference/data_parallel.py \ + python examples/features/data_parallel/data_parallel_offline.py \ --model="ibm-research/PowerMoE-3b" \ -dp=2 \ -tp=2 Multi-node: Node 0 (assume the node has ip of 10.99.48.128): - python examples/offline_inference/data_parallel.py \ + python examples/features/data_parallel/data_parallel_offline.py \ --model="ibm-research/PowerMoE-3b" \ -dp=2 \ -tp=2 \ @@ -19,7 +19,7 @@ --dp-master-addr=10.99.48.128 \ --dp-master-port=13345 Node 1: - python examples/offline_inference/data_parallel.py \ + python examples/features/data_parallel/data_parallel_offline.py \ --model="ibm-research/PowerMoE-3b" \ -dp=2 \ -tp=2 \ diff --git a/examples/online_serving/multi_instance_data_parallel.py b/examples/features/data_parallel/multi_instance_data_parallel.py similarity index 97% rename from examples/online_serving/multi_instance_data_parallel.py rename to examples/features/data_parallel/multi_instance_data_parallel.py index 04d21e048940..66fcd3d24644 100644 --- a/examples/online_serving/multi_instance_data_parallel.py +++ b/examples/features/data_parallel/multi_instance_data_parallel.py @@ -12,7 +12,7 @@ """ To run this example, run the following commands simultaneously with different CUDA_VISIBLE_DEVICES: - python examples/online_serving/multi_instance_data_parallel.py + python examples/features/data_parallel/multi_instance_data_parallel.py vllm serve ibm-research/PowerMoE-3b -dp 2 -dpr 1 \ --data-parallel-address 127.0.0.1 --data-parallel-rpc-port 62300 \ diff --git a/examples/online_serving/kv_events_subscriber.py b/examples/features/kv_events/kv_events_subscriber.py similarity index 100% rename from examples/online_serving/kv_events_subscriber.py rename to examples/features/kv_events/kv_events_subscriber.py diff --git a/examples/offline_inference/logits_processor/README.md b/examples/features/logits_processor/README.md similarity index 90% rename from examples/offline_inference/logits_processor/README.md rename to examples/features/logits_processor/README.md index 6b6e16942f85..07ca07dc71ed 100644 --- a/examples/offline_inference/logits_processor/README.md +++ b/examples/features/logits_processor/README.md @@ -9,7 +9,7 @@ This directory contains examples demonstrating how to use custom logits processo Demonstrates how to instantiate vLLM with a custom logits processor class that operates at the batch level. The example uses a `DummyLogitsProcessor` that masks out all tokens except a specified `target_token` when passed via `SamplingParams.extra_args`. ```bash -python examples/offline_inference/logits_processor/custom.py +python examples/features/logits_processor/custom.py ``` ### `custom_req.py` — Request-level logits processor wrapper @@ -17,7 +17,7 @@ python examples/offline_inference/logits_processor/custom.py Shows how to wrap a request-level logits processor (which operates on individual requests) to be compatible with vLLM's batch-level logits processing interface. ```bash -python examples/offline_inference/logits_processor/custom_req.py +python examples/features/logits_processor/custom_req.py ``` ### `custom_req_init.py` — Request-level processor with engine config @@ -25,7 +25,7 @@ python examples/offline_inference/logits_processor/custom_req.py A special case of wrapping a request-level logits processor where the processor needs access to engine configuration or model metadata during initialization (e.g., vocabulary size, tokenizer info). ```bash -python examples/offline_inference/logits_processor/custom_req_init.py +python examples/features/logits_processor/custom_req_init.py ``` ## Key Concepts diff --git a/examples/offline_inference/logits_processor/custom.py b/examples/features/logits_processor/custom.py similarity index 100% rename from examples/offline_inference/logits_processor/custom.py rename to examples/features/logits_processor/custom.py diff --git a/examples/offline_inference/logits_processor/custom_req.py b/examples/features/logits_processor/custom_req.py similarity index 100% rename from examples/offline_inference/logits_processor/custom_req.py rename to examples/features/logits_processor/custom_req.py diff --git a/examples/offline_inference/logits_processor/custom_req_init.py b/examples/features/logits_processor/custom_req_init.py similarity index 100% rename from examples/offline_inference/logits_processor/custom_req_init.py rename to examples/features/logits_processor/custom_req_init.py diff --git a/examples/offline_inference/lora_with_quantization_inference.py b/examples/features/lora/lora_with_quantization_offline.py similarity index 100% rename from examples/offline_inference/lora_with_quantization_inference.py rename to examples/features/lora/lora_with_quantization_offline.py diff --git a/examples/offline_inference/multilora_inference.py b/examples/features/lora/multilora_offline.py similarity index 100% rename from examples/offline_inference/multilora_inference.py rename to examples/features/lora/multilora_offline.py diff --git a/examples/offline_inference/openai_batch/README.md b/examples/features/openai_batch/README.md similarity index 94% rename from examples/offline_inference/openai_batch/README.md rename to examples/features/openai_batch/README.md index ef4e438d6b72..a9bd31691210 100644 --- a/examples/offline_inference/openai_batch/README.md +++ b/examples/features/openai_batch/README.md @@ -8,7 +8,7 @@ This is a guide to performing batch inference using the OpenAI batch file format The OpenAI batch file format consists of a series of json objects on new lines. -[See here for an example file.](https://github.com/vllm-project/vllm/blob/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl) +[See here for an example file.](https://github.com/vllm-project/vllm/blob/main/examples/features/openai_batch/openai_example_batch.jsonl) Each line represents a separate request. See the [OpenAI package reference](https://platform.openai.com/docs/api-reference/batch/requestInput) for more details. @@ -30,13 +30,13 @@ We currently support `/v1/chat/completions`, `/v1/embeddings`, and `/v1/score` e To follow along with this example, you can download the example batch, or create your own batch file in your working directory. ```bash -wget https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl +wget https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl ``` Once you've created your batch file it should look like this ```bash -cat offline_inference/openai_batch/openai_example_batch.jsonl +cat features/openai_batch/openai_example_batch.jsonl {"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "system", "content": "You are a helpful assistant."},{"role": "user", "content": "Hello world!"}],"max_completion_tokens": 1000}} {"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "system", "content": "You are an unhelpful assistant."},{"role": "user", "content": "Hello world!"}],"max_completion_tokens": 1000}} ``` @@ -49,7 +49,7 @@ You can run the batch with the following command, which will write its results t ```bash python -m vllm.entrypoints.openai.run_batch \ - -i offline_inference/openai_batch/openai_example_batch.jsonl \ + -i features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` @@ -58,7 +58,7 @@ or use command-line: ```bash vllm run-batch \ - -i offline_inference/openai_batch/openai_example_batch.jsonl \ + -i features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` @@ -77,11 +77,11 @@ cat results.jsonl The batch runner supports remote input and output urls that are accessible via http/https. -For example, to run against our example input file located at `https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl`, you can run +For example, to run against our example input file located at `https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl`, you can run ```bash python -m vllm.entrypoints.openai.run_batch \ - -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl \ + -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` @@ -90,7 +90,7 @@ or use command-line: ```bash vllm run-batch \ - -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl \ + -i https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl \ -o results.jsonl \ --model meta-llama/Meta-Llama-3-8B-Instruct ``` @@ -113,13 +113,13 @@ To integrate with cloud blob storage, we recommend using presigned urls. To follow along with this example, you can download the example batch, or create your own batch file in your working directory. ```bash -wget https://raw.githubusercontent.com/vllm-project/vllm/main/examples/offline_inference/openai_batch/openai_example_batch.jsonl +wget https://raw.githubusercontent.com/vllm-project/vllm/main/examples/features/openai_batch/openai_example_batch.jsonl ``` Once you've created your batch file it should look like this ```bash -cat offline_inference/openai_batch/openai_example_batch.jsonl +cat features/openai_batch/openai_example_batch.jsonl {"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "system", "content": "You are a helpful assistant."},{"role": "user", "content": "Hello world!"}],"max_completion_tokens": 1000}} {"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "meta-llama/Meta-Llama-3-8B-Instruct", "messages": [{"role": "system", "content": "You are an unhelpful assistant."},{"role": "user", "content": "Hello world!"}],"max_completion_tokens": 1000}} ``` @@ -127,7 +127,7 @@ cat offline_inference/openai_batch/openai_example_batch.jsonl Now upload your batch file to your S3 bucket. ```bash -aws s3 cp offline_inference/openai_batch/openai_example_batch.jsonl s3://MY_BUCKET/MY_INPUT_FILE.jsonl +aws s3 cp features/openai_batch/openai_example_batch.jsonl s3://MY_BUCKET/MY_INPUT_FILE.jsonl ``` ### Step 2: Generate your presigned urls diff --git a/examples/offline_inference/openai_batch/openai_example_batch.jsonl b/examples/features/openai_batch/openai_example_batch.jsonl similarity index 100% rename from examples/offline_inference/openai_batch/openai_example_batch.jsonl rename to examples/features/openai_batch/openai_example_batch.jsonl diff --git a/examples/online_serving/data_parallel_pause_resume.py b/examples/features/pause_resume/data_parallel_pause_resume.py similarity index 96% rename from examples/online_serving/data_parallel_pause_resume.py rename to examples/features/pause_resume/data_parallel_pause_resume.py index e94de22a1271..1f11536e5366 100644 --- a/examples/online_serving/data_parallel_pause_resume.py +++ b/examples/features/pause_resume/data_parallel_pause_resume.py @@ -1,135 +1,135 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -""" -Test pause/resume with Data Parallel (DP) via HTTP API. - -This example demonstrates coordinated pause/resume across multiple DP ranks. -The pause synchronizes across all DP engines via all-reduce. - -Prerequisites: - Start a vLLM server with data parallelism: - - $ VLLM_SERVER_DEV_MODE=1 vllm serve facebook/opt-125m \ - --enforce-eager \ - --data-parallel-size 4 \ - --tensor-parallel-size 1 - - Then run this script: - - $ python data_parallel_pause_resume.py - -The test verifies pause works by: -1. Starting a streaming generation request -2. Pausing the server mid-generation -3. Sleeping for PAUSE_DURATION seconds -4. Resuming the server -5. Verifying there was a gap in token generation matching the pause duration -""" - -import argparse -import threading -import time - -import requests -from openai import OpenAI - -BASE_URL = "http://localhost:8000" -MODEL_NAME = "facebook/opt-125m" -PAUSE_DURATION = 3.0 - - -def pause_generation(base_url: str, mode: str = "keep") -> None: - """Pause generation via HTTP endpoint.""" - url = f"{base_url}/pause" - response = requests.post(url, params={"mode": mode}, timeout=60) - response.raise_for_status() - print("Server paused") - - -def resume_generation(base_url: str) -> None: - """Resume generation via HTTP endpoint.""" - url = f"{base_url}/resume" - response = requests.post(url, timeout=60) - response.raise_for_status() - print("Server resumed") - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--base-url", default=BASE_URL) - parser.add_argument("--model", default=MODEL_NAME) - args = parser.parse_args() - - client = OpenAI( - base_url=f"{args.base_url}/v1", - api_key="EMPTY", - ) - - prompt = "Write a long story about a dragon. Once upon a time" - token_times: list[float] = [] - pause_token_idx = 0 - pause_triggered = threading.Event() - - def generator_thread(): - """Stream tokens and record timestamps.""" - stream = client.completions.create( - model=args.model, - prompt=prompt, - max_tokens=50, - stream=True, - ) - for chunk in stream: - if chunk.choices[0].text: - token_times.append(time.monotonic()) - token_count = len(token_times) - print(f"Token {token_count}: {chunk.choices[0].text!r}") - - # Signal controller after some tokens - if token_count >= 5 and not pause_triggered.is_set(): - pause_triggered.set() - - def controller_thread(): - """Pause and resume the server.""" - nonlocal pause_token_idx - - # Wait for some tokens - pause_triggered.wait() - - print(f"\nPausing server (keep mode) at token {len(token_times)}...") - pause_generation(args.base_url, mode="keep") - pause_token_idx = len(token_times) - print(f"Sleeping for {PAUSE_DURATION}s...") - - time.sleep(PAUSE_DURATION) - - print("Resuming server...") - resume_generation(args.base_url) - print("Resumed!\n") - - # Run both threads - gen_thread = threading.Thread(target=generator_thread) - ctrl_thread = threading.Thread(target=controller_thread) - - gen_thread.start() - ctrl_thread.start() - - gen_thread.join() - ctrl_thread.join() - - # Check gap at the pause point - if pause_token_idx < len(token_times): - pause_gap = token_times[pause_token_idx] - token_times[pause_token_idx - 1] - print( - f"\nGap after pause (token {pause_token_idx} -> " - f"{pause_token_idx + 1}): {pause_gap:.3f}s" - ) - if pause_gap >= PAUSE_DURATION * 0.9: - print("Test passed! Pause synchronized across DP ranks.") - else: - print(f"Test failed! Expected ~{PAUSE_DURATION}s gap, got {pause_gap:.3f}s") - else: - print("Test failed! No tokens were generated after resuming.") - - -if __name__ == "__main__": - main() +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +Test pause/resume with Data Parallel (DP) via HTTP API. + +This example demonstrates coordinated pause/resume across multiple DP ranks. +The pause synchronizes across all DP engines via all-reduce. + +Prerequisites: + Start a vLLM server with data parallelism: + + $ VLLM_SERVER_DEV_MODE=1 vllm serve facebook/opt-125m \ + --enforce-eager \ + --data-parallel-size 4 \ + --tensor-parallel-size 1 + + Then run this script: + + $ python data_parallel_pause_resume.py + +The test verifies pause works by: +1. Starting a streaming generation request +2. Pausing the server mid-generation +3. Sleeping for PAUSE_DURATION seconds +4. Resuming the server +5. Verifying there was a gap in token generation matching the pause duration +""" + +import argparse +import threading +import time + +import requests +from openai import OpenAI + +BASE_URL = "http://localhost:8000" +MODEL_NAME = "facebook/opt-125m" +PAUSE_DURATION = 3.0 + + +def pause_generation(base_url: str, mode: str = "keep") -> None: + """Pause generation via HTTP endpoint.""" + url = f"{base_url}/pause" + response = requests.post(url, params={"mode": mode}, timeout=60) + response.raise_for_status() + print("Server paused") + + +def resume_generation(base_url: str) -> None: + """Resume generation via HTTP endpoint.""" + url = f"{base_url}/resume" + response = requests.post(url, timeout=60) + response.raise_for_status() + print("Server resumed") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--base-url", default=BASE_URL) + parser.add_argument("--model", default=MODEL_NAME) + args = parser.parse_args() + + client = OpenAI( + base_url=f"{args.base_url}/v1", + api_key="EMPTY", + ) + + prompt = "Write a long story about a dragon. Once upon a time" + token_times: list[float] = [] + pause_token_idx = 0 + pause_triggered = threading.Event() + + def generator_thread(): + """Stream tokens and record timestamps.""" + stream = client.completions.create( + model=args.model, + prompt=prompt, + max_tokens=50, + stream=True, + ) + for chunk in stream: + if chunk.choices[0].text: + token_times.append(time.monotonic()) + token_count = len(token_times) + print(f"Token {token_count}: {chunk.choices[0].text!r}") + + # Signal controller after some tokens + if token_count >= 5 and not pause_triggered.is_set(): + pause_triggered.set() + + def controller_thread(): + """Pause and resume the server.""" + nonlocal pause_token_idx + + # Wait for some tokens + pause_triggered.wait() + + print(f"\nPausing server (keep mode) at token {len(token_times)}...") + pause_generation(args.base_url, mode="keep") + pause_token_idx = len(token_times) + print(f"Sleeping for {PAUSE_DURATION}s...") + + time.sleep(PAUSE_DURATION) + + print("Resuming server...") + resume_generation(args.base_url) + print("Resumed!\n") + + # Run both threads + gen_thread = threading.Thread(target=generator_thread) + ctrl_thread = threading.Thread(target=controller_thread) + + gen_thread.start() + ctrl_thread.start() + + gen_thread.join() + ctrl_thread.join() + + # Check gap at the pause point + if pause_token_idx < len(token_times): + pause_gap = token_times[pause_token_idx] - token_times[pause_token_idx - 1] + print( + f"\nGap after pause (token {pause_token_idx} -> " + f"{pause_token_idx + 1}): {pause_gap:.3f}s" + ) + if pause_gap >= PAUSE_DURATION * 0.9: + print("Test passed! Pause synchronized across DP ranks.") + else: + print(f"Test failed! Expected ~{PAUSE_DURATION}s gap, got {pause_gap:.3f}s") + else: + print("Test failed! No tokens were generated after resuming.") + + +if __name__ == "__main__": + main() diff --git a/examples/offline_inference/pause_resume.py b/examples/features/pause_resume/pause_resume_offline.py similarity index 100% rename from examples/offline_inference/pause_resume.py rename to examples/features/pause_resume/pause_resume_offline.py diff --git a/examples/offline_inference/run_one_batch.py b/examples/features/profiling/run_one_batch_offline.py similarity index 100% rename from examples/offline_inference/run_one_batch.py rename to examples/features/profiling/run_one_batch_offline.py diff --git a/examples/offline_inference/simple_profiling.py b/examples/features/profiling/simple_profiling_offline.py similarity index 100% rename from examples/offline_inference/simple_profiling.py rename to examples/features/profiling/simple_profiling_offline.py diff --git a/examples/online_serving/prompt_embed_inference_with_openai_client.py b/examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py similarity index 96% rename from examples/online_serving/prompt_embed_inference_with_openai_client.py rename to examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py index fa4b64c00703..40eae0c062dd 100644 --- a/examples/online_serving/prompt_embed_inference_with_openai_client.py +++ b/examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py @@ -15,7 +15,7 @@ --enable-prompt-embeds Run the client: -python examples/online_serving/prompt_embed_inference_with_openai_client.py +python examples/features/prompt_embed/prompt_embed_inference_with_openai_client.py Model: meta-llama/Llama-3.2-1B-Instruct Note: This model is gated on Hugging Face Hub. diff --git a/examples/offline_inference/prompt_embed_inference.py b/examples/features/prompt_embed/prompt_embed_offline.py similarity index 97% rename from examples/offline_inference/prompt_embed_inference.py rename to examples/features/prompt_embed/prompt_embed_offline.py index a0eaeb6810a2..29853bce9673 100644 --- a/examples/offline_inference/prompt_embed_inference.py +++ b/examples/features/prompt_embed/prompt_embed_offline.py @@ -15,7 +15,7 @@ - transformers Run: - python examples/offline_inference/prompt_embed_inference.py + python examples/features/prompt_embed/prompt_embed_offline.py """ import torch diff --git a/examples/offline_inference/llm_engine_reset_kv.py b/examples/features/reset_kv/reset_kv_offline.py similarity index 100% rename from examples/offline_inference/llm_engine_reset_kv.py rename to examples/features/reset_kv/reset_kv_offline.py diff --git a/examples/offline_inference/load_sharded_state.py b/examples/features/sharded_state/load_sharded_state_offline.py similarity index 94% rename from examples/offline_inference/load_sharded_state.py rename to examples/features/sharded_state/load_sharded_state_offline.py index 0085e8e8e32b..e867db5d12fe 100644 --- a/examples/offline_inference/load_sharded_state.py +++ b/examples/features/sharded_state/load_sharded_state_offline.py @@ -3,16 +3,16 @@ """ Validates the loading of a model saved with the sharded_state format. This script demonstrates how to load a model that was previously saved -using save_sharded_state.py and validates it by running inference. +using save_sharded_state_offline.py and validates it by running inference. Example usage: (First need to save a sharded_state mode) -python save_sharded_state.py \ +python save_sharded_state_offline.py \ --model /path/to/load \ --tensor-parallel-size 8 \ --output /path/to/save/sharded/model -python load_sharded_state.py \ +python load_sharded_state_offline.py \ --model /path/to/saved/sharded/model \ --load-format sharded_state \ --tensor-parallel-size 8 \ diff --git a/examples/offline_inference/save_sharded_state.py b/examples/features/sharded_state/save_sharded_state_offline.py similarity index 98% rename from examples/offline_inference/save_sharded_state.py rename to examples/features/sharded_state/save_sharded_state_offline.py index 14d472ee3f23..675f2e35a53f 100644 --- a/examples/offline_inference/save_sharded_state.py +++ b/examples/features/sharded_state/save_sharded_state_offline.py @@ -7,7 +7,7 @@ Example usage: -python save_sharded_state.py \ +python save_sharded_state_offline.py \ --model /path/to/load \ --tensor-parallel-size 8 \ --output /path/to/save diff --git a/examples/offline_inference/extract_hidden_states.py b/examples/features/speculative_decoding/extract_hidden_states_offline.py similarity index 100% rename from examples/offline_inference/extract_hidden_states.py rename to examples/features/speculative_decoding/extract_hidden_states_offline.py diff --git a/examples/offline_inference/mlpspeculator.py b/examples/features/speculative_decoding/mlpspeculator_offline.py similarity index 100% rename from examples/offline_inference/mlpspeculator.py rename to examples/features/speculative_decoding/mlpspeculator_offline.py diff --git a/examples/offline_inference/spec_decode.py b/examples/features/speculative_decoding/spec_decode_offline.py similarity index 100% rename from examples/offline_inference/spec_decode.py rename to examples/features/speculative_decoding/spec_decode_offline.py diff --git a/examples/online_serving/structured_outputs/README.md b/examples/features/structured_outputs/README.md similarity index 85% rename from examples/online_serving/structured_outputs/README.md rename to examples/features/structured_outputs/README.md index 7f539716ecf8..f2863eb0cbcf 100644 --- a/examples/online_serving/structured_outputs/README.md +++ b/examples/features/structured_outputs/README.md @@ -20,7 +20,7 @@ vllm serve deepseek-ai/DeepSeek-R1-Distill-Qwen-7B \ If you want to run this script standalone with `uv`, you can use the following: ```bash -uvx --from git+https://github.com/vllm-project/vllm#subdirectory=examples/online_serving/structured_outputs \ +uvx --from git+https://github.com/vllm-project/vllm#subdirectory=examples/features/structured_outputs \ structured-outputs ``` @@ -34,19 +34,19 @@ See [feature docs](https://docs.vllm.ai/en/latest/features/structured_outputs.ht Run all constraints, non-streaming: ```bash -uv run structured_outputs.py +uv run structured_outputs_offline.py ``` Run all constraints, streaming: ```bash -uv run structured_outputs.py --stream +uv run structured_outputs_offline.py --stream ``` Run certain constraints, for example `structural_tag` and `regex`, streaming: ```bash -uv run structured_outputs.py \ +uv run structured_outputs_offline.py \ --constraint structural_tag regex \ --stream ``` @@ -54,5 +54,5 @@ uv run structured_outputs.py \ Run all constraints, with reasoning models and streaming: ```bash -uv run structured_outputs.py --reasoning --stream +uv run structured_outputs_offline.py --reasoning --stream ``` diff --git a/examples/online_serving/structured_outputs/pyproject.toml b/examples/features/structured_outputs/pyproject.toml similarity index 100% rename from examples/online_serving/structured_outputs/pyproject.toml rename to examples/features/structured_outputs/pyproject.toml diff --git a/examples/online_serving/structured_outputs/structured_outputs.py b/examples/features/structured_outputs/structured_outputs_client.py similarity index 100% rename from examples/online_serving/structured_outputs/structured_outputs.py rename to examples/features/structured_outputs/structured_outputs_client.py diff --git a/examples/offline_inference/structured_outputs.py b/examples/features/structured_outputs/structured_outputs_offline.py similarity index 100% rename from examples/offline_inference/structured_outputs.py rename to examples/features/structured_outputs/structured_outputs_offline.py diff --git a/examples/offline_inference/torchrun_dp_example.py b/examples/features/torchrun/torchrun_dp_example_offline.py similarity index 95% rename from examples/offline_inference/torchrun_dp_example.py rename to examples/features/torchrun/torchrun_dp_example_offline.py index eb7ed969ea4b..f18f6042e9c6 100644 --- a/examples/offline_inference/torchrun_dp_example.py +++ b/examples/features/torchrun/torchrun_dp_example_offline.py @@ -7,15 +7,15 @@ To run this example: ```bash -$ torchrun --nproc-per-node=2 examples/offline_inference/torchrun_dp_example.py +$ torchrun --nproc-per-node=2 examples/features/torchrun/torchrun_dp_example_offline.py ``` With custom parallelism settings: ```bash -$ torchrun --nproc-per-node=8 examples/offline_inference/torchrun_dp_example.py \ +$ torchrun --nproc-per-node=8 examples/features/torchrun/torchrun_dp_example_offline.py \ --tp-size=2 --pp-size=1 --dp-size=4 --enable-ep ``` -""" +""" # noqa: E501 import argparse diff --git a/examples/offline_inference/torchrun_example.py b/examples/features/torchrun/torchrun_example_offline.py similarity index 99% rename from examples/offline_inference/torchrun_example.py rename to examples/features/torchrun/torchrun_example_offline.py index 2960d329968a..e41bcd420c20 100644 --- a/examples/offline_inference/torchrun_example.py +++ b/examples/features/torchrun/torchrun_example_offline.py @@ -4,7 +4,7 @@ experimental support for tensor-parallel inference with torchrun, see https://github.com/vllm-project/vllm/issues/11400 for the motivation and use case for this example. -run the script with `torchrun --nproc-per-node=4 torchrun_example.py`, +run the script with `torchrun --nproc-per-node=4 torchrun_example_offline.py`, the argument `4` should match the product of `tensor_parallel_size` and `pipeline_parallel_size` below. see `tests/distributed/test_torchrun_example.py` for the unit test. diff --git a/examples/offline_inference/routed_experts_e2e.py b/examples/rl/routed_experts_e2e.py similarity index 99% rename from examples/offline_inference/routed_experts_e2e.py rename to examples/rl/routed_experts_e2e.py index bb1d7b411f99..1666bc3ffe16 100644 --- a/examples/offline_inference/routed_experts_e2e.py +++ b/examples/rl/routed_experts_e2e.py @@ -9,7 +9,7 @@ 3. Results are deterministic across runs (baseline vs reference). Usage: - python examples/offline_inference/routed_experts_e2e.py \ + python examples/rl/routed_experts_e2e.py \ --model Qwen/Qwen3-30B-A3B \ --tp 4 \ --max-model-len 4096 \ diff --git a/examples/offline_inference/skip_loading_weights_in_engine_init.py b/examples/rl/skip_loading_weights_in_engine_init.py similarity index 100% rename from examples/offline_inference/skip_loading_weights_in_engine_init.py rename to examples/rl/skip_loading_weights_in_engine_init.py diff --git a/tests/distributed/test_torchrun_example.py b/tests/distributed/test_torchrun_example.py index f56d037fa547..e72f00bc91e0 100644 --- a/tests/distributed/test_torchrun_example.py +++ b/tests/distributed/test_torchrun_example.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# unit test for `examples/offline_inference/torchrun_example.py` +# unit test for `examples/features/torchrun/torchrun_example_offline.py` import os import random diff --git a/tests/distributed/test_torchrun_example_moe.py b/tests/distributed/test_torchrun_example_moe.py index 8c1d00561b16..969b5e92e3fc 100644 --- a/tests/distributed/test_torchrun_example_moe.py +++ b/tests/distributed/test_torchrun_example_moe.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -# unit test for `examples/offline_inference/torchrun_example.py` +# unit test for `examples/features/torchrun/torchrun_example_offline.py` import os import random diff --git a/tests/v1/spec_decode/test_acceptance_length.py b/tests/v1/spec_decode/test_acceptance_length.py index ec65e20cbde1..62ff100fdbf8 100644 --- a/tests/v1/spec_decode/test_acceptance_length.py +++ b/tests/v1/spec_decode/test_acceptance_length.py @@ -43,7 +43,8 @@ class Eagle3ModelConfig: # Model configurations for EAGLE3 acceptance length tests. # Expected acceptance lengths are determined by running baseline benchmarks -# using examples/offline_inference/spec_decode.py with the MT-Bench dataset. +# using examples/features/speculative_decoding/spec_decode_offline.py +# with the MT-Bench dataset. EAGLE3_MODEL_CONFIGS = [ Eagle3ModelConfig( verifier="meta-llama/Llama-3.1-8B-Instruct", diff --git a/vllm/entrypoints/llm.py b/vllm/entrypoints/llm.py index 29cc2b47e7be..e67046b14117 100644 --- a/vllm/entrypoints/llm.py +++ b/vllm/entrypoints/llm.py @@ -334,7 +334,7 @@ def _make_config(value: Any, cls: type[_R]) -> _R: f"LLM(data_parallel_size={_dp_size}) is not supported for single-" "process usage and may hang. Please use " "the explicit multi-process data-parallel example at " - "'examples/offline_inference/data_parallel.py'." + "'examples/features/data_parallel/data_parallel_offline.py'." ) engine_args = EngineArgs( diff --git a/vllm/model_executor/model_loader/sharded_state_loader.py b/vllm/model_executor/model_loader/sharded_state_loader.py index 87b4b72db2a1..3f57fe7e0265 100644 --- a/vllm/model_executor/model_loader/sharded_state_loader.py +++ b/vllm/model_executor/model_loader/sharded_state_loader.py @@ -31,8 +31,8 @@ class ShardedStateLoader(BaseModelLoader): Model loader that directly loads each worker's model state dict, which enables a fast load path for large tensor-parallel models where each worker only needs to read its own shard rather than the entire checkpoint. See - `examples/offline_inference/save_sharded_state.py` for creating a sharded - checkpoint. + `examples/features/sharded_state/save_sharded_state_offline.py` for creating + a sharded checkpoint. """ DEFAULT_PATTERN = "model-rank-{rank}-part-{part}.safetensors" diff --git a/vllm/model_executor/models/config.py b/vllm/model_executor/models/config.py index e8f5101b577d..459c16f8ec97 100644 --- a/vllm/model_executor/models/config.py +++ b/vllm/model_executor/models/config.py @@ -517,7 +517,7 @@ def verify_and_update_model_config(model_config: "ModelConfig") -> None: "Nomic context extension is disabled. " "Changing max_model_len from %s to %s. " "To enable context extension, see: " - "https://github.com/vllm-project/vllm/tree/main/examples/offline_inference/context_extension.py", + "https://github.com/vllm-project/vllm/tree/main/examples/features/context_extension/context_extension_offline.py", max_model_len_before, model_config.max_model_len, ) diff --git a/vllm/v1/engine/utils.py b/vllm/v1/engine/utils.py index 53cad2bc153f..7b0f00d14c8a 100644 --- a/vllm/v1/engine/utils.py +++ b/vllm/v1/engine/utils.py @@ -952,7 +952,7 @@ def get_engine_zmq_addresses( # In offline mode there is an LLM instance per DP rank and # one core engine per LLM, see - # examples/offline_inference/data_parallel.py. + # examples/features/data_parallel/data_parallel_offline.py. offline_mode = local_start_index is not None # client_local_only = True for cases where this front-end diff --git a/vllm/v1/executor/uniproc_executor.py b/vllm/v1/executor/uniproc_executor.py index b616c3b7b8ad..d006946079e7 100644 --- a/vllm/v1/executor/uniproc_executor.py +++ b/vllm/v1/executor/uniproc_executor.py @@ -147,7 +147,7 @@ class ExecutorWithExternalLauncher(UniProcExecutor): offline inference with tensor parallelism. see https://github.com/vllm-project/vllm/issues/11400 for - the motivation, and examples/offline_inference/torchrun_example.py + the motivation, and examples/features/torchrun/torchrun_example_offline.py for the usage example. The key idea: although it is tensor-parallel inference, we only From ea74f701db6c0dd4b2d954f5e79841101d0d8a5d Mon Sep 17 00:00:00 2001 From: zhrrr <43847754+izhuhaoran@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:33:49 +0800 Subject: [PATCH 004/237] Bugfix: fix SpecBench sample argument error (#40927) Signed-off-by: zhuhaoran --- vllm/benchmarks/datasets/datasets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vllm/benchmarks/datasets/datasets.py b/vllm/benchmarks/datasets/datasets.py index 745b5ab2ff8f..419275d2e6ae 100644 --- a/vllm/benchmarks/datasets/datasets.py +++ b/vllm/benchmarks/datasets/datasets.py @@ -2365,6 +2365,7 @@ def load_data(self) -> None: random.shuffle(self.data) def sample( + self, **kwargs, ) -> list[SampleRequest]: # leverage CustomDataset sample From bde0efdbb78a57dc10375e8d0686cf862332192c Mon Sep 17 00:00:00 2001 From: artem-spector Date: Tue, 28 Apr 2026 10:43:30 +0300 Subject: [PATCH 005/237] [Bugfix][Granite4Vision] Fix deepstack buffer causing decode slowdown in compiled mode (#40917) Signed-off-by: artemspector Co-authored-by: artemspector --- vllm/model_executor/models/granite4_vision.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vllm/model_executor/models/granite4_vision.py b/vllm/model_executor/models/granite4_vision.py index 147f02eced97..710fc94ee5f8 100644 --- a/vllm/model_executor/models/granite4_vision.py +++ b/vllm/model_executor/models/granite4_vision.py @@ -887,9 +887,10 @@ def forward( and get_pp_group().is_first_rank and self._ds_layer_indices ): + n = inputs_embeds.size(0) ds: IntermediateTensors | None = IntermediateTensors( { - f"ds_{llm_layer}": self._ds_buffers[lvl] + f"ds_{llm_layer}": self._ds_buffers[lvl][:n] for lvl, llm_layer in enumerate(self._ds_layer_indices) } ) From 9e92de51c61a47e5abb32d99b1930862473741d5 Mon Sep 17 00:00:00 2001 From: Roy Wang Date: Tue, 28 Apr 2026 15:52:54 +0800 Subject: [PATCH 006/237] [Bugfix] Exclude numa_bind fields from ParallelConfig DP hash (#41098) Signed-off-by: yasong --- vllm/config/parallel.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vllm/config/parallel.py b/vllm/config/parallel.py index afd0d1dd501a..4f903eeefa6d 100644 --- a/vllm/config/parallel.py +++ b/vllm/config/parallel.py @@ -713,6 +713,14 @@ def compute_hash(self): "worker_extension_cls", "_api_process_count", "_api_process_rank", + # NUMA binding is per-rank host-side memory locality; it does + # not affect collective-communication semantics. When numa_bind + # is enabled with auto-detection, each DP rank stores its own + # NUMA node in numa_bind_nodes (see vllm/utils/numa_utils.py + # `_get_numa_node`), which would otherwise diverge the DP hash. + "numa_bind", + "numa_bind_nodes", + "numa_bind_cpus", } from vllm.config.utils import get_hash_factors, hash_factors From de3da0b97cd9db8b1d429312992a5759c89ef881 Mon Sep 17 00:00:00 2001 From: zhangxin81 <115389973+zhangxin81@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:38:48 +0800 Subject: [PATCH 007/237] Add tuned triton fused_moe configs on H100 for gpt-oss (#39904) Signed-off-by: zhangxin81 <115389973+zhangxin81@users.noreply.github.com> --- ...880,device_name=NVIDIA_H100_80GB_HBM3.json | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 vllm/model_executor/layers/fused_moe/configs/E=128,N=2880,device_name=NVIDIA_H100_80GB_HBM3.json diff --git a/vllm/model_executor/layers/fused_moe/configs/E=128,N=2880,device_name=NVIDIA_H100_80GB_HBM3.json b/vllm/model_executor/layers/fused_moe/configs/E=128,N=2880,device_name=NVIDIA_H100_80GB_HBM3.json new file mode 100644 index 000000000000..2d53aedbed48 --- /dev/null +++ b/vllm/model_executor/layers/fused_moe/configs/E=128,N=2880,device_name=NVIDIA_H100_80GB_HBM3.json @@ -0,0 +1,147 @@ +{ + "triton_version": "3.6.0", + "1": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 4 + }, + "2": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 3 + }, + "4": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 128, + "GROUP_SIZE_M": 64, + "num_warps": 4, + "num_stages": 4 + }, + "8": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 128, + "GROUP_SIZE_M": 64, + "num_warps": 4, + "num_stages": 4 + }, + "16": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 16, + "num_warps": 4, + "num_stages": 5 + }, + "24": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 16, + "num_warps": 4, + "num_stages": 5 + }, + "32": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 4 + }, + "48": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 16, + "num_warps": 4, + "num_stages": 5 + }, + "64": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 4 + }, + "96": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 5 + }, + "128": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 5 + }, + "256": { + "BLOCK_SIZE_M": 16, + "BLOCK_SIZE_N": 32, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 5 + }, + "512": { + "BLOCK_SIZE_M": 32, + "BLOCK_SIZE_N": 64, + "BLOCK_SIZE_K": 256, + "GROUP_SIZE_M": 1, + "num_warps": 4, + "num_stages": 5 + }, + "1024": { + "BLOCK_SIZE_M": 64, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_K": 128, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 4 + }, + "1536": { + "BLOCK_SIZE_M": 64, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_K": 128, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 4 + }, + "2048": { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 5 + }, + "3072": { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 128, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 5 + }, + "4096": { + "BLOCK_SIZE_M": 128, + "BLOCK_SIZE_N": 256, + "BLOCK_SIZE_K": 64, + "GROUP_SIZE_M": 1, + "num_warps": 8, + "num_stages": 4 + } +} From 5aa371dc8e38e053754d89b444abca0a1d63f676 Mon Sep 17 00:00:00 2001 From: Yongye Zhu Date: Tue, 28 Apr 2026 12:08:55 -0400 Subject: [PATCH 008/237] [DSV4] Enable Multi-stream for Pre-Attn GEMM (#41061) Signed-off-by: Yongye Zhu --- .../layers/deepseek_compressor.py | 9 +- .../layers/deepseek_v4_attention.py | 149 +++++++++++++----- vllm/model_executor/models/deepseek_v4.py | 22 ++- vllm/utils/multi_stream_utils.py | 64 ++++++++ 4 files changed, 187 insertions(+), 57 deletions(-) diff --git a/vllm/model_executor/layers/deepseek_compressor.py b/vllm/model_executor/layers/deepseek_compressor.py index af2783f604da..cae80c35316a 100644 --- a/vllm/model_executor/layers/deepseek_compressor.py +++ b/vllm/model_executor/layers/deepseek_compressor.py @@ -14,7 +14,6 @@ from vllm.model_executor.layers.linear import ( MergedColumnParallelLinear, ) -from vllm.model_executor.layers.utils import cublas_gemm_bf16_bf16_fp32 from vllm.platforms import current_platform from vllm.triton_utils import tl, triton from vllm.v1.attention.backend import ( @@ -271,16 +270,12 @@ def __init__( def forward( self, - # [num_tokens, hidden_size] - x: torch.Tensor, + # [num_tokens, 2 * self.coff * self.head_dim] + kv_score: torch.Tensor, # [num_tokens] positions: torch.Tensor, rotary_emb, ) -> None: - num_tokens, _ = x.shape - # bf16 weights/activations but fp32 output for numerical stability of - # the downstream compressor math. - kv_score = cublas_gemm_bf16_bf16_fp32(x, self.fused_wkv_wgate.weight) # Each of shape [num_tokens, coff * self.head_dim] # input bf16, output are fp32 kv, score = kv_score.split( diff --git a/vllm/model_executor/layers/deepseek_v4_attention.py b/vllm/model_executor/layers/deepseek_v4_attention.py index 43242eddb5b2..a968a06bb650 100644 --- a/vllm/model_executor/layers/deepseek_v4_attention.py +++ b/vllm/model_executor/layers/deepseek_v4_attention.py @@ -4,8 +4,9 @@ DeepseekV4 MLA Attention Layer """ +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import torch import torch.nn as nn @@ -16,6 +17,7 @@ ReplicatedLinear, ) from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer +from vllm.model_executor.layers.utils import cublas_gemm_bf16_bf16_fp32 from vllm.utils.deep_gemm import fp8_einsum from vllm.utils.torch_utils import direct_register_custom_op from vllm.v1.attention.ops.deepseek_v4_ops import ( @@ -51,7 +53,10 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import ( GroupShape, ) -from vllm.utils.multi_stream_utils import maybe_execute_in_parallel +from vllm.utils.multi_stream_utils import ( + execute_in_parallel, + maybe_execute_in_parallel, +) from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata from vllm.v1.attention.backends.mla.flashmla_sparse import ( DeepseekV4FlashMLASparseBackend, @@ -94,7 +99,7 @@ class DeepseekV4MLAModules: indexer: torch.nn.Module | None indexer_rotary_emb: torch.nn.Module topk_indices_buffer: torch.Tensor | None - aux_stream: torch.cuda.Stream | None = None + aux_stream_list: list[torch.cuda.Stream] | None = None # --8<-- [start:multi_head_latent_attention] @@ -217,8 +222,11 @@ def __init__( + 1 # 1B pad ) - self.aux_stream = mla_modules.aux_stream - self.ln_events = [torch.cuda.Event(), torch.cuda.Event()] + self.aux_stream_list = mla_modules.aux_stream_list + # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events; + # [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins + # before post-GEMM starts. + self.ln_events = [torch.cuda.Event() for _ in range(4)] assert cache_config is not None, "DeepseekV4 attention requires cache_config" self.swa_cache_layer = DeepseekV4SWACache( @@ -277,9 +285,6 @@ def forward( hidden_states: torch.Tensor, llama_4_scaling: torch.Tensor | None = None, ) -> torch.Tensor: - qr_kv, _ = self.fused_wqa_wkv(hidden_states) - qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1) - # Pre-allocate attention output with FlashMLA-padded head count. # The op writes into `o_padded`; we slice to n_local_heads after. num_tokens = hidden_states.shape[0] @@ -292,8 +297,6 @@ def forward( # Attention (inside custom op for torch.compile boundary) torch.ops.vllm.deepseek_v4_attention( hidden_states, - qr, - kv, positions, o_padded, self.layer_name, @@ -332,17 +335,71 @@ def forward( return self.wo_b(z.flatten(1)) + def attn_gemm_parallel_execute(self, hidden_states) -> tuple[Any, ...]: + assert self.aux_stream_list is not None + assert len(self.aux_stream_list) >= 3 + + # fused_wqa_wkv (heaviest) on default; the three lighter input GEMMs + # on aux streams 0..2 when their owning module exists. ln_events[0] + # is the fan-out start event; ln_events[1..3] are per-aux done events. + aux_fns: list[Callable[[], Any] | None] = [None, None, None] + + if self.compressor is not None: + # Local ref so the closure keeps a non-None type for mypy. + compressor = self.compressor + + def compressor_kv_score() -> torch.Tensor: + return cublas_gemm_bf16_bf16_fp32( + hidden_states, compressor.fused_wkv_wgate.weight + ) + + aux_fns[0] = compressor_kv_score + + if self.indexer is not None: + indexer = self.indexer + + def indexer_weights_proj() -> torch.Tensor: + # ReplicatedLinear returns (output, bias); bias is None. + weights, _ = indexer.weights_proj(hidden_states) + return weights + + def indexer_compressor_kv_score() -> torch.Tensor: + return cublas_gemm_bf16_bf16_fp32( + hidden_states, indexer.compressor.fused_wkv_wgate.weight + ) + + aux_fns[1] = indexer_weights_proj + aux_fns[2] = indexer_compressor_kv_score + + def fused_wqa_wkv() -> torch.Tensor: + # MergedColumnParallelLinear returns (output, bias); bias is None. + qr_kv, _ = self.fused_wqa_wkv(hidden_states) + return qr_kv + + qr_kv, (kv_score, indexer_weights, indexer_kv_score) = execute_in_parallel( + fused_wqa_wkv, + aux_fns, + self.ln_events[0], + self.ln_events[1:4], + self.aux_stream_list[:3], + ) + + return qr_kv, kv_score, indexer_kv_score, indexer_weights + def attention_impl( self, hidden_states: torch.Tensor, - qr: torch.Tensor, - kv: torch.Tensor, positions: torch.Tensor, out: torch.Tensor, # [num_tokens, padded_heads, head_dim], written in place ) -> None: forward_context = get_forward_context() attn_metadata = forward_context.attn_metadata + qr_kv, kv_score, indexer_kv_score, indexer_weights = ( + self.attn_gemm_parallel_execute(hidden_states) + ) + + qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1) qr, kv = fused_q_kv_rmsnorm( qr, kv, @@ -350,42 +407,60 @@ def attention_impl( self.kv_norm.weight.data, self.eps, ) - q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim) - # Overlap kv_insert with whichever of indexer/compressor is present. - # Indexer implies compressor; when both exist, compressor rides on the - # aux stream alongside kv_insert so the heavy indexer owns default. + # wq_b + kv_insert (+ MLA compressor when an indexer is present) ride + # on the default stream so q stays on its consumer stream (mla_attn + # downstream reads q on default). Indexer/compressor go on aux for + # overlap with default's GEMM + cache write. if self.indexer is not None: + assert self.aux_stream_list is not None + aux_stream = self.aux_stream_list[0] indexer = self.indexer # Local ref so the closure keeps a non-None type for mypy. assert self.compressor is not None compressor = self.compressor - def kv_insert_and_compress() -> None: + def wq_b_kv_insert_and_compress() -> torch.Tensor: + q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim) self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata) - compressor(hidden_states, positions, self.rotary_emb) - - maybe_execute_in_parallel( - lambda: indexer(hidden_states, qr, positions, self.indexer_rotary_emb), - kv_insert_and_compress, + compressor(kv_score, positions, self.rotary_emb) + return q + + q, _ = maybe_execute_in_parallel( + wq_b_kv_insert_and_compress, + lambda: indexer( + hidden_states, + qr, + indexer_kv_score, + indexer_weights, + positions, + self.indexer_rotary_emb, + ), self.ln_events[0], self.ln_events[1], - self.aux_stream, + aux_stream, ) elif self.compressor is not None: - # Compressor on default, kv_insert on aux. + # wq_b + kv_insert on default, compressor on aux. + assert self.aux_stream_list is not None + aux_stream = self.aux_stream_list[0] compressor = self.compressor - maybe_execute_in_parallel( - lambda: compressor(hidden_states, positions, self.rotary_emb), - lambda: self._fused_qnorm_rope_kv_insert( - q, kv, positions, attn_metadata - ), + + def wq_b_kv_insert() -> torch.Tensor: + q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim) + self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata) + return q + + q, _ = maybe_execute_in_parallel( + wq_b_kv_insert, + lambda: compressor(kv_score, positions, self.rotary_emb), self.ln_events[0], self.ln_events[1], - self.aux_stream, + aux_stream, ) else: # SWA-only layer: no compressor, no overlap. + q = self.wq_b(qr).view(-1, self.n_local_heads, self.head_dim) self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata) # Handle dummy run (no metadata). @@ -455,21 +530,17 @@ def _fused_qnorm_rope_kv_insert( def deepseek_v4_attention( hidden_states: torch.Tensor, - qr: torch.Tensor, - kv: torch.Tensor, positions: torch.Tensor, out: torch.Tensor, layer_name: str, ) -> None: forward_context: ForwardContext = get_forward_context() self = forward_context.no_compile_layers[layer_name] - self.attention_impl(hidden_states, qr, kv, positions, out) + self.attention_impl(hidden_states, positions, out) def deepseek_v4_attention_fake( hidden_states: torch.Tensor, - qr: torch.Tensor, - kv: torch.Tensor, positions: torch.Tensor, out: torch.Tensor, layer_name: str, @@ -1057,18 +1128,20 @@ def forward( self, hidden_states: torch.Tensor, qr: torch.Tensor, + compressed_kv_score: torch.Tensor, + indexer_weights: torch.Tensor, positions: torch.Tensor, rotary_emb: nn.Module, ) -> torch.Tensor: + # ReplicatedLinear returns (output, bias); bias is None. q, _ = self.wq_b(qr) q = q.view(-1, self.n_head, self.head_dim) - k = self.compressor(hidden_states, positions, rotary_emb) - weights, _ = self.weights_proj(hidden_states) + k = self.compressor(compressed_kv_score, positions, rotary_emb) q_quant, weights = fused_indexer_q_rope_quant( positions, q, rotary_emb.cos_sin_cache, - weights, + indexer_weights, self.softmax_scale, self.n_head**-0.5, use_fp4=self.use_fp4_kv, diff --git a/vllm/model_executor/models/deepseek_v4.py b/vllm/model_executor/models/deepseek_v4.py index 97f755240a4c..d6edf0789f57 100644 --- a/vllm/model_executor/models/deepseek_v4.py +++ b/vllm/model_executor/models/deepseek_v4.py @@ -54,7 +54,6 @@ from vllm.platforms import current_platform from vllm.sequence import IntermediateTensors from vllm.triton_utils import tl, triton -from vllm.utils.multi_stream_utils import AuxStreamType from vllm.utils.torch_utils import direct_register_custom_op from .utils import ( @@ -926,7 +925,7 @@ def __init__( vllm_config: VllmConfig, prefix: str, topk_indices_buffer: torch.Tensor | None = None, - aux_stream: torch.cuda.Stream | None = None, + aux_stream_list: list[torch.cuda.Stream] | None = None, ): super().__init__() config = vllm_config.model_config.hf_config @@ -1059,7 +1058,7 @@ def __init__( indexer=self.indexer, indexer_rotary_emb=self.rotary_emb, topk_indices_buffer=topk_indices_buffer, - aux_stream=aux_stream, + aux_stream_list=aux_stream_list, ) self.mla_attn = DeepseekV4MultiHeadLatentAttentionWrapper( hidden_size=self.hidden_size, @@ -1095,7 +1094,7 @@ def __init__( vllm_config, prefix, topk_indices_buffer: torch.Tensor | None = None, - aux_stream_dict: dict[AuxStreamType, torch.cuda.Stream] | None = None, + aux_stream_list: list[torch.cuda.Stream] | None = None, ): super().__init__() config = vllm_config.model_config.hf_config @@ -1106,9 +1105,7 @@ def __init__( vllm_config, prefix=f"{prefix}.attn", topk_indices_buffer=topk_indices_buffer, - aux_stream=aux_stream_dict.get(AuxStreamType.Attention) - if aux_stream_dict is not None - else None, + aux_stream_list=aux_stream_list, ) self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn") @@ -1236,10 +1233,11 @@ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): self.hc_dim = self.hc_mult * config.hidden_size self.rms_norm_eps = config.rms_norm_eps - aux_stream_list = [torch.cuda.Stream() for _ in range(1)] - self.aux_stream_dict = { - AuxStreamType.Attention: aux_stream_list[0], - } + # Three aux streams: one per non-default input GEMM in + # DeepseekV4MultiHeadLatentAttentionWrapper.attn_gemm_parallel_execute + # (compressor kv_score, indexer.weights_proj, indexer.compressor + # kv_score). fused_wqa_wkv stays on the default stream. + aux_stream_list = [torch.cuda.Stream() for _ in range(3)] self.device = current_platform.device_type # Reserved topk indices buffer for all Indexer layers to reuse. @@ -1263,7 +1261,7 @@ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): vllm_config, prefix=prefix, topk_indices_buffer=self.topk_indices_buffer, - aux_stream_dict=self.aux_stream_dict, + aux_stream_list=aux_stream_list, ), prefix=f"{prefix}.layers", ) diff --git a/vllm/utils/multi_stream_utils.py b/vllm/utils/multi_stream_utils.py index cc6bc6462449..c00f08f93329 100644 --- a/vllm/utils/multi_stream_utils.py +++ b/vllm/utils/multi_stream_utils.py @@ -56,3 +56,67 @@ def maybe_execute_in_parallel( result0 = fn0() result1 = fn1() return (result0, result1) + + +def execute_in_parallel( + default_fn: Callable[[], Any], + aux_fns: list[Callable[[], Any] | None], + start_event: torch.cuda.Event, + done_events: list[torch.cuda.Event], + aux_streams: list[torch.cuda.Stream] | None = None, +) -> tuple[Any, list[Any]]: + """Run default_fn on the current stream and aux_fns concurrently on + aux_streams. + + Generalizes maybe_execute_in_parallel to N aux callables. Slots where + aux_fns[i] is None are skipped (no stream switch, no event record); their + corresponding entry in the returned aux_results list is None. + + start_event fans out from the current stream to every launched aux stream; + done_events[i] is recorded after aux_fns[i] so the current stream joins + before returning. When aux_streams is None, all aux_fns run sequentially + on the current stream. + + Args: + default_fn: Callable for the default (current) stream. + aux_fns: Per-aux callables; entries may be None to skip. + start_event: CUDA event recorded on the current stream before + default_fn so each launched aux stream can wait on it. + done_events: One CUDA event per aux slot, recorded after the + corresponding aux_fn. Length must match aux_fns. + aux_streams: Per-aux CUDA streams. Length must match aux_fns. + Multi-stream is disabled when None. + + Returns: + Tuple of (default_result, aux_results) where aux_results[i] is the + result of aux_fns[i] (or None when skipped). + """ + aux_results: list[Any] + if aux_streams is None: + default_result = default_fn() + aux_results = [fn() if fn is not None else None for fn in aux_fns] + return default_result, aux_results + + assert len(aux_fns) == len(aux_streams) == len(done_events), ( + "aux_fns, aux_streams, and done_events must be the same length" + ) + + aux_results = [None] * len(aux_fns) + pending: list[torch.cuda.Event] = [] + + start_event.record() + for i, fn in enumerate(aux_fns): + if fn is None: + continue + with torch.cuda.stream(aux_streams[i]): + start_event.wait() + aux_results[i] = fn() + done_events[i].record() + pending.append(done_events[i]) + + default_result = default_fn() + + for ev in pending: + ev.wait() + + return default_result, aux_results From a60883644be0bcf5219b792b5abbc448e4ea0dcf Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Tue, 28 Apr 2026 19:27:18 +0200 Subject: [PATCH 009/237] [Build] Defer flashinfer cubin download to avoid ~2.5 GB (decompressed) layer duplication (#41134) Signed-off-by: Benoit Tigeot --- docker/Dockerfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ca5e35f80c37..3d652a5dea62 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -585,9 +585,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ ARG FLASHINFER_VERSION=0.6.8.post1 RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --system flashinfer-jit-cache==${FLASHINFER_VERSION} \ - --extra-index-url https://flashinfer.ai/whl/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') \ - && flashinfer show-config \ - && flashinfer download-cubin + --extra-index-url https://flashinfer.ai/whl/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') # ============================================================ # OPENAI API SERVER DEPENDENCIES @@ -669,6 +667,13 @@ RUN --mount=type=bind,from=build,src=/tmp/ep_kernels_workspace/dist,target=/vllm uv pip install --system ep_kernels/dist/*.whl --verbose \ --extra-index-url ${PYTORCH_CUDA_INDEX_BASE_URL}/cu$(echo $CUDA_VERSION | cut -d. -f1,2 | tr -d '.') +# Download FlashInfer precompiled cubins AFTER all pip installs are done. +# This must run after the vLLM wheel and EP kernels installs above, because +# those can reinstall/touch flashinfer packages. Downloading cubins earlier +# (in the flashinfer-jit-cache layer) causes ~2.5 GB of layer duplication +# when a later pip install overwrites flashinfer package files. +RUN flashinfer show-config && flashinfer download-cubin + # CUDA image changed from /usr/local/nvidia to /usr/local/cuda in 12.8 but will # return to /usr/local/nvidia in 13.0 to allow container providers to mount drivers # consistently from the host (see https://github.com/vllm-project/vllm/issues/18859). From 358a755e43b07b9454904df9d3c3fae3340058f1 Mon Sep 17 00:00:00 2001 From: rasmith Date: Tue, 28 Apr 2026 13:14:59 -0500 Subject: [PATCH 010/237] [CI][AMD][BugFix] Update request URL in test_moriio_connector to match vllm-router compatibility changes (#41076) Signed-off-by: Randall Smith --- .../unit/test_moriio_connector.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/v1/kv_connector/unit/test_moriio_connector.py b/tests/v1/kv_connector/unit/test_moriio_connector.py index 902957e18309..16d34d90896b 100644 --- a/tests/v1/kv_connector/unit/test_moriio_connector.py +++ b/tests/v1/kv_connector/unit/test_moriio_connector.py @@ -3,6 +3,7 @@ import importlib.util import os import subprocess +import uuid from unittest.mock import MagicMock, patch import msgspec @@ -99,6 +100,11 @@ def _setup_kv_transfer_request( "remote_engine_id": "test_engine", } ) + zmq_addr = f"host:{remote_host},handshake:{fake_port},notify:{fake_port}" + fake_uuid = uuid.uuid4().hex + request.request_id = ( + f"___prefill_addr_{zmq_addr}___decode_addr_{zmq_addr}_{fake_uuid}" + ) return request @@ -254,13 +260,14 @@ def test_write_mode_saves_local_block_ids(): do_remote_decode=True, do_remote_prefill=False, ) + + # Setup KV transfer params and embed ZMQ addrs in request_id before + # adding to scheduler so the ID is consistent everywhere. + request = _setup_kv_transfer_request(request) request_id = request.request_id scheduler.add_request(request) - # Fake Config - request = _setup_kv_transfer_request(request) - # Remote Prefill, triggers MoRIIOConnectorMetadata. scheduler_output = scheduler.schedule() kv_connector_metadata = scheduler_output.kv_connector_metadata @@ -312,13 +319,14 @@ def test_write_mode_with_chunked_prefill_saves_local_block_ids(): do_remote_decode=True, do_remote_prefill=False, ) + + # Setup KV transfer params and embed ZMQ addrs in request_id before + # adding to scheduler so the ID is consistent everywhere. + request = _setup_kv_transfer_request(request) request_id = request.request_id scheduler.add_request(request) - # Fake Config - request = _setup_kv_transfer_request(request) - # Remote Prefill with chunked prefill, triggers multiple schedules. expected_counts = [(0, 0, 0), (0, 0, 0), (1, 0, 0)] kv_connector_metadata = None @@ -363,6 +371,10 @@ def test_read_mode_loads_remote_block_ids(moriio_read_mode): do_remote_decode=False, do_remote_prefill=True, ) + + # Setup KV transfer params and embed ZMQ addrs in request_id before + # adding to scheduler so the ID is consistent everywhere. + request = _setup_kv_transfer_request(request) request_id = request.request_id scheduler.add_request(request) @@ -370,8 +382,6 @@ def test_read_mode_loads_remote_block_ids(moriio_read_mode): 0 ].req_to_blocks[request_id] - request = _setup_kv_transfer_request(request) - # Set remote block ids to be fetched. request.kv_transfer_params["remote_block_ids"] = block_list From 0899f436aab42f798fb8e728872334c83aaebb79 Mon Sep 17 00:00:00 2001 From: Joe Rowell Date: Tue, 28 Apr 2026 20:23:00 +0200 Subject: [PATCH 011/237] [New Model] Laguna XS.2 implementation (#41129) Signed-off-by: Joe Rowell Signed-off-by: Robert Shaw Co-authored-by: Robert Shaw --- tests/models/registry.py | 1 + vllm/model_executor/models/laguna.py | 886 ++++++++++++++++++ vllm/model_executor/models/registry.py | 1 + vllm/reasoning/__init__.py | 4 + .../reasoning/poolside_v1_reasoning_parser.py | 72 ++ vllm/tool_parsers/__init__.py | 4 + vllm/tool_parsers/poolside_v1_tool_parser.py | 583 ++++++++++++ vllm/transformers_utils/config.py | 44 +- vllm/transformers_utils/configs/__init__.py | 2 + vllm/transformers_utils/configs/laguna.py | 120 +++ 10 files changed, 1701 insertions(+), 16 deletions(-) create mode 100644 vllm/model_executor/models/laguna.py create mode 100644 vllm/reasoning/poolside_v1_reasoning_parser.py create mode 100644 vllm/tool_parsers/poolside_v1_tool_parser.py create mode 100644 vllm/transformers_utils/configs/laguna.py diff --git a/tests/models/registry.py b/tests/models/registry.py index 6b041c67071d..19304d803160 100644 --- a/tests/models/registry.py +++ b/tests/models/registry.py @@ -372,6 +372,7 @@ def check_available_online( "KimiLinearForCausalLM": _HfExamplesInfo( "moonshotai/Kimi-Linear-48B-A3B-Instruct", trust_remote_code=True ), + "LagunaForCausalLM": _HfExamplesInfo("poolside/Laguna-XS.2"), "Lfm2ForCausalLM": _HfExamplesInfo("LiquidAI/LFM2-1.2B"), "Lfm2MoeForCausalLM": _HfExamplesInfo( "LiquidAI/LFM2-8B-A1B", diff --git a/vllm/model_executor/models/laguna.py b/vllm/model_executor/models/laguna.py new file mode 100644 index 000000000000..08f35d691817 --- /dev/null +++ b/vllm/model_executor/models/laguna.py @@ -0,0 +1,886 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Inference-only Laguna model compatible with HuggingFace weights.""" + +import typing +from collections.abc import Callable, Iterable +from itertools import islice + +import torch +import torch.nn.functional as F +from torch import nn + +from vllm.compilation.decorators import support_torch_compile +from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config +from vllm.distributed import ( + get_ep_group, + get_pp_group, + get_tensor_model_parallel_rank, + get_tensor_model_parallel_world_size, +) +from vllm.logger import init_logger +from vllm.model_executor.layers.attention import Attention +from vllm.model_executor.layers.fused_moe import FusedMoE +from vllm.model_executor.layers.layernorm import RMSNorm +from vllm.model_executor.layers.linear import ( + ColumnParallelLinear, + QKVParallelLinear, + ReplicatedLinear, + RowParallelLinear, +) +from vllm.model_executor.layers.logits_processor import LogitsProcessor +from vllm.model_executor.layers.quantization import QuantizationConfig +from vllm.model_executor.layers.rotary_embedding import get_rope +from vllm.model_executor.layers.vocab_parallel_embedding import ( + ParallelLMHead, + VocabParallelEmbedding, +) +from vllm.model_executor.model_loader.weight_utils import ( + default_weight_loader, + maybe_remap_kv_scale_name, +) +from vllm.model_executor.models.interfaces import SupportsLoRA, SupportsPP +from vllm.model_executor.models.utils import ( + AutoWeightsLoader, + PPMissingLayer, + extract_layer_index, + is_pp_missing_parameter, + make_empty_intermediate_tensors_factory, + make_layers, + maybe_prefix, +) +from vllm.sequence import IntermediateTensors + +logger = init_logger(__name__) + + +class LagunaMLP(nn.Module): + """Dense MLP for Laguna (used in mlp_only_layers).""" + + def __init__( + self, + hidden_size: int, + intermediate_size: int, + hidden_act: str, + quant_config: QuantizationConfig | None = None, + reduce_results: bool = True, + prefix: str = "", + ) -> None: + super().__init__() + # gate_proj and up_proj are kept as separate ColumnParallelLinear + # rather than merged via MergedColumnParallelLinear. The merged form + # requires per-partition NVFP4 global scales (weight_global_scale, + # input_global_scale) to be packed into a length-2 PerTensorScaleParameter + # and then collapsed via .max() in process_weights_after_loading; this + # doesn't round-trip cleanly through Marlin's NVFP4 stacked-layer code + # path. Splitting yields one global scale per Linear, exactly matching + # the standard compressed-tensors per-Linear schema on disk. + self.gate_proj = ColumnParallelLinear( + hidden_size, + intermediate_size, + bias=False, + quant_config=quant_config, + prefix=f"{prefix}.gate_proj", + ) + self.up_proj = ColumnParallelLinear( + hidden_size, + intermediate_size, + bias=False, + quant_config=quant_config, + prefix=f"{prefix}.up_proj", + ) + self.down_proj = RowParallelLinear( + intermediate_size, + hidden_size, + bias=False, + quant_config=quant_config, + reduce_results=reduce_results, + prefix=f"{prefix}.down_proj", + ) + if hidden_act != "silu": + raise ValueError( + f"Unsupported activation: {hidden_act}. Only silu is supported." + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + gate, _ = self.gate_proj(x) + up, _ = self.up_proj(x) + x, _ = self.down_proj(F.silu(gate) * up) + return x + + +class LagunaMoE(nn.Module): + """Sparse MoE block for Laguna with optional shared expert and sigmoid routing. + + Key differences from other MoE implementations: + - Uses SIGMOID routing activation (not softmax) + - Shared expert runs in parallel with routed experts (when enabled) + - Matches HF reference: modular_laguna.py LagunaSparseMoeBlock + """ + + def __init__( + self, + config, + quant_config: QuantizationConfig | None = None, + prefix: str = "", + enable_eplb: bool = False, + ): + super().__init__() + self.config = config + self.num_experts = config.num_experts + self.top_k = config.num_experts_per_tok + + self.tp_size = get_tensor_model_parallel_world_size() + self.ep_group = get_ep_group().device_group + self.ep_rank = self.ep_group.rank() + self.ep_size = self.ep_group.size() + + self.n_routed_experts = config.num_experts + self.n_shared_experts = 1 if config.shared_expert_intermediate_size > 0 else 0 + self.routed_scaling_factor = float( + getattr(config, "moe_routed_scaling_factor", 1.0) + ) + + if self.tp_size > config.num_experts: + raise ValueError( + f"Tensor parallel size {self.tp_size} is greater than " + f"the number of experts {config.num_experts}." + ) + + # Load balancing settings. + vllm_config = get_current_vllm_config() + eplb_config = vllm_config.parallel_config.eplb_config + self.enable_eplb = enable_eplb + eplb_config.num_redundant_experts = ( + eplb_config.num_redundant_experts + if eplb_config.num_redundant_experts is not None + else 0 + ) + self.n_redundant_experts = eplb_config.num_redundant_experts + self.n_logical_experts = self.n_routed_experts + self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts + self.n_local_physical_experts = self.n_physical_experts // self.ep_size + self.physical_expert_start = self.ep_rank * self.n_local_physical_experts + self.physical_expert_end = ( + self.physical_expert_start + self.n_local_physical_experts + ) + + # Router gate + self.gate = ReplicatedLinear( + config.hidden_size, + config.num_experts, + bias=False, + quant_config=None, + prefix=f"{prefix}.gate", + ) + + # Shared expert (optional) - passed to FusedMoE for overlap optimization + self.shared_expert: LagunaMLP | None + if config.shared_expert_intermediate_size > 0: + self.shared_expert = LagunaMLP( + hidden_size=config.hidden_size, + intermediate_size=config.shared_expert_intermediate_size, + hidden_act=config.hidden_act, + quant_config=quant_config, + reduce_results=False, # Reduce after shared+routed combine + prefix=f"{prefix}.shared_expert", + ) + else: + self.shared_expert = None + + # Auxiliary-loss-free load-balancing bias (arXiv:2408.15664). The + # checkpoint stores one [num_experts] tensor per MoE layer at + # `mlp.experts.e_score_correction_bias`; registering it as a Parameter + # on the FusedMoE lets the weight loader pick it up and the router + # add it during top-k selection. The fused top-k bias router requires + # float32 regardless of model dtype. + e_score_correction_bias = torch.nn.Parameter( + torch.zeros(config.num_experts, dtype=torch.float32), + requires_grad=False, + ) + + # FusedMoE with SIGMOID routing. Passing `shared_experts=` lets the + # layer overlap the shared-expert compute with the all2all dispatch. + # `apply_routed_scale_to_output=True` makes FusedMoE handle the + # routed_scaling_factor, shared+routed combine, and TP all-reduce + # internally, so forward() just returns the final hidden states. + self.experts = FusedMoE( + shared_experts=self.shared_expert, + num_experts=config.num_experts, + top_k=config.num_experts_per_tok, + hidden_size=config.hidden_size, + intermediate_size=config.moe_intermediate_size, + renormalize=config.norm_topk_prob, + quant_config=quant_config, + prefix=f"{prefix}.experts", + scoring_func="sigmoid", + use_grouped_topk=False, + apply_router_weight_on_input=bool(config.moe_apply_router_weight_on_input), + e_score_correction_bias=e_score_correction_bias, + enable_eplb=self.enable_eplb, + num_redundant_experts=self.n_redundant_experts, + routed_scaling_factor=self.routed_scaling_factor, + apply_routed_scale_to_output=True, + ) + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + orig_shape = hidden_states.shape + hidden_dim = hidden_states.shape[-1] + hidden_states = hidden_states.view(-1, hidden_dim) + + router_logits, _ = self.gate(hidden_states) + router_logits = router_logits.float() + softcap = getattr(self.config, "moe_router_logit_softcapping", 0.0) or 0.0 + if softcap > 0.0: + router_logits = torch.tanh(router_logits / softcap) * softcap + + final_hidden_states = self.experts(hidden_states, router_logits) + return final_hidden_states.view(orig_shape) + + +class LagunaAttention(nn.Module): + """Laguna attention with optional softplus output gating. + + Supports per-layer sliding window attention when ``config.layer_types`` + is present. Layers whose type is ``"sliding_attention"`` use + ``config.sliding_window``; all other layers (typically labelled + ``"full_attention"``) use full attention. When ``layer_types`` is + absent every layer defaults to full attention for backwards + compatibility. + """ + + def __init__( + self, + config, + hidden_size: int, + num_heads: int, + num_kv_heads: int, + max_position_embeddings: int = 131072, + head_dim: int | None = None, + cache_config: CacheConfig | None = None, + quant_config: QuantizationConfig | None = None, + prefix: str = "", + attention_sink: bool = False, + ) -> None: + super().__init__() + self.hidden_size = hidden_size + tp_size = get_tensor_model_parallel_world_size() + self.total_num_heads = num_heads + assert self.total_num_heads % tp_size == 0 + self.num_heads = self.total_num_heads // tp_size + self.total_num_kv_heads = num_kv_heads + if self.total_num_kv_heads >= tp_size: + assert self.total_num_kv_heads % tp_size == 0 + else: + assert tp_size % self.total_num_kv_heads == 0 + self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size) + self.head_dim = head_dim or (hidden_size // self.total_num_heads) + self.q_size = self.num_heads * self.head_dim + self.kv_size = self.num_kv_heads * self.head_dim + self.scaling = self.head_dim**-0.5 + self.max_position_embeddings = max_position_embeddings + + # Gating flag + self.gating = config.gating + + # Per-layer sliding window (follows Gemma2/Cohere2 convention) + layer_types = getattr(config, "layer_types", None) + if layer_types is not None: + layer_idx = extract_layer_index(prefix) + is_sliding = layer_types[layer_idx] == "sliding_attention" + self.sliding_window = config.sliding_window if is_sliding else None + else: + self.sliding_window = None + + # QKV projection (no bias for Laguna) + self.qkv_proj = QKVParallelLinear( + self.hidden_size, + self.head_dim, + self.total_num_heads, + self.total_num_kv_heads, + bias=config.qkv_bias, + quant_config=quant_config, + prefix=f"{prefix}.qkv_proj", + ) + + # Output projection + self.o_proj = RowParallelLinear( + self.total_num_heads * self.head_dim, + self.hidden_size, + bias=config.attention_bias, + quant_config=quant_config, + prefix=f"{prefix}.o_proj", + ) + + # Gating projection (Laguna-specific, optional) + # config.gating may be: + # - True / "per-element": one gate per (head, head_dim) channel + # - "per-head": one gate per head, broadcast across head_dim + if self.gating: + # v5 LagunaConfig uses ``gating=True`` for per-head; older configs + # used ``"per-head"``. Accept both. ``"per-element"`` (or legacy + # ``True``) means per-element gating with output size num_heads × + # head_dim. + gate_per_head = self.gating is True or self.gating == "per-head" + g_out = ( + self.total_num_heads + if gate_per_head + else self.total_num_heads * self.head_dim + ) + self.g_proj = ColumnParallelLinear( + hidden_size, + g_out, + bias=False, + quant_config=quant_config, + prefix=f"{prefix}.g_proj", + ) + self.gate_per_head = gate_per_head + else: + self.g_proj = None + self.gate_per_head = False + + # Attention sinks (learnable per-head bias for SWA layers) + sinks = None + if attention_sink: + self.sink = torch.nn.Parameter( + torch.empty(self.total_num_heads // tp_size, requires_grad=False) + ) + sinks = self.sink + + # Resolve rope params per-layer-type. ``config.rope_parameters`` is + # either a flat dict (legacy) or a nested ``{layer_type: rope_dict}`` + # (v5 Laguna-XS schema). The v5 form is unhashable as-is and would + # crash `get_rope`'s cache lookup, so always pull out the layer's + # sub-dict before forwarding. + layer_type = ( + layer_types[extract_layer_index(prefix)] + if layer_types is not None + else "full_attention" + ) + is_sliding = layer_type == "sliding_attention" + + top_rope = getattr(config, "rope_parameters", None) or {} + if any(isinstance(v, dict) for v in top_rope.values()): + # Nested per-layer-type form. + base_rope = top_rope.get(layer_type) or top_rope.get("full_attention") or {} + else: + base_rope = top_rope + + # Older flat-rope ckpts can carry a separate `swa_rope_parameters` + # for SWA layers. Prefer it when present; otherwise the nested + # rope dict above already supplies the correct sub-config. + swa_rope = getattr(config, "swa_rope_parameters", None) + if ( + is_sliding + and swa_rope is None + and not any(isinstance(v, dict) for v in top_rope.values()) + ): + logger.warning_once( + "Laguna config has sliding_attention layers but neither " + "`swa_rope_parameters` nor a nested per-layer-type " + "`rope_parameters` — SWA layers will reuse the global rope. " + "If the checkpoint was trained with distinct SWA rope " + "(theta / partial_rotary_factor), regenerate its HF config " + "to include either form." + ) + rope_params = swa_rope if (is_sliding and swa_rope is not None) else base_rope + # `partial_rotary_factor` may live on the top-level config (main attention) + # or on the per-layer rope dict itself (e.g. SWA can differ). Inject the + # top-level value into `rope_params` if the dict doesn't already set it. + top_partial = getattr(config, "partial_rotary_factor", None) + if top_partial is not None and "partial_rotary_factor" not in rope_params: + rope_params = {**rope_params, "partial_rotary_factor": top_partial} + + # Rotary embeddings (YaRN) + self.rotary_emb = get_rope( + head_size=self.head_dim, + max_position=max_position_embeddings, + is_neox_style=True, + rope_parameters=rope_params, + ) + + self.attn = Attention( + self.num_heads, + self.head_dim, + self.scaling, + num_kv_heads=self.num_kv_heads, + cache_config=cache_config, + quant_config=quant_config, + per_layer_sliding_window=self.sliding_window, + prefix=f"{prefix}.attn", + sinks=sinks, + ) + + # QK normalization (like Qwen3) + self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps) + self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps) + + def forward( + self, + positions: torch.Tensor, + hidden_states: torch.Tensor, + ) -> torch.Tensor: + qkv, _ = self.qkv_proj(hidden_states) + q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1) + + q_by_head = q.view(*q.shape[:-1], q.shape[-1] // self.head_dim, self.head_dim) + q_by_head = self.q_norm(q_by_head) + q = q_by_head.view(q.shape) + + k_by_head = k.view(*k.shape[:-1], k.shape[-1] // self.head_dim, self.head_dim) + k_by_head = self.k_norm(k_by_head) + k = k_by_head.view(k.shape) + + q, k = self.rotary_emb(positions, q, k) + attn_output = self.attn(q, k, v) + + # Apply gating if enabled (compute softplus in float32 for precision) + if self.gating and self.g_proj is not None: + gate, _ = self.g_proj(hidden_states) + gate = F.softplus(gate.float()).type_as(attn_output) + if self.gate_per_head: + # gate: [..., num_heads]; broadcast across head_dim + attn_shape = attn_output.shape + attn_output = ( + attn_output.view(*attn_shape[:-1], self.num_heads, self.head_dim) + * gate.unsqueeze(-1) + ).view(attn_shape) + else: + attn_output = attn_output * gate + + output, _ = self.o_proj(attn_output) + return output + + +class LagunaDecoderLayer(nn.Module): + def __init__( + self, + config, + cache_config: CacheConfig | None = None, + quant_config: QuantizationConfig | None = None, + prefix: str = "", + enable_eplb: bool = False, + ) -> None: + super().__init__() + self.hidden_size = config.hidden_size + layer_idx = extract_layer_index(prefix) + + # Determine if this layer uses sliding window attention + layer_types = getattr(config, "layer_types", None) + is_sliding = ( + layer_types is not None and layer_types[layer_idx] == "sliding_attention" + ) + + # Enable attention sinks on SWA layers when configured + attention_sink = is_sliding and getattr( + config, "swa_attention_sink_enabled", False + ) + + # Optional per-layer override of head count (Laguna-XS). + per_layer_heads = getattr(config, "num_attention_heads_per_layer", None) + layer_num_heads = ( + per_layer_heads[layer_idx] + if per_layer_heads is not None + else config.num_attention_heads + ) + + self.self_attn = LagunaAttention( + config=config, + hidden_size=self.hidden_size, + num_heads=layer_num_heads, + num_kv_heads=config.num_key_value_heads, + max_position_embeddings=config.max_position_embeddings, + head_dim=getattr(config, "head_dim", None), + cache_config=cache_config, + quant_config=quant_config, + prefix=f"{prefix}.self_attn", + attention_sink=attention_sink, + ) + + # Check if this layer uses MoE or dense MLP (matches Qwen2/Qwen3 convention) + mlp_only_layers = ( + [] if not hasattr(config, "mlp_only_layers") else config.mlp_only_layers + ) + self.is_moe_layer = ( + (layer_idx not in mlp_only_layers) + and (config.num_experts > 0) + and ((layer_idx + 1) % config.decoder_sparse_step == 0) + ) + + if self.is_moe_layer: + self.mlp = LagunaMoE( + config=config, + quant_config=quant_config, + prefix=f"{prefix}.mlp", + enable_eplb=enable_eplb, + ) + else: + self.mlp = LagunaMLP( + hidden_size=config.hidden_size, + intermediate_size=config.intermediate_size, + hidden_act=config.hidden_act, + quant_config=quant_config, + prefix=f"{prefix}.mlp", + ) + + self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = RMSNorm( + config.hidden_size, eps=config.rms_norm_eps + ) + + def forward( + self, + positions: torch.Tensor, + hidden_states: torch.Tensor, + residual: torch.Tensor | None, + ) -> tuple[torch.Tensor, torch.Tensor]: + # Self Attention + if residual is None: + residual = hidden_states + hidden_states = self.input_layernorm(hidden_states) + else: + hidden_states, residual = self.input_layernorm(hidden_states, residual) + + hidden_states = self.self_attn( + positions=positions, + hidden_states=hidden_states, + ) + + # Fully Connected + hidden_states, residual = self.post_attention_layernorm(hidden_states, residual) + hidden_states = self.mlp(hidden_states) + + return hidden_states, residual + + +@support_torch_compile +class LagunaModel(nn.Module): + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + super().__init__() + + config = vllm_config.model_config.hf_config + cache_config = vllm_config.cache_config + quant_config = vllm_config.quant_config + enable_eplb = vllm_config.parallel_config.enable_eplb + eplb_config = vllm_config.parallel_config.eplb_config + self.num_redundant_experts = eplb_config.num_redundant_experts + self.config = config + self.quant_config = quant_config + + # Disable the model-level sliding-window fallback in Attention.__init__. + # Laguna drives SWA per-layer via `layer_types`, passing + # `per_layer_sliding_window=self.sliding_window` (None for global + # layers). Without this, global layers whose `per_layer_sliding_window` + # is None would pick up `cache_config.sliding_window` + # (populated from `config.sliding_window`) as a fallback, silently + # applying a 512-token window to full-attention layers. + if cache_config is not None: + cache_config.sliding_window = None + + self.vocab_size = config.vocab_size + + if get_pp_group().is_first_rank or ( + config.tie_word_embeddings and get_pp_group().is_last_rank + ): + self.embed_tokens = VocabParallelEmbedding( + config.vocab_size, + config.hidden_size, + quant_config=quant_config, + prefix=f"{prefix}.embed_tokens", + ) + else: + self.embed_tokens = PPMissingLayer() + + self.start_layer, self.end_layer, self.layers = make_layers( + config.num_hidden_layers, + lambda prefix: LagunaDecoderLayer( + config=config, + cache_config=cache_config, + quant_config=quant_config, + prefix=prefix, + enable_eplb=enable_eplb, + ), + prefix=f"{prefix}.layers", + ) + + if get_pp_group().is_last_rank: + self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + else: + self.norm = PPMissingLayer() + + self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory( + ["hidden_states", "residual"], config.hidden_size + ) + + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: + return self.embed_tokens(input_ids) + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + intermediate_tensors: IntermediateTensors | None = None, + inputs_embeds: torch.Tensor | None = None, + ) -> torch.Tensor | IntermediateTensors: + if get_pp_group().is_first_rank: + if inputs_embeds is not None: + hidden_states = inputs_embeds + else: + hidden_states = self.embed_tokens(input_ids) + residual = None + else: + assert intermediate_tensors is not None + hidden_states = intermediate_tensors["hidden_states"] + residual = intermediate_tensors["residual"] + + for layer in islice(self.layers, self.start_layer, self.end_layer): + hidden_states, residual = layer(positions, hidden_states, residual) + + if not get_pp_group().is_last_rank: + return IntermediateTensors( + {"hidden_states": hidden_states, "residual": residual} + ) + + hidden_states, _ = self.norm(hidden_states, residual) + return hidden_states + + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]: + """Get expert parameter mapping for weight loading. + + Returns mapping tuples of (param_name, weight_name, expert_id, shard_id) + that handle both weights and quantization scales. + """ + return FusedMoE.make_expert_params_mapping( + self, + ckpt_gate_proj_name="gate_proj", + ckpt_down_proj_name="down_proj", + ckpt_up_proj_name="up_proj", + num_experts=self.config.num_experts, + num_redundant_experts=self.num_redundant_experts, + ) + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + stacked_params_mapping = [ + # (param_name, shard_name, shard_id) + ("qkv_proj", "q_proj", "q"), + ("qkv_proj", "k_proj", "k"), + ("qkv_proj", "v_proj", "v"), + # gate_proj and up_proj are loaded as separate Linears (see + # LagunaMLP) so no merge entry is needed here. + ] + + # Suffixes to skip for GPTQ/modelopt models if param doesn't exist + ignore_suffixes = ( + ".bias", + "_bias", + ".k_scale", + "_k_scale", + ".v_scale", + "_v_scale", + ".weight_scale", + "_weight_scale", + ".input_scale", + "_input_scale", + ) + + params_dict = dict(self.named_parameters()) + loaded_params: set[str] = set() + expert_params_mapping = self.get_expert_mapping() + + tp_rank = get_tensor_model_parallel_rank() + + for name, loaded_weight in weights: + # Handle attention sinks (distributed across ranks). Derive the + # per-rank slice from the parameter's own shape so per-layer + # variations in head count are handled correctly. + if "sink" in name: + param = params_dict.get(name) + if param is not None: + layer_heads_per_rank = param.shape[0] + layer_head_start = tp_rank * layer_heads_per_rank + narrow_weight = loaded_weight.narrow( + 0, layer_head_start, layer_heads_per_rank + ) + param.data.copy_(narrow_weight) + loaded_params.add(name) + continue + + # Handle KV cache quantization scales + if self.quant_config is not None and ( + scale_name := self.quant_config.get_cache_scale(name) + ): + param = params_dict[scale_name] + weight_loader = getattr(param, "weight_loader", default_weight_loader) + assert loaded_weight.numel() == 1, ( + f"KV scale numel {loaded_weight.numel()} != 1" + ) + loaded_weight = loaded_weight.squeeze() + weight_loader(param, loaded_weight) + loaded_params.add(scale_name) + continue + + # Handle stacked params (QKV, gate_up for + # non-expert layers and shared_expert) + for param_name, weight_name, shard_id in stacked_params_mapping: + if weight_name not in name: + continue + # Skip expert weights - handled below via expert_params_mapping + if "mlp.experts" in name and "shared_expert" not in name: + continue + name = name.replace(weight_name, param_name) + + if name.endswith(ignore_suffixes) and name not in params_dict: + continue + if is_pp_missing_parameter(name, self): + continue + # Remap FP8 kv_scale names for backwards compatibility + if name.endswith("scale"): + name = maybe_remap_kv_scale_name(name, params_dict) + if name is None: + continue + if name not in params_dict: + continue + + param = params_dict[name] + weight_loader = getattr(param, "weight_loader", default_weight_loader) + if weight_loader == default_weight_loader: + weight_loader(param, loaded_weight) + else: + weight_loader(param, loaded_weight, shard_id) + loaded_params.add(name) + break + else: + # Try expert params mapping (handles weights + quantization scales) + is_expert_weight = False + for mapping in expert_params_mapping: + param_name, weight_name, expert_id, shard_id = mapping + if weight_name not in name: + continue + + # Mark as expert weight so we skip regular loading below + is_expert_weight = True + + # Create mapped name without modifying original + name_mapped = name.replace(weight_name, param_name) + + if is_pp_missing_parameter(name_mapped, self): + continue + if ( + name_mapped.endswith(ignore_suffixes) + and name_mapped not in params_dict + ): + continue + if name_mapped not in params_dict: + continue + + param = params_dict[name_mapped] + # Use return_success to handle expert parallelism correctly + weight_loader = typing.cast( + Callable[..., bool], param.weight_loader + ) + success = weight_loader( + param, + loaded_weight, + name_mapped, + shard_id=shard_id, + expert_id=expert_id, + return_success=True, + ) + if success: + loaded_params.add(name_mapped) + break + else: + # Expert weight not mapped to this rank - skip + if is_expert_weight: + continue + + # Remap kv_scale names before the ignore_suffixes filter: + # the suffix list includes .k_scale/.v_scale, so filtering + # first drops the checkpoint key before remap can rewrite + # it to the .attn.* name that exists in params_dict. + name = maybe_remap_kv_scale_name(name, params_dict) + if name is None: + continue + + if name.endswith(ignore_suffixes) and name not in params_dict: + continue + + if is_pp_missing_parameter(name, self): + continue + + if name not in params_dict: + continue + + param = params_dict[name] + weight_loader = getattr( + param, "weight_loader", default_weight_loader + ) + weight_loader(param, loaded_weight) + loaded_params.add(name) + + return loaded_params + + +class LagunaForCausalLM(nn.Module, SupportsPP, SupportsLoRA): + fall_back_to_pt_during_load = False + + packed_modules_mapping = { + "qkv_proj": ["q_proj", "k_proj", "v_proj"], + } + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + super().__init__() + config = vllm_config.model_config.hf_config + quant_config = vllm_config.quant_config + self.config = config + self.quant_config = quant_config + + self.model = LagunaModel( + vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") + ) + + if get_pp_group().is_last_rank: + self.lm_head = ParallelLMHead( + config.vocab_size, + config.hidden_size, + quant_config=quant_config, + prefix=maybe_prefix(prefix, "lm_head"), + ) + if self.config.tie_word_embeddings: + self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens) + else: + self.lm_head = PPMissingLayer() + + self.logits_processor = LogitsProcessor(config.vocab_size) + self.make_empty_intermediate_tensors = ( + self.model.make_empty_intermediate_tensors + ) + + def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: + return self.model.embed_input_ids(input_ids) + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + intermediate_tensors: IntermediateTensors | None = None, + inputs_embeds: torch.Tensor | None = None, + ) -> torch.Tensor | IntermediateTensors: + hidden_states = self.model( + input_ids, positions, intermediate_tensors, inputs_embeds + ) + return hidden_states + + def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None: + logits = self.logits_processor(self.lm_head, hidden_states) + return logits + + def get_expert_mapping(self) -> list[tuple[str, str, int, str]]: + return self.model.get_expert_mapping() + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + loader = AutoWeightsLoader( + self, + skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None), + ) + return loader.load_weights(weights) diff --git a/vllm/model_executor/models/registry.py b/vllm/model_executor/models/registry.py index 80cc8b895345..eba288dcc77a 100644 --- a/vllm/model_executor/models/registry.py +++ b/vllm/model_executor/models/registry.py @@ -150,6 +150,7 @@ "KimiLinearForCausalLM": ("kimi_linear", "KimiLinearForCausalLM"), "Lfm2ForCausalLM": ("lfm2", "Lfm2ForCausalLM"), "Lfm2MoeForCausalLM": ("lfm2_moe", "Lfm2MoeForCausalLM"), + "LagunaForCausalLM": ("laguna", "LagunaForCausalLM"), "LlamaForCausalLM": ("llama", "LlamaForCausalLM"), "Llama4ForCausalLM": ("llama4", "Llama4ForCausalLM"), # For decapoda-research/llama-* diff --git a/vllm/reasoning/__init__.py b/vllm/reasoning/__init__.py index 755fa56d294c..2347eae54c25 100644 --- a/vllm/reasoning/__init__.py +++ b/vllm/reasoning/__init__.py @@ -32,6 +32,10 @@ "deepseek_v3_reasoning_parser", "DeepSeekV3ReasoningParser", ), + "poolside_v1": ( + "poolside_v1_reasoning_parser", + "PoolsideV1ReasoningParser", + ), "ernie45": ( "ernie45_reasoning_parser", "Ernie45ReasoningParser", diff --git a/vllm/reasoning/poolside_v1_reasoning_parser.py b/vllm/reasoning/poolside_v1_reasoning_parser.py new file mode 100644 index 000000000000..30031d8513a9 --- /dev/null +++ b/vllm/reasoning/poolside_v1_reasoning_parser.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +Laguna reasoning parser. + +``DeepSeekV3ReasoningParser.is_reasoning_end`` walks the entire +token sequence backwards and returns ``True`` on the first ```` it +sees. When called on ``prompt_token_ids`` that mistakes any stray +```` in conversation history, few-shot examples or tool descriptions +for a template-injected "thinking already ended" marker. In the streaming +path (see ``vllm/entrypoints/openai/chat_completion/serving.py``, +``prompt_is_reasoning_end_arr``) that false positive short-circuits the +reasoning parser for the whole response, so any ``...`` the +model emits itself ends up in the content field instead of the reasoning +field. + +As we have more flexible templates, we instead scope +the backward search to the current assistant turn: the +walk terminates as soon as we hit the ```` start-of-message +token. A ```` in a prior user turn or few-shot example is no longer +visible. +""" + +from collections.abc import Sequence + +from transformers import PreTrainedTokenizerBase + +from vllm.reasoning.deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser +from vllm.reasoning.deepseek_v3_reasoning_parser import DeepSeekV3ReasoningParser +from vllm.reasoning.identity_reasoning_parser import IdentityReasoningParser + + +class PoolsideV1ReasoningParser(DeepSeekV3ReasoningParser): + """Drop-in replacement for ``deepseek_v3`` that tolerates ```` + tokens appearing anywhere in the prompt other than the generation prefix. + """ + + _start_of_assistant_message = "" + + def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs): + super().__init__(tokenizer, *args, **kwargs) + + if self._start_of_assistant_message not in self.vocab: + raise ValueError( + f"Tokenizer must contain {self._start_of_assistant_message!r} token" + ) + self._start_of_assistant_message_token_id = self.vocab[ + self._start_of_assistant_message + ] + + def is_reasoning_end(self, input_ids: Sequence[int]) -> bool: + # IdentityReasoningParser always returns True: no reasoning to parse. + if isinstance(self._parser, IdentityReasoningParser): + return True + + assert isinstance(self._parser, DeepSeekR1ReasoningParser) + for tok_id in reversed(input_ids): + # : reasoning is not yet ended. + if tok_id == self._parser.start_token_id: + return False + # : reasoning has ended. + if tok_id == self._parser.end_token_id: + return True + # : reached the start of the current assistant turn + # without seeing either marker. Anything further back belongs to + # the prior conversation and should be ignored. + if tok_id == self._start_of_assistant_message_token_id: + return False + return False + + +__all__ = ["PoolsideV1ReasoningParser"] diff --git a/vllm/tool_parsers/__init__.py b/vllm/tool_parsers/__init__.py index 8a39ca825d5f..61a11cbcc376 100644 --- a/vllm/tool_parsers/__init__.py +++ b/vllm/tool_parsers/__init__.py @@ -66,6 +66,10 @@ "hermes_tool_parser", "Hermes2ProToolParser", ), + "poolside_v1": ( + "poolside_v1_tool_parser", + "PoolsideV1ToolParser", + ), "hunyuan_a13b": ( "hunyuan_a13b_tool_parser", "HunyuanA13BToolParser", diff --git a/vllm/tool_parsers/poolside_v1_tool_parser.py b/vllm/tool_parsers/poolside_v1_tool_parser.py new file mode 100644 index 000000000000..f14b47362917 --- /dev/null +++ b/vllm/tool_parsers/poolside_v1_tool_parser.py @@ -0,0 +1,583 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +GLM-4 Tool Call Parser with incremental string streaming support. + +This parser fixes the streaming issue reported in Issue #32829 where long string +parameters (e.g., file content with 4000+ characters of code) are buffered until +complete, causing multi-second delays before the user sees any content. + +The fix streams string values incrementally as they arrive, providing a true +streaming experience for long content. +""" + +import ast +import json +from collections.abc import Sequence +from typing import Any + +import partial_json_parser.core.complete +import regex as re +from partial_json_parser.core.options import Allow + +from vllm.entrypoints.chat_utils import make_tool_call_id +from vllm.entrypoints.openai.chat_completion.protocol import ( + ChatCompletionRequest, +) +from vllm.entrypoints.openai.engine.protocol import ( + DeltaFunctionCall, + DeltaMessage, + DeltaToolCall, + ExtractedToolCallInformation, + FunctionCall, + ToolCall, +) +from vllm.entrypoints.openai.responses.protocol import ( + ResponsesRequest, +) +from vllm.logger import init_logger +from vllm.tokenizers import TokenizerLike +from vllm.tool_parsers.abstract_tool_parser import ( + Tool, + ToolParser, +) + +logger = init_logger(__name__) + + +class PoolsideV1ToolParser(ToolParser): + """Tool parser for GLM-4 models with incremental string streaming. + + This parser emits tool-call deltas incrementally as arguments arrive. + For string-type parameters, content is streamed character-by-character + rather than waiting for the complete tag. + """ + + def __init__(self, tokenizer: TokenizerLike, tools: list[Tool] | None = None): + super().__init__(tokenizer, tools) + # Stateful streaming fields + self.current_tool_name_sent: bool = False + self.prev_tool_call_arr: list[dict[str, Any]] = [] + self.current_tool_id: int = -1 + self.streamed_args_for_tool: list[str] = [] + + self.tool_call_start_token: str = "" + self.tool_call_end_token: str = "" + self.arg_key_start: str = "" + self.arg_key_end: str = "" + self.arg_val_start: str = "" + self.arg_val_end: str = "" + + self.tool_calls_start_token = self.tool_call_start_token + + self.func_call_regex = re.compile(r".*?", re.DOTALL) + self.func_detail_regex = re.compile( + r"([^\n]*)\n(.*)", re.DOTALL + ) + self.func_arg_regex = re.compile( + r"(.*?)\s*(.*?)", re.DOTALL + ) + + if not self.model_tokenizer: + raise ValueError( + "The model tokenizer must be passed to the ToolParser " + "constructor during construction." + ) + + self.tool_call_start_token_id = self.vocab.get(self.tool_call_start_token) + self.tool_call_end_token_id = self.vocab.get(self.tool_call_end_token) + self._buffer: str = "" + + # Streaming state for incremental tool-call streaming + self._in_tool_call: bool = False + self._current_tool_name: str | None = None + self._pending_key: str | None = None + self._streaming_string_value: bool = False + self._tool_call_ids: list[str] = [] + self._args_started: list[bool] = [] + self._args_closed: list[bool] = [] + self._seen_keys: list[set[str]] = [] + + @staticmethod + def _deserialize(value: str) -> Any: + try: + return json.loads(value) + except json.JSONDecodeError: + pass + + try: + return ast.literal_eval(value) + except (ValueError, SyntaxError): + pass + + return value + + @staticmethod + def _json_escape_string_content(s: str) -> str: + """JSON-escape string content for incremental streaming. + + This escapes the content that goes INSIDE a JSON string (between quotes), + not including the surrounding quotes themselves. + """ + if not s: + return "" + return json.dumps(s, ensure_ascii=False)[1:-1] + + @staticmethod + def _is_string_type( + tool_name: str, + arg_name: str, + tools: list[Tool] | None, + ) -> bool: + if tools is None: + return False + for tool in tools: + if tool.function.name != tool_name: + continue + if tool.function.parameters is None: + return False + arg_type = ( + tool.function.parameters.get("properties", {}) + .get(arg_name, {}) + .get("type", None) + ) + return arg_type == "string" + logger.debug("No tool named '%s'.", tool_name) + return False + + @staticmethod + def _tools_enabled(request: ChatCompletionRequest) -> bool: + """Return whether tool parsing should be applied for this request.""" + try: + tools = getattr(request, "tools", None) + tool_choice = getattr(request, "tool_choice", None) + return bool(tools) and tool_choice != "none" + except Exception: + logger.exception("Failed to determine if tools are enabled.") + return False + + def adjust_request( + self, request: ChatCompletionRequest | ResponsesRequest + ) -> ChatCompletionRequest | ResponsesRequest: + """Adjust request parameters for tool call token handling.""" + request = super().adjust_request(request) + if request.tools and request.tool_choice != "none": + # Ensure tool call tokens (, ) are not skipped + # during decoding. Even though they are not marked as special tokens, + # setting skip_special_tokens=False ensures proper handling in + # transformers 5.x where decoding behavior may have changed. + request.skip_special_tokens = False + return request + + def extract_tool_calls( + self, + model_output: str, + request: ChatCompletionRequest, + ) -> ExtractedToolCallInformation: + matched_tool_calls = self.func_call_regex.findall(model_output) + logger.debug("model_output: %s", model_output) + try: + tool_calls: list[ToolCall] = [] + for match in matched_tool_calls: + tc_detail = self.func_detail_regex.search(match) + if not tc_detail: + logger.warning( + "Failed to parse tool call details from: %s", + match, + ) + continue + tc_name = tc_detail.group(1).strip() + tc_args = tc_detail.group(2) + pairs = self.func_arg_regex.findall(tc_args) if tc_args else [] + arg_dct: dict[str, Any] = {} + for key, value in pairs: + arg_key = key.strip() + arg_val = value.strip() + if not self._is_string_type(tc_name, arg_key, request.tools): + arg_val = self._deserialize(arg_val) + logger.debug("arg_key = %s, arg_val = %s", arg_key, arg_val) + arg_dct[arg_key] = arg_val + tool_calls.append( + ToolCall( + type="function", + function=FunctionCall( + name=tc_name, + arguments=json.dumps(arg_dct, ensure_ascii=False), + ), + ) + ) + except Exception: + logger.exception("Failed to extract tool call spec") + return ExtractedToolCallInformation( + tools_called=False, tool_calls=[], content=model_output + ) + else: + if len(tool_calls) > 0: + content: str | None = model_output[ + : model_output.find(self.tool_calls_start_token) + ] + # Normalize empty/whitespace-only content to None + if not content or not content.strip(): + content = None + return ExtractedToolCallInformation( + tools_called=True, tool_calls=tool_calls, content=content + ) + return ExtractedToolCallInformation( + tools_called=False, tool_calls=[], content=model_output + ) + + def extract_tool_calls_streaming( + self, + previous_text: str, + current_text: str, + delta_text: str, + previous_token_ids: Sequence[int], + current_token_ids: Sequence[int], + delta_token_ids: Sequence[int], + request: ChatCompletionRequest, + ) -> DeltaMessage | None: + if not self._tools_enabled(request): + return DeltaMessage(content=delta_text) if delta_text else None + + self._buffer += delta_text + + pending_deltas: dict[int, DeltaToolCall] = {} + content: str | None = None + + while True: + if not self._in_tool_call: + start_idx = self._buffer.find(self.tool_call_start_token) + if start_idx == -1: + # Check for partial start token at end of buffer + for i in range(1, len(self.tool_call_start_token)): + if self._buffer.endswith(self.tool_call_start_token[:i]): + out = self._buffer[:-i] + self._buffer = self._buffer[-i:] + if out: + content = (content or "") + out + break + else: + out = self._buffer + self._buffer = "" + if out: + content = (content or "") + out + break + + if start_idx > 0: + content = (content or "") + self._buffer[:start_idx] + self._buffer = self._buffer[start_idx:] + + self._buffer = self._buffer[len(self.tool_call_start_token) :] + self._begin_tool_call() + continue + + # Parse tool name first + if not self.current_tool_name_sent: + nl = self._buffer.find("\n") + ak = self._buffer.find(self.arg_key_start) + end = self._buffer.find(self.tool_call_end_token) + candidates = [i for i in [nl, ak, end] if i != -1] + if not candidates: + break + cut = min(candidates) + tool_name = self._buffer[:cut].strip() + if tool_name == "" and cut == end: + # Handle empty tool call like ``. + # Consume the tokens and reset state to avoid infinite loop. + self._buffer = self._buffer[end + len(self.tool_call_end_token) :] + self._finish_tool_call() + self._revert_last_tool_call_state() + continue + + if cut == nl: + self._buffer = self._buffer[nl + 1 :] + else: + self._buffer = self._buffer[cut:] + + self._current_tool_name = tool_name + self.current_tool_name_sent = True + self._update_tool_name(pending_deltas, tool_name) + continue + + assert self._current_tool_name is not None + + # Handle incremental string value streaming + if self._streaming_string_value: + val_end = self._buffer.find(self.arg_val_end) + if val_end != -1: + raw_content = self._buffer[:val_end] + self._buffer = self._buffer[val_end + len(self.arg_val_end) :] + self._streaming_string_value = False + self._pending_key = None + + escaped = self._json_escape_string_content(raw_content) + frag = escaped + '"' + self.streamed_args_for_tool[self.current_tool_id] += frag + self._update_tool_args(pending_deltas, frag) + continue + + # Check for partial at end + safe_len = len(self._buffer) + for i in range(1, len(self.arg_val_end)): + if self._buffer.endswith(self.arg_val_end[:i]): + safe_len = len(self._buffer) - i + break + + if safe_len > 0: + to_emit = self._buffer[:safe_len] + self._buffer = self._buffer[safe_len:] + escaped = self._json_escape_string_content(to_emit) + if escaped: + self.streamed_args_for_tool[self.current_tool_id] += escaped + self._update_tool_args(pending_deltas, escaped) + break + + # If we have a pending key, parse its value + if self._pending_key is not None: + val_pos = self._buffer.find(self.arg_val_start) + if val_pos == -1: + break + if val_pos > 0: + self._buffer = self._buffer[val_pos:] + + key = (self._pending_key or "").strip() + + is_string = self._is_string_type( + self._current_tool_name, key, request.tools + ) + + if is_string: + # String type: stream incrementally + self._buffer = self._buffer[len(self.arg_val_start) :] + + if key in self._seen_keys[self.current_tool_id]: + self._pending_key = None + continue + + self._seen_keys[self.current_tool_id].add(key) + key_json = json.dumps(key, ensure_ascii=False) + + if not self._args_started[self.current_tool_id]: + frag = "{" + key_json + ': "' + self._args_started[self.current_tool_id] = True + else: + frag = ", " + key_json + ': "' + + self.streamed_args_for_tool[self.current_tool_id] += frag + self._streaming_string_value = True + self._update_tool_args(pending_deltas, frag) + continue + + # Non-string type: wait for complete value + val_end = self._buffer.find(self.arg_val_end) + if val_end == -1: + break + + raw_val = self._buffer[len(self.arg_val_start) : val_end].strip() + self._buffer = self._buffer[val_end + len(self.arg_val_end) :] + self._pending_key = None + + frag_or_none = self._append_arg_fragment(key=key, raw_val=raw_val) + if frag_or_none: + self._update_tool_args(pending_deltas, frag_or_none) + continue + + # Parse next arg or close + end_pos = self._buffer.find(self.tool_call_end_token) + key_pos = self._buffer.find(self.arg_key_start) + if end_pos != -1 and (key_pos == -1 or end_pos < key_pos): + self._buffer = self._buffer[end_pos + len(self.tool_call_end_token) :] + frag_or_none = self._close_args_if_needed() + # Finalize prev_tool_call_arr with complete parsed arguments + if self._current_tool_name: + try: + full_args_str = self.streamed_args_for_tool[ + self.current_tool_id + ] + args_dict = json.loads(full_args_str) + self.prev_tool_call_arr[self.current_tool_id] = { + "name": self._current_tool_name, + "arguments": args_dict, + } + except (json.JSONDecodeError, IndexError) as e: + logger.warning( + "Failed to finalize tool call state for tool %d: %s", + self.current_tool_id, + e, + ) + self._finish_tool_call() + if frag_or_none: + self._update_tool_args(pending_deltas, frag_or_none) + continue + + if key_pos == -1: + break + if key_pos > 0: + self._buffer = self._buffer[key_pos:] + key_end = self._buffer.find(self.arg_key_end) + if key_end == -1: + break + key = self._buffer[len(self.arg_key_start) : key_end] + self._buffer = self._buffer[key_end + len(self.arg_key_end) :] + self._pending_key = key + continue + + tool_calls = list(pending_deltas.values()) + if content is None and len(tool_calls) == 0: + if request.logprobs: + return DeltaMessage(content="") + return None + return DeltaMessage(content=content, tool_calls=tool_calls) + + def _ensure_tool_state(self) -> None: + while len(self._tool_call_ids) <= self.current_tool_id: + self._tool_call_ids.append( + make_tool_call_id(id_type="random", func_name=None, idx=None) + ) + while len(self.streamed_args_for_tool) <= self.current_tool_id: + self.streamed_args_for_tool.append("") + while len(self.prev_tool_call_arr) <= self.current_tool_id: + self.prev_tool_call_arr.append({}) + while len(self._args_started) <= self.current_tool_id: + self._args_started.append(False) + while len(self._args_closed) <= self.current_tool_id: + self._args_closed.append(False) + while len(self._seen_keys) <= self.current_tool_id: + self._seen_keys.append(set()) + + def _begin_tool_call(self) -> None: + if self.current_tool_id == -1: + self.current_tool_id = 0 + else: + self.current_tool_id += 1 + self._ensure_tool_state() + self.current_tool_name_sent = False + self._current_tool_name = None + self._pending_key = None + self._streaming_string_value = False + self._in_tool_call = True + + def _finish_tool_call(self) -> None: + self._in_tool_call = False + self._current_tool_name = None + self._pending_key = None + self._streaming_string_value = False + + def _revert_last_tool_call_state(self) -> None: + """Revert the state allocation for the last tool call.""" + if self.current_tool_id < 0: + return + self._tool_call_ids.pop() + self.streamed_args_for_tool.pop() + self.prev_tool_call_arr.pop() + self._args_started.pop() + self._args_closed.pop() + self._seen_keys.pop() + self.current_tool_id -= 1 + + def _get_or_create_delta(self, pending: dict[int, DeltaToolCall]) -> DeltaToolCall: + idx = self.current_tool_id + if idx not in pending: + pending[idx] = DeltaToolCall( + index=idx, + function=DeltaFunctionCall(), + ) + delta = pending[idx] + assert delta.function is not None + return delta + + def _update_tool_name( + self, pending: dict[int, DeltaToolCall], tool_name: str + ) -> None: + self.prev_tool_call_arr[self.current_tool_id] = { + "name": self._current_tool_name, + "arguments": {}, + } + delta = self._get_or_create_delta(pending) + delta.id = self._tool_call_ids[self.current_tool_id] + delta.type = "function" + assert delta.function is not None + delta.function.name = tool_name + if delta.function.arguments is None: + delta.function.arguments = "" + + @staticmethod + def _complete_json_prefix( + json_prefix: str, + allowed_partial_types: Allow, + ) -> dict | None: + """Complete a partial JSON prefix into a valid JSON object. + + Returns (formatted_prefix, parsed_dict) or None on failure. + + Note: ``partial_json_parser`` strips trailing whitespace before + parsing (``complete.py:20``), which means the returned slice is + shorter than ``json_prefix`` when it has trailing whitespace. + Since the parser controls the construction of the json_prefix value, + this code relies on it being a valid prefix and we only use the fix for + the completion of the JSON object. + """ + try: + _, partial_str_completion = partial_json_parser.core.complete.fix( + json_prefix, + allowed_partial_types, + ) + return json.loads(json_prefix + partial_str_completion) + except Exception: + return None + + def _update_tool_args( + self, pending: dict[int, DeltaToolCall], fragment: str + ) -> None: + result = self._complete_json_prefix( + self.streamed_args_for_tool[self.current_tool_id], + Allow.ALL, + ) + if result is not None: + self.prev_tool_call_arr[self.current_tool_id]["arguments"] = result + delta = self._get_or_create_delta(pending) + assert delta.function is not None + if delta.function.arguments is None: + delta.function.arguments = "" + delta.function.arguments += fragment + + def _append_arg_fragment( + self, + *, + key: str, + raw_val: str, + ) -> str | None: + key = key.strip() + if not key: + return None + if key in self._seen_keys[self.current_tool_id]: + return None + + # This function is only called for non-string types (already checked + # by _is_string_type in the caller), so we always deserialize. + val_obj: Any = self._deserialize(raw_val) + + key_json = json.dumps(key, ensure_ascii=False) + val_json = json.dumps(val_obj, ensure_ascii=False) + + if not self._args_started[self.current_tool_id]: + fragment = "{" + key_json + ": " + val_json + self._args_started[self.current_tool_id] = True + else: + fragment = ", " + key_json + ": " + val_json + + self._seen_keys[self.current_tool_id].add(key) + self.streamed_args_for_tool[self.current_tool_id] += fragment + return fragment + + def _close_args_if_needed(self) -> str | None: + if self._args_closed[self.current_tool_id]: + return None + self._args_closed[self.current_tool_id] = True + if not self._args_started[self.current_tool_id]: + fragment = "{}" + self.streamed_args_for_tool[self.current_tool_id] = fragment + else: + fragment = "}" + self.streamed_args_for_tool[self.current_tool_id] += fragment + return fragment diff --git a/vllm/transformers_utils/config.py b/vllm/transformers_utils/config.py index bb6ad1056b7b..47b74093b06c 100644 --- a/vllm/transformers_utils/config.py +++ b/vllm/transformers_utils/config.py @@ -127,6 +127,7 @@ def __getitem__(self, key): qwen3_next="Qwen3NextConfig", qwen3_5="Qwen3_5Config", qwen3_5_moe="Qwen3_5MoeConfig", + laguna="LagunaConfig", lfm2_moe="Lfm2MoeConfig", tarsier2="Tarsier2Config", ) @@ -409,22 +410,33 @@ def patch_rope_parameters(config: PretrainedConfig) -> None: ompe = getattr(config, "original_max_position_embeddings", None) if Version(version("transformers")) < Version("5.0.0"): - # Transformers v4 installed, legacy config fields may be present - if (rope_scaling := getattr(config, "rope_scaling", None)) is not None: - config.rope_parameters = rope_scaling - if ( - rope_theta is not None - or partial_rotary_factor is not None - or ompe is not None - ) and not getattr(config, "rope_parameters", None): - config.rope_parameters = {"rope_type": "default"} - # Patch legacy fields into rope_parameters - if rope_theta is not None: - config.rope_parameters["rope_theta"] = rope_theta - if partial_rotary_factor is not None: - config.rope_parameters["partial_rotary_factor"] = partial_rotary_factor - if ompe is not None: - config.rope_parameters["original_max_position_embeddings"] = ompe + # Transformers v4 installed, legacy config fields may be present. + existing_rp = getattr(config, "rope_parameters", None) + if isinstance(existing_rp, dict) and is_rope_parameters_nested(existing_rp): + # Interleaved-attention models (e.g. Laguna-XS.2) ship a nested + # {layer_type: {...}} rope_parameters that the model code indexes + # by layer_type. The per-layer-type sub-dicts already carry the + # correct rope_theta / partial_rotary_factor / ompe (the converter + # places top-level legacy fields inside full_attention), so don't + # merge top-level fields here — that would shadow the per-type + # values and break sliding-attention layers. + pass + else: + if (rope_scaling := getattr(config, "rope_scaling", None)) is not None: + config.rope_parameters = rope_scaling + if ( + rope_theta is not None + or partial_rotary_factor is not None + or ompe is not None + ) and not getattr(config, "rope_parameters", None): + config.rope_parameters = {"rope_type": "default"} + # Patch legacy fields into rope_parameters + if rope_theta is not None: + config.rope_parameters["rope_theta"] = rope_theta + if partial_rotary_factor is not None: + config.rope_parameters["partial_rotary_factor"] = partial_rotary_factor + if ompe is not None: + config.rope_parameters["original_max_position_embeddings"] = ompe elif rope_theta is not None or getattr(config, "rope_parameters", None): # Transformers v5 installed # Patch these fields in case they used non-standard names diff --git a/vllm/transformers_utils/configs/__init__.py b/vllm/transformers_utils/configs/__init__.py index 667ed5a2596c..8c4d01a428bd 100644 --- a/vllm/transformers_utils/configs/__init__.py +++ b/vllm/transformers_utils/configs/__init__.py @@ -45,6 +45,7 @@ # `FalconConfig` class from the official HuggingFace transformers library. "RWConfig": "vllm.transformers_utils.configs.falcon", "JAISConfig": "vllm.transformers_utils.configs.jais", + "LagunaConfig": "vllm.transformers_utils.configs.laguna", "Lfm2MoeConfig": "vllm.transformers_utils.configs.lfm2_moe", "MedusaConfig": "vllm.transformers_utils.configs.medusa", "MiDashengLMConfig": "vllm.transformers_utils.configs.midashenglm", @@ -105,6 +106,7 @@ "IsaacConfig", "RWConfig", "JAISConfig", + "LagunaConfig", "Lfm2MoeConfig", "MedusaConfig", "MiDashengLMConfig", diff --git a/vllm/transformers_utils/configs/laguna.py b/vllm/transformers_utils/configs/laguna.py new file mode 100644 index 000000000000..2702d3af5aa1 --- /dev/null +++ b/vllm/transformers_utils/configs/laguna.py @@ -0,0 +1,120 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from transformers.configuration_utils import PretrainedConfig + + +class LagunaConfig(PretrainedConfig): + model_type = "laguna" + keys_to_ignore_at_inference = ["past_key_values"] + base_model_tp_plan = { + "layers.*.self_attn.q_proj": "colwise", + "layers.*.self_attn.k_proj": "colwise", + "layers.*.self_attn.v_proj": "colwise", + "layers.*.self_attn.g_proj": "colwise", + "layers.*.self_attn.o_proj": "rowwise", + "layers.*.mlp.gate_proj": "colwise", + "layers.*.mlp.up_proj": "colwise", + "layers.*.mlp.down_proj": "rowwise", + } + base_model_pp_plan = { + "embed_tokens": (["input_ids"], ["inputs_embeds"]), + "layers": (["hidden_states", "attention_mask"], ["hidden_states"]), + "norm": (["hidden_states"], ["hidden_states"]), + } + + def __init__( + self, + vocab_size: int = 100352, + hidden_size: int = 2048, + intermediate_size: int = 8192, + num_hidden_layers: int = 40, + num_attention_heads: int = 48, + num_key_value_heads: int = 8, + head_dim: int = 128, + qkv_bias: bool = False, + attention_bias: bool = False, + gating: bool | str = True, + hidden_act: str = "silu", + max_position_embeddings: int = 131072, + initializer_range: float = 0.02, + rms_norm_eps: float = 1e-6, + use_cache: bool = True, + tie_word_embeddings: bool = False, + rope_theta: float = 500000.0, + rope_scaling: dict | None = None, + rope_parameters: dict | None = None, + partial_rotary_factor: float = 1.0, + attention_dropout: float = 0.0, + sliding_window: int | None = None, + layer_types: list[str] | None = None, + swa_attention_sink_enabled: bool = False, + swa_rope_parameters: dict | None = None, + num_attention_heads_per_layer: list[int] | None = None, + num_experts: int = 256, + num_experts_per_tok: int = 8, + moe_intermediate_size: int = 512, + shared_expert_intermediate_size: int = 512, + norm_topk_prob: bool = True, + decoder_sparse_step: int = 1, + mlp_only_layers: list[int] | None = None, + router_aux_loss_coef: float = 0.001, + output_router_logits: bool = False, + moe_routed_scaling_factor: float = 1.0, + moe_apply_router_weight_on_input: bool = False, + **kwargs, + ): + if mlp_only_layers is None: + mlp_only_layers = [0] + + # Accept either v4-style (rope_theta + rope_scaling) or v5-style + # (rope_parameters). Translate v5 → v4 so downstream code has one path. + if rope_parameters is not None: + rp = dict(rope_parameters) + rope_theta = float(rp.pop("rope_theta", rope_theta)) + rt = rp.pop("rope_type", None) + if rt is not None and rt != "default": + rope_scaling = {"rope_type": rt, **rp} + elif rp and rope_scaling is None: + rope_scaling = {"rope_type": "default", **rp} + + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.head_dim = head_dim + self.qkv_bias = qkv_bias + self.attention_bias = attention_bias + self.gating = gating + self.hidden_act = hidden_act + self.max_position_embeddings = max_position_embeddings + self.initializer_range = initializer_range + self.rms_norm_eps = rms_norm_eps + self.use_cache = use_cache + self.rope_theta = rope_theta + self.rope_scaling = rope_scaling + self.partial_rotary_factor = partial_rotary_factor + self.attention_dropout = attention_dropout + self.sliding_window = sliding_window + self.layer_types = layer_types + self.swa_attention_sink_enabled = swa_attention_sink_enabled + self.swa_rope_parameters = swa_rope_parameters + self.num_attention_heads_per_layer = num_attention_heads_per_layer + self.num_experts = num_experts + self.num_experts_per_tok = num_experts_per_tok + self.moe_intermediate_size = moe_intermediate_size + self.shared_expert_intermediate_size = shared_expert_intermediate_size + self.norm_topk_prob = norm_topk_prob + self.decoder_sparse_step = decoder_sparse_step + self.mlp_only_layers = mlp_only_layers + self.router_aux_loss_coef = router_aux_loss_coef + self.output_router_logits = output_router_logits + self.moe_routed_scaling_factor = moe_routed_scaling_factor + self.moe_apply_router_weight_on_input = moe_apply_router_weight_on_input + + super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs) + + +__all__ = ["LagunaConfig"] From de3fe8dc62f3d77eb8dab8125ca90436f606bccb Mon Sep 17 00:00:00 2001 From: yangrz <37785043+yangrz7@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:38:43 +0800 Subject: [PATCH 012/237] [Bugfix] release KV blocks for skipped P-ranks to prevent invalid KV errors and timeouts when P_tp > D_tp and MLA (#40449) Signed-off-by: yangruize Co-authored-by: Roger Wang --- .../kv_connector/unit/test_nixl_connector.py | 119 ++++++++++++++++++ .../kv_connector/v1/nixl/worker.py | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/tests/v1/kv_connector/unit/test_nixl_connector.py b/tests/v1/kv_connector/unit/test_nixl_connector.py index 50e83aa2ef20..fb4b641e1376 100644 --- a/tests/v1/kv_connector/unit/test_nixl_connector.py +++ b/tests/v1/kv_connector/unit/test_nixl_connector.py @@ -2479,3 +2479,122 @@ def test_handshake_decode_errors(default_vllm_config, dist_init, error_scenario) remote_tp_size=1, expected_engine_id=FakeNixlConnectorWorker.REMOTE_ENGINE_ID, ) + + @patch( + "vllm.distributed.kv_transfer.kv_connector.v1.nixl.worker.NixlWrapper", + FakeNixlWrapper, + ) + def test_mla_broadcast_notif_uses_remote_request_id( + self, default_vllm_config, dist_init + ): + """MLA + remote TP > local TP: the broadcast notification sent to + non-read prefill ranks must be keyed by the prefill-side request + id (``meta.remote.request_id``), not the local decode request id. + + Prefill ranks key ``_reqs_to_send`` by their own request id, so a + broadcast keyed by the decode id is rejected in + ``_get_new_notifs`` with "Potentially invalid KV blocks for + unrecognized request" and the blocks only release via the abort + timeout. See ``_read_blocks_for_req`` in + ``vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py``. + """ + decode_tp_size = 1 + prefill_tp_size = 4 + + vllm_config = create_vllm_config() + vllm_config.parallel_config.tensor_parallel_size = decode_tp_size + + connector = NixlConnector( + vllm_config, KVConnectorRole.WORKER, make_kv_cache_config(block_size=16) + ) + connector.connector_worker = FakeNixlConnectorWorker( + vllm_config, connector.engine_id, hand_shake_latency=0 + ) + worker = connector.connector_worker + + # Force the MLA path; only `self.use_mla` gates the branches we + # exercise inside `_read_blocks_for_req`. + worker.use_mla = True + + # Manually register the remote (P) engine and pre-populate the + # per-rank state the handshake would normally fill in. The real + # `_nixl_handshake` is unnecessary here — we only need + # `transfer_topo` to know `remote_tp_size`, and `_remote_agents` + # / `dst_xfer_side_handles` to be keyed by remote rank. + remote_engine_id = "remote_engine" + worker.transfer_topo.register_remote_engine( + remote_engine_id=remote_engine_id, + remote_tp_size=prefill_tp_size, + remote_block_size=worker.block_size, + remote_block_len=worker.block_size * 4096, + remote_physical_blocks_per_logical=1, + local_block_len=worker.block_size * 4096, + ) + worker._remote_agents[remote_engine_id] = { + rank: f"agent_p{rank}" for rank in range(prefill_tp_size) + } + worker.dst_xfer_side_handles = { + remote_engine_id: {rank: 100 + rank for rank in range(prefill_tp_size)} + } + # Sanity: D TP=1, P TP=4 => tp_ratio = -4 (P > D). + assert worker.transfer_topo.tp_ratio(prefill_tp_size) == -prefill_tp_size + + # Distinct ids on each side — that's the whole point of the bug. + decode_req_id = "decode-req-AAAA" + prefill_req_id = "prefill-req-BBBB" + assert decode_req_id != prefill_req_id + + metadata = NixlConnectorMetadata() + metadata.add_new_req_to_recv( + request_id=decode_req_id, + local_block_ids=([0, 1, 2],), + kv_transfer_params={ + "remote_block_ids": ([10, 11, 12],), + "remote_engine_id": remote_engine_id, + "remote_request_id": prefill_req_id, + "remote_host": "localhost", + "remote_port": 1234, + "remote_tp_size": prefill_tp_size, + }, + ) + meta = metadata.reqs_to_recv[decode_req_id] + + # Capture broadcast send_notif calls; stub `_read_blocks` so we + # don't need a working xfer path. Real `_read_blocks` emits its + # auto-notif via `make_prepped_xfer`, not via `send_notif`, so + # any captured `send_notif` here is a broadcast. + send_notif_calls: list[tuple[str, bytes]] = [] + worker.nixl_wrapper.send_notif = ( # type: ignore[method-assign] + lambda agent_name, notif_msg: send_notif_calls.append( + (agent_name, notif_msg) + ) + ) + worker._read_blocks = MagicMock() # type: ignore[method-assign] + + worker._read_blocks_for_req(decode_req_id, meta) + + # MLA: read once from rank 0 and broadcast to the other ranks. + worker._read_blocks.assert_called_once() + assert worker._read_blocks.call_args.kwargs["remote_rank"] == 0 + assert ( + worker._read_blocks.call_args.kwargs["remote_request_id"] == prefill_req_id + ) + + # Broadcast goes to ranks {1, 2, 3} only, never to the read target. + expected_recipients = { + worker._remote_agents[remote_engine_id][r] + for r in range(1, prefill_tp_size) + } + assert {agent for agent, _ in send_notif_calls} == expected_recipients + + # Every broadcast notif must be keyed by the prefill request id. + # Pre-fix this used the *decode* request id, which prefill ranks + # didn't recognize. + expected_notif = f"{prefill_req_id}:{decode_tp_size}".encode() + bad_notif = f"{decode_req_id}:{decode_tp_size}".encode() + for agent, notif in send_notif_calls: + assert notif == expected_notif, ( + f"Broadcast notif to {agent!r} must use prefill_req_id; " + f"got {notif!r} (expected {expected_notif!r}, " + f"buggy form would be {bad_notif!r})" + ) diff --git a/vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py b/vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py index bd7ef5973f62..607bf4b988ff 100644 --- a/vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py +++ b/vllm/distributed/kv_transfer/kv_connector/v1/nixl/worker.py @@ -1971,7 +1971,7 @@ def _read_blocks_for_req(self, req_id: str, meta: ReqMeta): if self.use_mla and tp_ratio < 0: # ..but we still need to notify the other remote ranks that we # have the blocks we need so they can update the request state. - notif_id = f"{req_id}:{self.world_size}".encode() + notif_id = f"{meta.remote.request_id}:{self.world_size}".encode() remote_agents = self._remote_agents[meta.remote.engine_id] for rank_to_notify, agent in remote_agents.items(): if rank_to_notify != remote_rank: From e9f8f31e9a4c31d6842ca1adffe2619ed204fafb Mon Sep 17 00:00:00 2001 From: Julien Denize <40604584+juliendenize@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:22:20 +0200 Subject: [PATCH 013/237] [FEATURE] Add EagleMistralForCausalLM (#41024) Signed-off-by: juliendenize --- tests/models/registry.py | 5 + vllm/model_executor/models/mistral_eagle.py | 166 ++++++++++++++++++++ vllm/model_executor/models/registry.py | 1 + 3 files changed, 172 insertions(+) create mode 100644 vllm/model_executor/models/mistral_eagle.py diff --git a/tests/models/registry.py b/tests/models/registry.py index 19304d803160..3a8244238170 100644 --- a/tests/models/registry.py +++ b/tests/models/registry.py @@ -1464,6 +1464,11 @@ def check_available_online( speculative_model="yuhuili/EAGLE3-LLaMA3.1-Instruct-8B", tokenizer="MiniMaxAI/MiniMax-M2", ), + "EagleMistralForCausalLM": _HfExamplesInfo( + "mistralai/Mistral-Medium-3.5-128B", + speculative_model="mistralai/Mistral-Medium-3.5-128B-EAGLE", + is_available_online=False, + ), "EagleMistralLarge3ForCausalLM": _HfExamplesInfo( "mistralai/Mistral-Large-3-675B-Instruct-2512", speculative_model="mistralai/Mistral-Large-3-675B-Instruct-2512-Eagle", diff --git a/vllm/model_executor/models/mistral_eagle.py b/vllm/model_executor/models/mistral_eagle.py new file mode 100644 index 000000000000..908b50f7ca00 --- /dev/null +++ b/vllm/model_executor/models/mistral_eagle.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from collections.abc import Iterable + +import torch +import torch.nn as nn + +from vllm.compilation.decorators import support_torch_compile +from vllm.config import VllmConfig +from vllm.logger import init_logger +from vllm.model_executor.layers.layernorm import RMSNorm +from vllm.model_executor.layers.linear import RowParallelLinear +from vllm.model_executor.layers.logits_processor import LogitsProcessor +from vllm.model_executor.layers.quantization.base_config import QuantizationConfig +from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding +from vllm.model_executor.models.interfaces import MultiModalEmbeddings +from vllm.model_executor.models.llama import LlamaConfig +from vllm.model_executor.models.mistral import ( + MistralDecoderLayer, + MistralForCausalLM, + MistralModel, +) +from vllm.model_executor.models.utils import ( + _merge_multimodal_embeddings, + get_draft_quant_config, + maybe_prefix, +) + +logger = init_logger(__name__) + + +class EagleMistralDecoderLayer(MistralDecoderLayer): + def __init__( + self, + vllm_config: VllmConfig, + prefix: str = "", + config: LlamaConfig | None = None, + ) -> None: + super().__init__(vllm_config, prefix=prefix, config=config) + + def get_quant_config(self, vllm_config: VllmConfig) -> QuantizationConfig | None: + return get_draft_quant_config(vllm_config) + + +@support_torch_compile +class EagleMistralModel(MistralModel): + def __init__( + self, + *, + vllm_config: VllmConfig, + prefix: str = "", + start_layer_id: int = 0, + ) -> None: + # Bypass MistralModel.__init__ to avoid creating duplicate attention + # layer entries in the global context. + nn.Module.__init__(self) + self.config = vllm_config.speculative_config.draft_model_config.hf_config + self.vocab_size = self.config.vocab_size + # Get drafter's quantization config + self.quant_config = get_draft_quant_config(vllm_config) + + self.embed_tokens = VocabParallelEmbedding( + self.config.vocab_size, + self.config.hidden_size, + prefix=maybe_prefix(prefix, "embed_tokens"), + quant_config=self.quant_config, + ) + + self.layers = nn.ModuleList( + [ + EagleMistralDecoderLayer( + vllm_config, + prefix=maybe_prefix(prefix, f"layers.{i + start_layer_id}"), + config=self.config, + ) + for i in range(self.config.num_hidden_layers) + ] + ) + self.fc = RowParallelLinear( + self.config.hidden_size * 2, + self.config.hidden_size, + bias=False, + input_is_parallel=False, + quant_config=self.quant_config, + prefix=maybe_prefix(prefix, "fc"), + return_bias=False, + ) + self.norm = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps) + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + hidden_states: torch.Tensor, + inputs_embeds: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + if inputs_embeds is None: + inputs_embeds = self.embed_input_ids(input_ids) + hidden_states = self.fc(torch.cat((inputs_embeds, hidden_states), dim=-1)) + residual = None + for layer in self.layers: + hidden_states, residual = layer( + positions, + hidden_states, + residual, + ) + hidden_states, _ = self.norm(hidden_states, residual) + return hidden_states, hidden_states + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + # Pretend embed_tokens is loaded; the actual weight is shared + # from the target model at runtime by `load_eagle_model`. + return super().load_weights(weights) | {"embed_tokens.weight"} + + +class EagleMistralForCausalLM(MistralForCausalLM): + mistral_mapping = MistralForCausalLM.mistral_mapping | { + "eagle_linear": "model.fc", + } + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None: + # Bypass MistralForCausalLM.__init__ to use the draft model config + # and to avoid creating an lm_head. + nn.Module.__init__(self) + self.config = vllm_config.speculative_config.draft_model_config.hf_config + target_layer_num = vllm_config.model_config.get_num_layers( + vllm_config.parallel_config + ) + self.model = EagleMistralModel( + vllm_config=vllm_config, prefix="model", start_layer_id=target_layer_num + ) + + logit_scale = getattr(self.config, "logit_scale", 1.0) + self.logits_processor = LogitsProcessor( + self.config.vocab_size, scale=logit_scale + ) + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + hidden_states: torch.Tensor, + inputs_embeds: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + return self.model(input_ids, positions, hidden_states, inputs_embeds) + + def embed_input_ids( + self, + input_ids: torch.Tensor, + multimodal_embeddings: MultiModalEmbeddings | None = None, + *, + is_multimodal: torch.Tensor | None = None, + ) -> torch.Tensor: + inputs_embeds = super().embed_input_ids(input_ids) + + if multimodal_embeddings is None or len(multimodal_embeddings) == 0: + return inputs_embeds + + assert is_multimodal is not None + + return _merge_multimodal_embeddings( + inputs_embeds=inputs_embeds, + multimodal_embeddings=multimodal_embeddings, + is_multimodal=is_multimodal, + ) diff --git a/vllm/model_executor/models/registry.py b/vllm/model_executor/models/registry.py index eba288dcc77a..4369d715e640 100644 --- a/vllm/model_executor/models/registry.py +++ b/vllm/model_executor/models/registry.py @@ -584,6 +584,7 @@ "LlamaForCausalLMEagle3": ("llama_eagle3", "Eagle3LlamaForCausalLM"), "Eagle3Qwen2_5vlForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"), "Eagle3Qwen3vlForCausalLM": ("llama_eagle3", "Eagle3LlamaForCausalLM"), + "EagleMistralForCausalLM": ("mistral_eagle", "EagleMistralForCausalLM"), "EagleMistralLarge3ForCausalLM": ( "mistral_large_3_eagle", "EagleMistralLarge3ForCausalLM", From f05f3664c35804bf2b5b64eecd17ddfdbb8ed5e3 Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Tue, 28 Apr 2026 17:53:19 -0400 Subject: [PATCH 014/237] [Doc] Add missing API endpoints to security documentation (#40532) Signed-off-by: Russell Bryant --- docs/usage/security.md | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/usage/security.md b/docs/usage/security.md index 4879ddbf64ef..300cabbfcc19 100644 --- a/docs/usage/security.md +++ b/docs/usage/security.md @@ -138,14 +138,22 @@ When `--api-key` is configured, the following `/v1` endpoints require Bearer tok - `/v1/models` - List available models - `/v1/chat/completions` - Chat completions +- `/v1/chat/completions/batch` - Batch chat completions +- `/v1/chat/completions/render` - Render chat completion requests - `/v1/completions` - Text completions +- `/v1/completions/render` - Render completion requests - `/v1/embeddings` - Generate embeddings - `/v1/audio/transcriptions` - Audio transcription - `/v1/audio/translations` - Audio translation - `/v1/messages` - Anthropic-compatible messages API -- `/v1/responses` - Response management +- `/v1/messages/count_tokens` - Count tokens for Anthropic messages +- `/v1/responses` - Create a response +- `/v1/responses/{response_id}` - Retrieve a response +- `/v1/responses/{response_id}/cancel` - Cancel a response - `/v1/score` - Scoring API - `/v1/rerank` - Reranking API +- `/v1/load_lora_adapter` - Load a LoRA adapter (can alter model behavior; only available when `--enable-lora` is set and `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True`) +- `/v1/unload_lora_adapter` - Unload a LoRA adapter (can alter model behavior; only available when `--enable-lora` is set and `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True`) ### Unprotected Endpoints (No API Key Required) @@ -155,16 +163,23 @@ The following endpoints **do not require authentication** even when `--api-key` - `/invocations` - SageMaker-compatible endpoint (routes to the same inference functions as `/v1` endpoints) - `/inference/v1/generate` - Generate completions +- `/generative_scoring` - Generative scoring API - `/pooling` - Pooling API - `/classify` - Classification API - `/score` - Scoring API (non-`/v1` variant) - `/rerank` - Reranking API (non-`/v1` variant) -**Operational control endpoints (always enabled):** +**Operational control endpoints (only when `"generate"` task is supported):** - `/pause` - Pause generation (causes denial of service) - `/resume` - Resume generation +- `/is_paused` - Check if generation is paused - `/scale_elastic_ep` - Trigger scaling operations +- `/is_scaling_elastic_ep` - Check if scaling is in progress +- `/init_weight_transfer_engine` - Initialize weight transfer engine for RLHF +- `/update_weights` - Update model weights (can alter model behavior) +- `/get_world_size` - Get distributed world size +- `/abort_requests` - Abort in-flight requests (only when `--tokens-only` is also set) **Utility endpoints:** @@ -207,9 +222,9 @@ These endpoints are only available when profiling is enabled and should only be An attacker who can reach the vLLM HTTP server can: -1. **Bypass authentication** by using non-`/v1` endpoints like `/invocations`, `/inference/v1/generate`, `/pooling`, `/classify`, `/score`, or `/rerank` to run arbitrary inference without credentials -2. **Cause denial of service** by calling `/pause` or `/scale_elastic_ep` without a token -3. **Access operational controls** to manipulate server state (e.g., pausing generation) +1. **Bypass authentication** by using non-`/v1` endpoints like `/invocations`, `/inference/v1/generate`, `/generative_scoring`, `/pooling`, `/classify`, `/score`, or `/rerank` to run arbitrary inference without credentials +2. **Cause denial of service** by calling `/pause`, `/scale_elastic_ep`, or `/abort_requests` without a token +3. **Access operational controls** to manipulate server state (e.g., pausing generation, updating model weights via `/update_weights`) 4. **If `--enable-tokenizer-info-endpoint` is set:** Access sensitive tokenizer configuration including chat templates, which may reveal prompt engineering strategies or other implementation details 5. **If `VLLM_SERVER_DEV_MODE=1` is set:** Execute arbitrary RPC commands via `/collective_rpc`, reset caches, put the engine to sleep, and access detailed server configuration @@ -288,6 +303,12 @@ To disable the Python code interpreter specifically, omit `code_interpreter` fro **Consider a custom implementation**: The GPT-OSS Python tool is a reference implementation. For production deployments, consider implementing a custom code execution sandbox with stricter isolation guarantees. See the [GPT-OSS documentation](https://github.com/openai/gpt-oss?tab=readme-ov-file#python) for guidance. +## Dynamic LoRA Loading + +vLLM supports dynamically loading and unloading LoRA adapters at runtime via the `/v1/load_lora_adapter` and `/v1/unload_lora_adapter` API endpoints. This functionality is **not enabled by default** — it requires both `--enable-lora` and the environment variable `VLLM_ALLOW_RUNTIME_LORA_UPDATING=True` to be set. + +**Warning:** Dynamic LoRA loading is not a secure operation and should not be enabled in deployments exposed to untrusted clients. If you must enable dynamic LoRA loading, restrict access to the `/v1/load_lora_adapter` and `/v1/unload_lora_adapter` endpoints to trusted administrators only, using a reverse proxy or network-level access controls. Do not expose these endpoints to end users. For details on configuring LoRA adapters, see the [LoRA Adapters documentation](../features/lora.md). + ## Reporting Security Vulnerabilities If you believe you have found a security vulnerability in vLLM, please report it following the project's security policy. For more information on how to report security issues and the project's security policy, please see the [vLLM Security Policy](https://github.com/vllm-project/vllm/blob/main/SECURITY.md). From e68fa1b90a7bc52510c11fe2edeae11db15f98fc Mon Sep 17 00:00:00 2001 From: Nick Hill Date: Tue, 28 Apr 2026 15:44:09 -0700 Subject: [PATCH 015/237] [Core] Account for `num_gpu_blocks_override` in `max_model_len` checks (#41069) Signed-off-by: Nick Hill --- tests/compile/h100/test_startup.py | 7 +- tests/v1/core/test_kv_cache_utils.py | 48 +++++++ tests/v1/e2e/general/test_async_scheduling.py | 10 +- vllm/v1/core/kv_cache_utils.py | 121 ++++++++++-------- vllm/v1/worker/gpu_model_runner.py | 2 +- 5 files changed, 128 insertions(+), 60 deletions(-) diff --git a/tests/compile/h100/test_startup.py b/tests/compile/h100/test_startup.py index ff4496c2ba6d..78554a3e93da 100644 --- a/tests/compile/h100/test_startup.py +++ b/tests/compile/h100/test_startup.py @@ -34,7 +34,10 @@ def _run_vllm(vllm_runner): mode=CompilationMode.VLLM_COMPILE, cudagraph_mode=CUDAGraphMode.NONE, ), - num_gpu_blocks_override=8, + # Phi-tiny-MoE uses SWA, whose admission cap is `cdiv(L, block_size) + 1` + # at default block_size=16 — i.e. 17 blocks for max_model_len=256. Use + # 32 for headroom. + num_gpu_blocks_override=32, ): pass @@ -190,7 +193,7 @@ def _run_model(vllm_runner, spec: ModelStartupSpec): cudagraph_mode=CUDAGraphMode.NONE, pass_config=PassConfig(fuse_allreduce_rms=False), ), - num_gpu_blocks_override=8, + num_gpu_blocks_override=16, ): pass diff --git a/tests/v1/core/test_kv_cache_utils.py b/tests/v1/core/test_kv_cache_utils.py index cfd03c5f687e..985b97c69ca4 100644 --- a/tests/v1/core/test_kv_cache_utils.py +++ b/tests/v1/core/test_kv_cache_utils.py @@ -2074,6 +2074,54 @@ def test_auto_fit_max_model_len_not_triggered(): assert vllm_config.model_config.max_model_len == 16 +def test_auto_fit_max_model_len_respects_num_gpu_blocks_override(): + """Auto-fit must size max_model_len against the override-clamped pool, not + the raw `available_memory`. Without this, auto-fit could pick a + max_model_len that no longer fits once `num_gpu_blocks_override` is applied. + """ + model_config = ModelConfig(max_model_len=16384) + model_config.original_max_model_len = -1 # request auto-fit + vllm_config = VllmConfig(model_config=model_config) + # Cap the cache to 32 blocks regardless of available memory. + vllm_config.cache_config.num_gpu_blocks_override = 32 + + mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2 + kv_cache_specs = { + "layer_1": new_kv_cache_spec(), # block_size=16 + "layer_2": new_kv_cache_spec(), + } + # Plenty of raw memory (1024 blocks per layer would fit max_model_len=16384). + large_available_memory = mem_per_block_per_layer * 2 * 1024 + + get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory]) + + # 32 blocks * block_size 16 = 512 token slots, so max_model_len must + # auto-fit at or below that. + assert 0 < vllm_config.model_config.max_model_len <= 32 * 16 + + +def test_check_enough_kv_cache_memory_respects_num_gpu_blocks_override(): + """Admission check must use the override-clamped pool size, not raw + `available_memory`. Without this, startup could accept a max_model_len + that does not actually fit in `num_gpu_blocks_override` blocks. + """ + model_config = ModelConfig(max_model_len=16384) + vllm_config = VllmConfig(model_config=model_config) + # 32 blocks is far too small for max_model_len=16384 (would need 1024). + vllm_config.cache_config.num_gpu_blocks_override = 32 + + mem_per_block_per_layer = 16 * 2 * 64 * 4 * 2 + kv_cache_specs = { + "layer_1": new_kv_cache_spec(), + "layer_2": new_kv_cache_spec(), + } + # Plenty of raw memory: a bytes-only check against this would pass. + large_available_memory = mem_per_block_per_layer * 2 * 1024 + + with pytest.raises(ValueError, match="max seq len"): + get_kv_cache_configs(vllm_config, [kv_cache_specs], [large_available_memory]) + + def test_unify_hybrid_kv_cache_specs(): # 1. has_full_attention and has_sliding_window before_spec_1 = new_kv_cache_spec() diff --git a/tests/v1/e2e/general/test_async_scheduling.py b/tests/v1/e2e/general/test_async_scheduling.py index 8e1eddb0f64e..28a1bedbe0b2 100644 --- a/tests/v1/e2e/general/test_async_scheduling.py +++ b/tests/v1/e2e/general/test_async_scheduling.py @@ -324,10 +324,13 @@ def run_test( ): spec_decoding = spec_config is not None cache_arg: dict[str, Any] = ( - # Force preemptions - dict(num_gpu_blocks_override=32) + # Force preemptions: with 32 blocks the cache holds at most a single + # max-length request, so the ~34 concurrent prompts contend and trigger + # preemption. (Prompts here are << max_model_len, so dropping + # max_model_len from 4096 to 512 doesn't change generation behavior.) + dict(num_gpu_blocks_override=32, max_model_len=512) if test_preemption - else dict(gpu_memory_utilization=0.9) + else dict(gpu_memory_utilization=0.9, max_model_len=4096) ) spec_mml = (spec_config or {}).get("max_model_len") spec_method = (spec_config or {}).get("method", "none") @@ -343,7 +346,6 @@ def run_test( with VllmRunner( model, - max_model_len=4096, enable_chunked_prefill=test_prefill_chunking, # Force prefill chunking max_num_batched_tokens=48 if test_prefill_chunking else None, diff --git a/vllm/v1/core/kv_cache_utils.py b/vllm/v1/core/kv_cache_utils.py index 3e0e7fcb8c5b..b57e10b67faa 100644 --- a/vllm/v1/core/kv_cache_utils.py +++ b/vllm/v1/core/kv_cache_utils.py @@ -890,31 +890,48 @@ def get_max_concurrency_for_kv_cache_config( return max_concurrency -def may_override_num_blocks( - vllm_config: VllmConfig, num_blocks: int, suppress_log: bool = False -) -> int: +def may_override_num_blocks(vllm_config: VllmConfig, num_blocks: int) -> int: """ Override the number of kv cache blocks if `num_gpu_blocks_override` is set. + The override is logged once, at the call site in `get_kv_cache_configs`. """ if vllm_config.cache_config.num_gpu_blocks_override is not None: - num_gpu_blocks_override = vllm_config.cache_config.num_gpu_blocks_override - if not suppress_log: - logger.info( - "Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d", - num_blocks, - num_gpu_blocks_override, - ) - num_blocks = num_gpu_blocks_override - + num_blocks = vllm_config.cache_config.num_gpu_blocks_override return num_blocks +def _pool_bytes_per_block(kv_cache_groups: list[KVCacheGroupSpec]) -> int: + """ + Bytes consumed by one block in the worker's shared KV cache pool, mirroring + the divisor used by `get_kv_cache_config_from_groups` to convert + `available_memory` into `num_blocks`. Used to compute the effective KV cache + capacity once `num_gpu_blocks_override` is applied. + """ + if len(kv_cache_groups) == 1 and isinstance( + kv_cache_groups[0].kv_cache_spec, UniformTypeKVCacheSpecs + ): + return kv_cache_groups[0].kv_cache_spec.page_size_bytes + if all( + isinstance(g.kv_cache_spec, UniformTypeKVCacheSpecs) for g in kv_cache_groups + ): + # DeepseekV4: shared layout sized by the largest per-page-size bucket. + full_mla_spec = cast(UniformTypeKVCacheSpecs, kv_cache_groups[0].kv_cache_spec) + layer_tuple_page_bytes = sum(full_mla_spec.get_page_sizes()) + num_layer_tuples = max( + cast(UniformTypeKVCacheSpecs, g.kv_cache_spec).get_num_layer_tuples() + for g in kv_cache_groups + ) + return layer_tuple_page_bytes * num_layer_tuples + group_size = max(len(g.layer_names) for g in kv_cache_groups) + page_size = get_uniform_page_size([g.kv_cache_spec for g in kv_cache_groups]) + return page_size * group_size + + def get_num_blocks( vllm_config: VllmConfig, num_layers: int, available_memory: int, page_size: int, - suppress_log: bool = False, ) -> int: """ Get the number of kv cache blocks. @@ -924,15 +941,10 @@ def get_num_blocks( num_layers: The number of layers available_memory: Memory available for KV cache in bytes. page_size: The page size of the KV cache. - suppress_log: Whether to suppress override log messages. Used when creating a - temporary/dummy KV cache config, e.g. during CG memory profiling """ num_blocks = int(available_memory // page_size // num_layers) num_blocks = max(num_blocks, 0) - num_blocks = may_override_num_blocks( - vllm_config, num_blocks, suppress_log=suppress_log - ) - return num_blocks + return may_override_num_blocks(vllm_config, num_blocks) def get_uniform_page_size(kv_cache_specs: Iterable[KVCacheSpec]) -> int: @@ -1220,7 +1232,6 @@ def get_kv_cache_config_from_groups( vllm_config: VllmConfig, kv_cache_groups: list[KVCacheGroupSpec], available_memory: int, - suppress_log: bool = False, ) -> KVCacheConfig: """ Generate the KV cache configuration from the KV cache groups and spec @@ -1252,9 +1263,7 @@ def get_kv_cache_config_from_groups( num_blocks = ( available_memory // kv_cache_groups[0].kv_cache_spec.page_size_bytes ) - num_blocks = may_override_num_blocks( - vllm_config, num_blocks, suppress_log=suppress_log - ) + num_blocks = may_override_num_blocks(vllm_config, num_blocks) per_layer_specs = kv_cache_groups[0].kv_cache_spec.kv_cache_specs kv_cache_tensors = [ KVCacheTensor( @@ -1288,11 +1297,7 @@ def get_kv_cache_config_from_groups( ) assert group_size > 0, "group_size must be greater than 0" num_blocks = get_num_blocks( - vllm_config, - group_size, - available_memory, - page_size, - suppress_log=suppress_log, + vllm_config, group_size, available_memory, page_size ) kv_cache_tensors = [] for i in range(group_size): @@ -1688,36 +1693,24 @@ def _report_kv_cache_config( vllm_config: The global VllmConfig kv_cache_config: The resolved KV cache configuration """ - min_block_size = min( - [group.kv_cache_spec.block_size for group in kv_cache_config.kv_cache_groups] - ) - - # Log the KV cache size and maximum concurrency. - num_tokens = ( - kv_cache_config.num_blocks - // len(kv_cache_config.kv_cache_groups) - * min_block_size - ) - dcp_size = vllm_config.parallel_config.decode_context_parallel_size - pcp_size = vllm_config.parallel_config.prefill_context_parallel_size - if pcp_size * dcp_size > 1: - num_tokens *= pcp_size * dcp_size - logger.info( - "Multiplying the GPU KV cache size by the cp_world_size %d " - "(pcp_world_size %d * dcp_world_size %d).", - pcp_size * dcp_size, - pcp_size, - dcp_size, - ) - num_tokens_str = f"{num_tokens:,}" - logger.info_once("GPU KV cache size: %s tokens", num_tokens_str) - max_model_len_str = f"{vllm_config.model_config.max_model_len:,}" + max_model_len = vllm_config.model_config.max_model_len max_concurrency = get_max_concurrency_for_kv_cache_config( vllm_config, kv_cache_config ) + + # GPU KV cache size in tokens = max_concurrency * max_model_len: the total + # tokens of context the pool can hold at peak utilization. Sourcing this + # from the concurrency calculation handles hybrid layouts correctly: SWA / + # chunked-local groups have a per-request block count that's capped by + # their window, so a naive `num_blocks // num_groups * block_size` formula + # underestimates capacity for these models. DCP/PCP sharding is already + # accounted for in each spec's `max_memory_usage_bytes`. + num_tokens = int(max_concurrency * max_model_len) + + logger.info_once("GPU KV cache size: %s tokens", f"{num_tokens:,}") logger.info_once( "Maximum concurrency for %s tokens per request: %.2fx", - max_model_len_str, + f"{max_model_len:,}", max_concurrency, ) @@ -1988,6 +1981,28 @@ def get_kv_cache_configs( for worker_spec in kv_cache_specs ] + # If `num_gpu_blocks_override` is set, the cache size that will actually + # be allocated is decoupled from the profiled `available_memory`: + # `may_override_num_blocks` in `get_kv_cache_config_from_groups` clamps + # `num_blocks` to the override. Reflect that in `available_memory` here so + # auto-fit, the admission check, and the per-worker config builder all + # plan against the same effective capacity. + override = vllm_config.cache_config.num_gpu_blocks_override + if override is not None: + adjusted_memory: list[int] = [] + for groups, avail_mem in zip(projected_groups_per_worker, available_memory): + if not groups: + adjusted_memory.append(avail_mem) + continue + bytes_per_block = _pool_bytes_per_block(groups) + logger.info( + "Overriding num_gpu_blocks=%d with num_gpu_blocks_override=%d", + avail_mem // bytes_per_block, + override, + ) + adjusted_memory.append(override * bytes_per_block) + available_memory = adjusted_memory + if vllm_config.model_config.original_max_model_len == -1: _auto_fit_max_model_len( vllm_config, projected_groups_per_worker, available_memory diff --git a/vllm/v1/worker/gpu_model_runner.py b/vllm/v1/worker/gpu_model_runner.py index a0ba47f945a7..caf3bfdfc3a8 100644 --- a/vllm/v1/worker/gpu_model_runner.py +++ b/vllm/v1/worker/gpu_model_runner.py @@ -5874,7 +5874,7 @@ def _init_minimal_kv_cache_for_profiling(self) -> None: saved_override = self.cache_config.num_gpu_blocks_override self.cache_config.num_gpu_blocks_override = min_blocks minimal_config = get_kv_cache_config_from_groups( - self.vllm_config, kv_cache_groups, available_memory=0, suppress_log=True + self.vllm_config, kv_cache_groups, available_memory=0 ) self.cache_config.num_gpu_blocks_override = saved_override From d109eacd05f774008c7e1d17afc76fc48c4fcbc5 Mon Sep 17 00:00:00 2001 From: chelnnexy <86009079+chelnnexy@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:04:53 -0500 Subject: [PATCH 016/237] [Bugfix][ROCm] Fix gemm_a4w4 call to use updated AITER API signature (#40754) Signed-off-by: cheiluno --- .../quantization/quark/schemes/quark_ocp_mx.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py b/vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py index 620d29515d95..70a7e81cc455 100644 --- a/vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py +++ b/vllm/model_executor/layers/quantization/quark/schemes/quark_ocp_mx.py @@ -96,21 +96,12 @@ def gemm_with_dynamic_quant( x_q = x x_s = x_scales - # 32 alignment is enough for dim0 padding of output for - # gemm_a4w4 kernel - y = torch.empty( - (M + 31) // 32 * 32, - weight.shape[0], - device=x_q.device, - dtype=out_dtype, - ) - - gemm_a4w4( + y = gemm_a4w4( x_q, weight.view(x_q.dtype), x_s, weight_scale.view(x_s.dtype), - y, + dtype=out_dtype, bpreshuffle=True, ) return y[:M] From 6fb3f7b46b12ea63265afbe6d53d6f15a5de7b3a Mon Sep 17 00:00:00 2001 From: qizixi <22851944+zixi-qi@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:22:03 -0700 Subject: [PATCH 017/237] [DSV4] Align aux stream API with DeepseekV4DecoderLayer (#41171) Signed-off-by: zixi-qi --- vllm/model_executor/models/deepseek_v4_mtp.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/vllm/model_executor/models/deepseek_v4_mtp.py b/vllm/model_executor/models/deepseek_v4_mtp.py index cb2ae6a55d84..a3724e5ebe80 100644 --- a/vllm/model_executor/models/deepseek_v4_mtp.py +++ b/vllm/model_executor/models/deepseek_v4_mtp.py @@ -35,7 +35,6 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader from vllm.platforms import current_platform from vllm.sequence import IntermediateTensors -from vllm.utils.multi_stream_utils import AuxStreamType from .deepseek_mtp import SharedHead from .deepseek_v2 import get_spec_layer_idx_from_weight_name @@ -65,6 +64,7 @@ def __init__( vllm_config: VllmConfig, topk_indices_buffer: torch.Tensor, prefix: str, + aux_stream_list: list[torch.cuda.Stream] | None = None, ) -> None: super().__init__() @@ -112,14 +112,11 @@ def __init__( self.shared_head = SharedHead( config=config, prefix=prefix, quant_config=quant_config ) - self.aux_stream_dict = { - AuxStreamType.Attention: torch.cuda.Stream(), - } self.mtp_block = DeepseekV4DecoderLayer( vllm_config, prefix, topk_indices_buffer=topk_indices_buffer, - aux_stream_dict=self.aux_stream_dict, + aux_stream_list=aux_stream_list, ) def forward( @@ -169,6 +166,10 @@ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): device=self.device, ) + # Three aux streams shared across all MTP layers, mirroring + # DeepseekV4Model. + aux_stream_list = [torch.cuda.Stream() for _ in range(3)] + # to map the exact layer index from weights self.layers = torch.nn.ModuleDict( { @@ -176,6 +177,7 @@ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): vllm_config, self.topk_indices_buffer, f"{prefix}.layers.{idx}", + aux_stream_list=aux_stream_list, ) for idx in range( self.mtp_start_layer_idx, From 856b15c62c8a574a1a0a289444d5b9a8120433e3 Mon Sep 17 00:00:00 2001 From: rasmith Date: Tue, 28 Apr 2026 21:12:17 -0500 Subject: [PATCH 018/237] [CI][AMD][BugFix] Patch has_flashinfer decorator for test_select_rocm_aiter_backend (#41072) Signed-off-by: Randall Smith --- tests/kernels/moe/test_unquantized_backend_selection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/kernels/moe/test_unquantized_backend_selection.py b/tests/kernels/moe/test_unquantized_backend_selection.py index 73aa54fa579e..2c4cd7b94e78 100644 --- a/tests/kernels/moe/test_unquantized_backend_selection.py +++ b/tests/kernels/moe/test_unquantized_backend_selection.py @@ -85,7 +85,7 @@ def test_select_default_backend_by_platform( @patch( - "vllm.model_executor.layers.fused_moe.oracle.unquantized.has_flashinfer", + "vllm.utils.flashinfer.has_flashinfer", return_value=False, ) @patch( From 4b95e9cec4e9a1a90d3f4b2afa62e88459b2b90e Mon Sep 17 00:00:00 2001 From: haosdent Date: Wed, 29 Apr 2026 10:23:26 +0800 Subject: [PATCH 019/237] [CI] Return HTTP 400 for unsupported chat content part type (#41121) Signed-off-by: haosdent --- .../entrypoints/openai/test_openai_schema.py | 71 +++++-------------- vllm/entrypoints/chat_utils.py | 9 ++- 2 files changed, 24 insertions(+), 56 deletions(-) diff --git a/tests/entrypoints/openai/test_openai_schema.py b/tests/entrypoints/openai/test_openai_schema.py index 083290ed5b3a..7d17eb9d4569 100644 --- a/tests/entrypoints/openai/test_openai_schema.py +++ b/tests/entrypoints/openai/test_openai_schema.py @@ -1,18 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import json -from http import HTTPStatus from typing import Final import pytest import schemathesis -from httpx import URL -from hypothesis import settings +from hypothesis import HealthCheck, settings from schemathesis import GenerationConfig -from schemathesis.checks import not_a_server_error -from schemathesis.internal.checks import CheckContext from schemathesis.models import Case -from schemathesis.transports.responses import GenericResponse from vllm.platforms import current_platform @@ -65,20 +60,10 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy): def no_invalid_types(case: schemathesis.models.Case): """ - This filter skips test cases with invalid data that schemathesis - incorrectly generates due to permissive schema configurations. - - 1. Skips `POST /tokenize` endpoint cases with `"type": "file"` in - message content, which isn't implemented. - - 2. Skips tool_calls with `"type": "custom"` which schemathesis - incorrectly generates instead of the valid `"type": "function"`. - - Example test cases that are skipped: - curl -X POST -H 'Content-Type: application/json' \ - -d '{"messages": [{"content": [{"file": {}, "type": "file"}], "role": "user"}]}' \ - http://localhost:8000/tokenize + Skips tool_calls with `"type": "custom"` which schemathesis incorrectly + generates instead of the valid `"type": "function"`. + Example test case that is skipped: curl -X POST -H 'Content-Type: application/json' \ -d '{"messages": [{"role": "assistant", "tool_calls": [{"custom": {"input": "", "name": ""}, "id": "", "type": "custom"}]}]}' \ http://localhost:8000/v1/chat/completions @@ -93,20 +78,6 @@ def no_invalid_types(case: schemathesis.models.Case): if not isinstance(message, dict): continue - # Check for invalid file type in tokenize endpoint - if op.method.lower() == "post" and op.path == "/tokenize": - content = message.get("content", []) - if ( - isinstance(content, list) - and len(content) > 0 - and any( - isinstance(item, dict) and item.get("type") == "file" - for item in content - ) - ): - return False - - # Check for invalid tool_calls with non-function types tool_calls = message.get("tool_calls", []) if isinstance(tool_calls, list): for tool_call in tool_calls: @@ -136,24 +107,19 @@ def no_invalid_types(case: schemathesis.models.Case): return strategy.filter(no_invalid_types) -def customized_not_a_server_error( - ctx: CheckContext, response: GenericResponse, case: Case -) -> bool | None: - try: - return not_a_server_error(ctx, response, case) - except Exception: - if ( - URL(response.request.url).path - in ["/v1/chat/completions/render", "/v1/chat/completions"] - and response.status_code == HTTPStatus.NOT_IMPLEMENTED.value - ): - return True - raise - - @schema.parametrize() @schema.override(headers={"Content-Type": "application/json"}) -@settings(deadline=LONG_TIMEOUT_SECONDS * 1000, max_examples=50) +@settings( + deadline=LONG_TIMEOUT_SECONDS * 1000, + max_examples=50, + # Under CI's derandomized hypothesis seed, the schemathesis strategy + # for /v1/chat/completions/batch's nested-message body, combined with + # the no_invalid_types filter (notably the grammar=="" rule), exceeds + # the default filtered-vs-good ratio. The filter is intentional, so + # suppress the health check rather than drop the filter — dropping it + # exposes pre-existing server bugs out of scope here. + suppress_health_check=[HealthCheck.filter_too_much], +) def test_openapi_stateless(case: Case): key = ( case.operation.method.upper(), @@ -180,9 +146,4 @@ def test_openapi_stateless(case: Case): }.get(key, DEFAULT_TIMEOUT_SECONDS) # No need to verify SSL certificate for localhost - case.call_and_validate( - verify=False, - timeout=timeout, - additional_checks=(customized_not_a_server_error,), - excluded_checks=(not_a_server_error,), - ) + case.call_and_validate(verify=False, timeout=timeout) diff --git a/vllm/entrypoints/chat_utils.py b/vllm/entrypoints/chat_utils.py index bd4f29a00410..73a32033aa48 100644 --- a/vllm/entrypoints/chat_utils.py +++ b/vllm/entrypoints/chat_utils.py @@ -40,6 +40,7 @@ from vllm import envs from vllm.config import ModelConfig +from vllm.exceptions import VLLMValidationError from vllm.inputs import MultiModalDataDict, MultiModalUUIDDict from vllm.logger import init_logger from vllm.model_executor.models import SupportsMultiModal @@ -1501,7 +1502,13 @@ def _parse_chat_message_content_part( mm_parser.parse_video(str_content, uuid) modality = "video" else: - raise NotImplementedError(f"Unknown part type: {part_type}") + supported = sorted(MM_PARSER_MAP.keys() | set(PART_TYPES_TO_SKIP_NONE_CONTENT)) + raise VLLMValidationError( + f"Unsupported chat content part type: {part_type!r}. " + f"Supported types: {', '.join(supported)}.", + parameter="type", + value=part_type, + ) if wrap_dicts: return {"type": modality} From 75a7cf2c10f2dcc484c3e0444af33e0eaf3f4311 Mon Sep 17 00:00:00 2001 From: haosdent Date: Wed, 29 Apr 2026 11:23:59 +0800 Subject: [PATCH 020/237] [CI] De-flake test_chat_completion_n_parameter_non_streaming (#41147) Signed-off-by: haosdent --- tests/entrypoints/openai/chat_completion/test_chat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/entrypoints/openai/chat_completion/test_chat.py b/tests/entrypoints/openai/chat_completion/test_chat.py index 212839f78d5c..80f54f6800ae 100644 --- a/tests/entrypoints/openai/chat_completion/test_chat.py +++ b/tests/entrypoints/openai/chat_completion/test_chat.py @@ -845,9 +845,10 @@ async def test_chat_completion_n_parameter_non_streaming( chat_completion = await client.chat.completions.create( model=model_name, messages=messages, - max_completion_tokens=20, - temperature=0.7, + max_completion_tokens=50, + temperature=1.0, n=3, + seed=42, stream=False, ) @@ -859,7 +860,6 @@ async def test_chat_completion_n_parameter_non_streaming( assert choice.message.content is not None assert len(choice.message.content) > 0 - # Verify all responses are different (highly likely with temperature > 0) contents = [choice.message.content for choice in chat_completion.choices] assert len(set(contents)) > 1, "Expected different responses with n=3" From 99255f3cb5cec7466bf9fa5310fd310baf87d711 Mon Sep 17 00:00:00 2001 From: Isotr0py Date: Wed, 29 Apr 2026 12:04:49 +0800 Subject: [PATCH 021/237] [UX] Allow enable/disable model weights loading tracking by config (#41086) Signed-off-by: Isotr0py Co-authored-by: Copilot --- .../model_loader/default_loader.py | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/vllm/model_executor/model_loader/default_loader.py b/vllm/model_executor/model_loader/default_loader.py index 037195b9063a..a76092028671 100644 --- a/vllm/model_executor/model_loader/default_loader.py +++ b/vllm/model_executor/model_loader/default_loader.py @@ -76,7 +76,11 @@ def __init__(self, load_config: LoadConfig): self.local_expert_ids: set[int] | None = None extra_config = load_config.model_loader_extra_config - allowed_keys = {"enable_multithread_load", "num_threads"} + allowed_keys = { + "enable_multithread_load", + "num_threads", + "enable_weights_track", + } unexpected_keys = set(extra_config.keys()) - allowed_keys if unexpected_keys: @@ -86,6 +90,10 @@ def __init__(self, load_config: LoadConfig): f"{unexpected_keys}" ) + self.enable_weights_track: bool | None = extra_config.get( + "enable_weights_track", None + ) + def _prepare_weights( self, model_name_or_path: str, @@ -377,7 +385,6 @@ def load_weights(self, model: nn.Module, model_config: ModelConfig) -> None: self._init_ep_weight_filter(model_config) - weights_to_load = {name for name, _ in model.named_parameters()} loaded_weights = model.load_weights(self.get_all_weights(model_config, model)) self.counter_after_loading_weights = time.perf_counter() @@ -386,8 +393,36 @@ def load_weights(self, model: nn.Module, model_config: ModelConfig) -> None: self.counter_after_loading_weights - self.counter_before_loading_weights, ) # We only enable strict check for non-quantized models - # that have loaded weights tracking currently. - if model_config.quantization is None and loaded_weights is not None: + # that have loaded weights tracking by default. + default_enable_weights_track = ( + model_config.quantization is None and loaded_weights is not None + ) + enable_weights_track = ( + self.enable_weights_track + if self.enable_weights_track is not None + else default_enable_weights_track + ) + if enable_weights_track: + self.track_weights_loading(model, loaded_weights) + + def track_weights_loading( + self, model: nn.Module, loaded_weights: set[str] | None + ) -> None: + weights_to_load = {name for name, _ in model.named_parameters()} + if loaded_weights is not None: + # ignore online quantization scales + for name, module in model.named_modules(): + quant_method = getattr(module, "quant_method", None) + has_online_quant = getattr(quant_method, "uses_meta_device", False) + has_postprocess_quant = getattr( + quant_method, "process_weights_after_loading", None + ) + # ignore kv_cache scale and online quant scale, + # which can be missing in checkpoints + if has_online_quant or has_postprocess_quant: + for param_name, _ in module.named_parameters(): + full_name = f"{name}.{param_name}" if name else param_name + loaded_weights.add(full_name) weights_not_loaded = weights_to_load - loaded_weights if weights_not_loaded: raise ValueError( From 7fd05e05aeb3664ca19346771dc559d93423acd4 Mon Sep 17 00:00:00 2001 From: liangel-02 Date: Wed, 29 Apr 2026 00:05:14 -0400 Subject: [PATCH 022/237] uncomment flex backend for batch invariant mode (#40842) Signed-off-by: Angel Li --- tests/v1/determinism/utils.py | 1 + vllm/v1/attention/backends/flex_attention.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/v1/determinism/utils.py b/tests/v1/determinism/utils.py index f9bebec98619..bbef61477232 100644 --- a/tests/v1/determinism/utils.py +++ b/tests/v1/determinism/utils.py @@ -26,6 +26,7 @@ BACKENDS: list[str] = [ "FLASH_ATTN", "TRITON_ATTN", + "FLEX_ATTENTION", ] # FlashInfer temporarily disabled due to invariant CTA sizes. diff --git a/vllm/v1/attention/backends/flex_attention.py b/vllm/v1/attention/backends/flex_attention.py index a917235ed8cb..b70902478e8f 100644 --- a/vllm/v1/attention/backends/flex_attention.py +++ b/vllm/v1/attention/backends/flex_attention.py @@ -99,6 +99,10 @@ def supports_attn_type(cls, attn_type: str) -> bool: """FlexAttention supports both decoder and encoder-only attention.""" return attn_type in (AttentionType.DECODER, AttentionType.ENCODER_ONLY) + @classmethod + def supports_batch_invariance(cls) -> bool: + return True + @classmethod def supports_mm_prefix(cls) -> bool: """FlexAttention supports full attention for image tokens.""" @@ -326,15 +330,9 @@ class BlockSparsityHint(NamedTuple): def copy_to_persistent(dst, src): - try: - dst = dst.as_strided(src.shape, src.stride()) - except RuntimeError as e: - raise RuntimeError( - f"Fail to re-stride a persistent tensor of shape {dst.shape} " - f"for a tensor of shape {src.shape}" - ) from e - dst.copy_(src) - return dst + sliced = dst[tuple(slice(0, s) for s in src.shape)] + sliced.copy_(src) + return sliced @dataclass From a085b5257dd8cc8d6c255e9b92e4642ee12fc3aa Mon Sep 17 00:00:00 2001 From: Kyle Sayers Date: Wed, 29 Apr 2026 00:06:38 -0400 Subject: [PATCH 023/237] [Docs] [QeRL] Layerwise Reloading Documentation (#40317) Signed-off-by: Kyle Sayers Co-authored-by: Flora Feng <4florafeng@gmail.com> --- docs/assets/training/layerwise.png | Bin 0 -> 159555 bytes .../assets/training/layerwise_bad_loading.png | Bin 0 -> 186632 bytes .../training/layerwise_good_loading.png | Bin 0 -> 197624 bytes docs/training/layerwise.md | 146 ++++++++++++++++++ .../model_loader/reload/layerwise.py | 40 ++++- .../model_loader/reload/utils.py | 34 +++- 6 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 docs/assets/training/layerwise.png create mode 100644 docs/assets/training/layerwise_bad_loading.png create mode 100644 docs/assets/training/layerwise_good_loading.png create mode 100644 docs/training/layerwise.md diff --git a/docs/assets/training/layerwise.png b/docs/assets/training/layerwise.png new file mode 100644 index 0000000000000000000000000000000000000000..bc1e4f24d4a80b6e0228ff74035de05b8004a108 GIT binary patch literal 159555 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 z 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%0A?7JMCa-b!KgL;$pU1%7 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 Qi66<}TuqL-_GZPIN;Ip5 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 z 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 z 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 Z{#4K&lY$}8ae|8re#!ig%8NH6{|i&pI6wdZ literal 0 HcmV?d00001 diff --git a/docs/training/layerwise.md b/docs/training/layerwise.md new file mode 100644 index 000000000000..d304c4a8425d --- /dev/null +++ b/docs/training/layerwise.md @@ -0,0 +1,146 @@ +# What is Layerwise (Re)loading? + +Layerwise reloading is the system used to handle the loading of new weight data into existing weight data destinations without triggering recompilation of the cuda graph and other runtime artifacts. This system is used to enable [QeRL](https://arxiv.org/pdf/2510.11696)-style post training flows, where full-precision trainer weights are quantized and loaded into a target vLLM instance for fast, high-exploration rollouts. The core implementation can be found in [layerwise.py](../../vllm/model_executor/model_loader/reload/layerwise.py). + +![Layerwise](../assets/training/layerwise.png) + +## Layerwise Reloading for QeRL + +In order to load new weights into existing weight data destinations, a weight must undergo the following operations: + +- Transfer: weights must be transferred from trainer model to target node/device +- Fuse: weight partitions must be fused, for example qkv/gate_up +- Process: this typically means online quantization and kernel-specific padding or striding +- Shard: weights must be sharded according to the selected parallelism strategy +- Copy: weights must be copied into the existing weight data destinations + +Layerwise reloading achieves this using the following steps: + +1. Weights are **transferred** from the trainer to the target (see [weight_transfer](weight_transfer/README.md)) +2. Weights loaded via `model.load_weights`, during which they are **sharded** and **fused** +3. Weights are **processed** in an online fashion as soon as all of a layer's weights are loaded +4. Weights are **copied** into the existing weight data destinations + +For more information on implementation, see [Low Level `layerwise` API](#low-level-layerwise-api). + +## Layerwise Loading with Online Quantization + +Online quantization refers to when a user provides full precision weights and those weights are quantized on-the-fly as they are loaded into the model. The layerwise reloading system handles this by treating online quantization as a **processing** step, which is then handled in an online way both during first-time load and during reload. A typical online quantization method implementation should look like this: + +```python +class Fp8OnlineLinearMethod(Fp8LinearMethod): + """Online version of Fp8LinearMethod which loads a full precision checkpoint + and quantizes weights during loading.""" + + uses_meta_device: bool = True + + def create_weights(self, layer: torch.nn.Module, ...): + # weight is materialized and processed during loading + layer.weight = ModelWeightParameter( + data=torch.empty(..., device="meta"), + weight_loader=weight_loader, + ) + + # set up online processing + initialize_online_processing(layer) + + def process_weights_after_loading(self, layer: Module) -> None: + if getattr(layer, "_already_called_process_weights_after_loading", False): + return + + layer.weight, layer.weight_scale = ops.scaled_fp8_quant(layer.weight) + + # Prevent duplicate processing (e.g., during weight reload) + layer._already_called_process_weights_after_loading = True +``` + +## Example Usages + +### High Level Weight Transfer API + +The layerwise reloading system is integrated with the post-training weight transfer system. To use layerwise reloading in conjunction to the weight transfer system, follow the examples found [here](../../examples/rl/). Layerwise reloading is controlled by the `WeightTransferUpdateInfo.is_checkpoint_format` flag and is set to `True` by default. + +### Mid Level `reload_weights` API + +Layerwise reloading is also exposed via the `reload_weights` API. This interface can be called using the following code: + +```python +from vllm import LLM + +llm = LLM("Qwen/Qwen3-0.6B") +llm.collective_rpc("reload_weights") +``` + +This interface also allows specifying a `weights_path` which can be used to select a checkpoint path to load from: + +```python +from vllm import LLM + +# fine tuned model checkpoints for testing +mul_path = "inference-optimization/Qwen3-0.6B-debug-multiply" +add_path = "inference-optimization/Qwen3-0.6B-debug-add" + +llm = LLM("Qwen/Qwen3-0.6B") +llm.collective_rpc("reload_weights", kwargs={"weights_path": mul_path}) +llm.generate("3 4 = ") # 12 + +llm.collective_rpc("reload_weights", kwargs={"weights_path": add_path}) +llm.generate("3 4 = ") # 7 +``` + +Finally, a `weights_iterator` can be provided directly. This iterator can be lazy or eagerly defined. + +```python +from vllm import LLM + +weights_iterator = [("q_proj", ...), ("k_proj", ...), ...] + +llm = LLM("Qwen/Qwen3-0.6B") +llm.collective_rpc("reload_weights", kwargs={"weights_iterator": weights_iterator}) +``` + +### Low Level `layerwise` API + +[layerwise.py](../../vllm/model_executor/model_loader/reload/layerwise.py) Implements the following functions to execute its lifecycle: + +| Function | Purpose | Quantized Reload | Online Quantization | +| - | - | - | - | +| `record_metadata_for_reloading` | Record tensor metadata so that layers can be restored on the meta device | Called by `BaseModelLoader` | Called by `BaseModelLoader` | +| `restore_layer_on_meta` | Restore layer to model format at start of reload | Called by `initialize_layerwise_reload` | Not called. Online quantized weights already start on meta device via `...OnlineLinearMethod.create_weights` | +| `initialize_online_processing` | Wrap weight loaders with the `online_process_loader` wrapper, which buffers weights until all layer weights have been loaded | Called by `initialize_layerwise_reload` | Called by `...OnlineLinearMethod.create_weights` | +| `_layerwise_process` | Process layer once all weights are loaded | Called by `online_process_loader` during loading | Called by `online_process_loader` during loading | +| `_copy_and_restore_kernel_tensors` | Copy processed weights into original tensor locations to affect compiled cuda graphs, etc. | Called by `_layerwise_process` after `process_weights_after_loading` | Not called. There is no compiled cuda graph yet | +| `finalize_layerwise_processing` | Catch any layers which did not load all weights (for example attention weights or weights with padding) | Called by `BaseModelLoader` | Called by `BaseModelLoader` | + +You can plug into this lifecycle directly by calling the `initialize_layerwise_reload`, loading weights, then calling `finalize_layerwise_processing`: + +```python +from vllm import LLM +from vllm.model_executor.model_loader.reload import initialize_layerwise_reload, finalize_layerwise_processing + +llm = LLM("Qwen/Qwen3-0.6B") + +# this model path requires `VLLM_ENABLE_V1_MULTIPROCESSING=0` and is not stable +model = llm.llm_engine.engine_core.engine_core.model_executor.driver_worker.worker.get_model() + +# layerwise reload +initialize_layerwise_reload(model) +model.load_weights(...) +finalize_layerwise_processing(model, llm.model_config) +``` + +## Troubleshooting Excessive Memory Usage + +Layerwise reloading allows users to incrementally load and process weights as they are loaded into the model. This system relies on buffering layer weights on device until all weights of a layer have been loaded. However, without offloading, this approach necessarily causes excessive buffering if weights are loaded out of order. + +For this reason, users must take care as to the order of weights when they are reloading into the model. Weight should be loaded "in order", meaning that each layer's weights are fully loaded before beginning to load the next layer's weights. "Out of order" loading can cause layer weights to stay buffered while other layer weights are loading, leading to excessive memory usage. In the example below, q_proj, k_proj, v_proj, and up_proj are all buffered at the same time, using more memory than if up_proj was loaded after q_proj, k_proj and v_proj. + +| Correct Loading | Incorrect Loading | +| - | - | +| ![Layerwise](../assets/training/layerwise_good_loading.png) | ![Layerwise](../assets/training/layerwise_bad_loading.png) | + +Users will see a warning like the one below if weights are loaded out-of-order. + +```console +WARNING [layerwise.py:198] Allocating 28.5 MB of device memory to buffers to load ["QKVParallelLinear", "MergedColumnParallelLinear"] layers. This extra memory usage can be avoided by ordering weights by their parent layer when reloading. +``` diff --git a/vllm/model_executor/model_loader/reload/layerwise.py b/vllm/model_executor/model_loader/reload/layerwise.py index 2ebe25444781..f8d8304199ea 100644 --- a/vllm/model_executor/model_loader/reload/layerwise.py +++ b/vllm/model_executor/model_loader/reload/layerwise.py @@ -3,7 +3,7 @@ import inspect from collections.abc import Callable from functools import wraps -from weakref import WeakKeyDictionary +from weakref import WeakKeyDictionary, WeakSet import torch @@ -21,7 +21,13 @@ restore_layer_on_meta, ) from .types import LayerReloadingInfo -from .utils import get_layer_params_buffers, get_layer_size, get_layer_tensors +from .utils import ( + get_info_size, + get_layer_params_buffers, + get_layer_size, + get_layer_tensors, + has_device_tensors, +) logger = init_logger(__name__) @@ -43,6 +49,9 @@ WeakKeyDictionary() ) +# Global set used to track loading for logging purposes only +LOADING_LAYERS: WeakSet[torch.nn.Module] = WeakSet() + def get_layerwise_info(layer: torch.nn.Module) -> LayerReloadingInfo: """ @@ -174,11 +183,30 @@ def online_process_loader(*args, **kwargs): info.load_numel_total, ) + # Do not online process attention layers, must wait until finalize + if isinstance(layer, (Attention, MLAAttention)): + return ret + + # Log warnings allocating excessive buffers on device + if has_device_tensors(bound_args): + LOADING_LAYERS.add(layer) + if len(LOADING_LAYERS) >= 2: + names = sorted([layer.__class__.__name__ for layer in LOADING_LAYERS]) + mem_used = sum( + get_info_size(LAYERWISE_INFO[layer]) for layer in LOADING_LAYERS + ) + logger.warning_once( + "Allocating %.1f MB of device memory to buffers to load %s layers. " + "This extra memory usage can be avoided by ordering weights " + "by their parent layer when reloading.", + mem_used / 1e6, + str(list(names)), + ) + # Process and copy when all weights are loaded - if info.load_numel >= info.load_numel_total and not isinstance( # type: ignore[operator] - layer, (Attention, MLAAttention) - ): + if info.load_numel >= info.load_numel_total: # type: ignore[operator] _layerwise_process(layer, info) + LOADING_LAYERS.discard(layer) return ret @@ -240,6 +268,8 @@ def finalize_layerwise_processing(model: torch.nn.Module, model_config: ModelCon _finalize_attention_layer(layer, info, model_config) info.reset() + LOADING_LAYERS.clear() + def finalize_layerwise_reload(*args, **kwargs): finalize_layerwise_processing(*args, **kwargs) diff --git a/vllm/model_executor/model_loader/reload/utils.py b/vllm/model_executor/model_loader/reload/utils.py index 463ff6422213..7a3d6873e101 100644 --- a/vllm/model_executor/model_loader/reload/utils.py +++ b/vllm/model_executor/model_loader/reload/utils.py @@ -1,14 +1,18 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project +from inspect import BoundArguments + import torch -from .types import LayerTensors +from .types import LayerReloadingInfo, LayerTensors __all__ = [ "get_layer_tensors", "get_layer_params_buffers", "get_layer_size", + "has_device_tensors", + "get_info_size", ] @@ -39,3 +43,31 @@ def get_layer_size(layer: torch.nn.Module) -> int: for name, tensor in get_layer_tensors(layer).items() if name not in SKIP_TENSORS ) + + +def has_device_tensors(bound_args: BoundArguments) -> bool: + """ + Return True if the loaded weights exist on an accelerator device + + :param bound_args: args to load weights + :return: True if weights are on accelerator device + """ + return any( + isinstance(value, torch.Tensor) and value.device.type not in ("meta", "cpu") + for value in bound_args.arguments.values() + ) + + +def get_info_size(info: LayerReloadingInfo) -> int: + """ + Calculate the number of bytes used by loaded weights for a given layer + + :param info: layerwise info to get size of + :return: number of bytes used by loaded weights + """ + return sum( + value.nbytes + for _, args in info.loaded_weights + for value in args.arguments.values() + if isinstance(value, torch.Tensor) and value.device.type not in ("meta", "cpu") + ) From 916e56c05c997155b865dd4f46172f26e755da3d Mon Sep 17 00:00:00 2001 From: Kyle Sayers Date: Wed, 29 Apr 2026 00:06:54 -0400 Subject: [PATCH 024/237] [QeRL] Add warnings for extra memory buffering (#40309) Signed-off-by: Kyle Sayers Co-authored-by: Flora Feng <4florafeng@gmail.com> From fa1b9840f6d87ef6e3b247a78514ccc1d6e5f1ce Mon Sep 17 00:00:00 2001 From: Lucas Kabela Date: Tue, 28 Apr 2026 21:07:24 -0700 Subject: [PATCH 025/237] [BE][Torch 2.12] Remove workaround code for fixed cublas issue (#40845) Signed-off-by: Lucas Kabela Signed-off-by: Lucas Kabela Co-authored-by: Wentao Ye <44945378+yewentao256@users.noreply.github.com> --- vllm/model_executor/layers/batch_invariant.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/vllm/model_executor/layers/batch_invariant.py b/vllm/model_executor/layers/batch_invariant.py index 3831f7aa9658..bcdd30500329 100644 --- a/vllm/model_executor/layers/batch_invariant.py +++ b/vllm/model_executor/layers/batch_invariant.py @@ -930,22 +930,17 @@ def enable_batch_invariant_mode(): _batch_invariant_MODE = True _batch_invariant_LIB = torch.library.Library("aten", "IMPL") - if current_platform.is_device_capability_family( - 100 - ) or current_platform.is_device_capability_family(80): - # For PyTorch 2.9, B200 uses GEMV for bs=1 - # Requires https://github.com/pytorch/pytorch/pull/166735 + if current_platform.is_device_capability_family(80): + # SM80 (Ampere) cannot rely on cuBLASLt-only determinism; install the + # triton persistent matmul overrides for mm/addmm/matmul/linear. _batch_invariant_LIB.impl("aten::mm", mm_batch_invariant, "CUDA") _batch_invariant_LIB.impl("aten::addmm", addmm_batch_invariant, "CUDA") _batch_invariant_LIB.impl("aten::matmul", matmul_batch_invariant, "CUDA") _batch_invariant_LIB.impl("aten::linear", linear_batch_invariant, "CUDA") - - # Query the shared memory size and set block size - # accordingly to avoid triton OutOfResources - _fp16_block_size_n = 256 if get_max_shared_memory_bytes() > 106496 else 128 else: - # Only source of batch invariance for Hopper is split-k, can disable through - # cuBLAS workspace config + # Hopper (SM90) and Blackwell (SM100): the only source of batch + # variance is split-k, which we disable via the cuBLAS workspace + # config. _original_cublas_workspace_cfg = os.environ.get("CUBLAS_WORKSPACE_CONFIG", None) _original_cublaslt_workspace_size = os.environ.get( "CUBLASLT_WORKSPACE_SIZE", None @@ -953,6 +948,11 @@ def enable_batch_invariant_mode(): os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8" os.environ["CUBLASLT_WORKSPACE_SIZE"] = "1" + # Triton bmm/persistent-matmul kernels read this for the FP16 N-tile size; + # set unconditionally because bmm is overridden on all CUDA platforms. + if current_platform.is_cuda(): + _fp16_block_size_n = 256 if get_max_shared_memory_bytes() > 106496 else 128 + _batch_invariant_LIB.impl( "aten::_log_softmax", _log_softmax_batch_invariant, "CUDA" ) From 1312f0753115cb36410334e3667961d1237a287b Mon Sep 17 00:00:00 2001 From: Walter Beller-Morales Date: Wed, 29 Apr 2026 00:07:53 -0400 Subject: [PATCH 026/237] [Feature] add cohere reasoning and tool parsers (#40422) Signed-off-by: walterbm --- docs/features/reasoning_outputs.md | 1 + docs/features/tool_calling.md | 10 + vllm/reasoning/__init__.py | 8 + .../cohere_command_reasoning_parser.py | 546 ++++++++++++++++++ vllm/tool_parsers/__init__.py | 8 + .../cohere_command_tool_parser.py | 127 ++++ 6 files changed, 700 insertions(+) create mode 100644 vllm/reasoning/cohere_command_reasoning_parser.py create mode 100644 vllm/tool_parsers/cohere_command_tool_parser.py diff --git a/docs/features/reasoning_outputs.md b/docs/features/reasoning_outputs.md index ef3b3ad6ec07..374149786e14 100644 --- a/docs/features/reasoning_outputs.md +++ b/docs/features/reasoning_outputs.md @@ -13,6 +13,7 @@ vLLM currently supports the following reasoning models: | Model Series | Parser Name | Structured Output Support | Tool Calling | | ------------ | ----------- | ---------------- | ----------- | +| [Cohere Command A Reasoning](https://huggingface.co/CohereLabs/command-a-reasoning-08-2025) | `cohere_command3` | `json`, `regex` | ✅ | | [DeepSeek R1 series](https://huggingface.co/collections/deepseek-ai/deepseek-r1-678e1e131c0169c0bc89728d) | `deepseek_r1` | `json`, `regex` | ❌ | | [DeepSeek-V3.1](https://huggingface.co/collections/deepseek-ai/deepseek-v31-68a491bed32bd77e7fca048f) | `deepseek_v3` | `json`, `regex` | ❌ | | [ERNIE-4.5-VL series](https://huggingface.co/baidu/ERNIE-4.5-VL-28B-A3B-PT) | `ernie45` | `json`, `regex` | ❌ | diff --git a/docs/features/tool_calling.md b/docs/features/tool_calling.md index e9aa87a69647..9c60255d6928 100644 --- a/docs/features/tool_calling.md +++ b/docs/features/tool_calling.md @@ -369,6 +369,16 @@ Flags: * For non-reasoning: `--tool-call-parser hunyuan_a13b` * For reasoning: `--tool-call-parser hunyuan_a13b --reasoning-parser hunyuan_a13b` +### Cohere Command A Reasoning (`cohere_command3`) + +Supported models: + +* [`CohereLabs/command-a-reasoning-08-2025`](https://huggingface.co/CohereLabs/command-a-reasoning-08-2025) + +Flags: `--tool-call-parser cohere_command3 --reasoning-parser cohere_command3` + +Note: the Cohere tool parser requires the `cohere_melody` package, which is not installed by default. Before using this parser please install the [cohere_melody](https://pypi.org/project/cohere-melody/) package. + ### LongCat-Flash-Chat Models (`longcat`) Supported models: diff --git a/vllm/reasoning/__init__.py b/vllm/reasoning/__init__.py index 2347eae54c25..cd51f106503a 100644 --- a/vllm/reasoning/__init__.py +++ b/vllm/reasoning/__init__.py @@ -36,6 +36,14 @@ "poolside_v1_reasoning_parser", "PoolsideV1ReasoningParser", ), + "cohere_command3": ( + "cohere_command_reasoning_parser", + "CohereCommand3ReasoningParser", + ), + "cohere_command4": ( + "cohere_command_reasoning_parser", + "CohereCommand4ReasoningParser", + ), "ernie45": ( "ernie45_reasoning_parser", "Ernie45ReasoningParser", diff --git a/vllm/reasoning/cohere_command_reasoning_parser.py b/vllm/reasoning/cohere_command_reasoning_parser.py new file mode 100644 index 000000000000..c96b21d4e8fb --- /dev/null +++ b/vllm/reasoning/cohere_command_reasoning_parser.py @@ -0,0 +1,546 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from __future__ import annotations + +import json +from collections.abc import Mapping, Sequence +from typing import Any, NamedTuple, TypedDict, TypeGuard + +import regex as re +import xgrammar as xgr + +try: + from cohere_melody import PyFilter, PyFilterOptions +except ImportError as e: + raise ImportError( + "The Cohere reasoning parser requires the `cohere_melody` " + "package, which is not installed. Install it with:\n" + " pip install cohere_melody" + ) from e + + +from vllm.entrypoints.mcp.tool_server import ToolServer +from vllm.entrypoints.openai.chat_completion.protocol import ( + ChatCompletionRequest, +) +from vllm.entrypoints.openai.engine.protocol import ( + AnyResponseFormat, + DeltaFunctionCall, + DeltaMessage, + DeltaToolCall, +) +from vllm.entrypoints.openai.responses.protocol import ResponsesRequest +from vllm.reasoning import ReasoningParser +from vllm.sampling_params import StructuredOutputsParams +from vllm.tokenizers import TokenizerLike + +REPLACEMENT_CHAR = "\ufffd" + + +class CohereTagRegistry(NamedTuple): + """A single ``structural_tag`` begin("trigger")/end pair.""" + + trigger: str + end: str + + +class CohereTagStyle(NamedTuple): + """The structural tags style for a given model architecture.""" + + json: CohereTagRegistry + tools: CohereTagRegistry + + +class CohereNormalizedTool(TypedDict): + """A tool definition normalized to the shape ``collect_tool_schema`` expects. + + ``parameters`` is a JSON Schema object (possibly empty) describing the tool's + call signature. + """ + + name: str + parameters: dict[str, Any] + + +COMMAND_A_TOOLS_TAG = CohereTagRegistry( + trigger="<|START_ACTION|>", end="<|END_ACTION|>" +) +COMMAND_A_JSON_TAG = CohereTagRegistry( + trigger="<|START_RESPONSE|>", end="<|END_RESPONSE|>" +) + +MODEL_TO_TAG_STYLE: dict[str, CohereTagStyle] = { + "Cohere2ForCausalLM": CohereTagStyle( + json=COMMAND_A_JSON_TAG, tools=COMMAND_A_TOOLS_TAG + ), + "Cohere2VisionForConditionalGeneration": CohereTagStyle( + json=COMMAND_A_JSON_TAG, tools=COMMAND_A_TOOLS_TAG + ), +} + + +def collect_tool_schema(tool_schema: list[CohereNormalizedTool]) -> str: + """Build an xgrammar EBNF grammar that matches a JSON array of tool calls. + + The grammar shape is architecture-independent; callers are responsible for + wrapping it in the correct structural tag (see ``CohereTagStyle.tools``). + """ + tool_dictionary: dict[str, str] = {} + for tool in tool_schema: + tool_name = tool["name"] + tool_parameters = json.dumps(tool["parameters"]) + json_schema = f"""{{ + "type": "object", + "properties": {{ + "tool_call_id": {{ + "type": "string", + "pattern": "^[0-9]+$" + }}, + "tool_name": {{ + "type": "string", + "const": "{tool_name}" + }}, + "parameters": {tool_parameters} + }} + }}""" + tool_grammar = str(xgr.Grammar.from_json_schema(json_schema)) + for match in re.findall(r"\b(\w+)\s*::=", tool_grammar): + tool_grammar = re.sub( + rf"\b{re.escape(match)}\b", tool_name + match, tool_grammar + ) + tool_dictionary[tool_name] = f"{tool_name} ::= {tool_name}root\n{tool_grammar}" + # Emitted grammar shape: + # root ::= tools + # tools ::= ws "[" ws tool ws ("," ws tool)* ws "]" ws + # ws ::= (" " | "\t" | "\n")* + # tool ::= | | ... (one alternative per input) + # ::= root (per-tool xgrammar rules) + # root ::= ... (from xgr.Grammar.from_json_schema) + tool_alternatives = "tool ::= " + " | ".join(tool_dictionary.keys()) + tool_rules = "\n ".join(tool_dictionary.values()) + grammar = f"""root ::= tools + tools ::= ws "[" ws tool ws ("," ws tool)* ws "]" ws + ws ::= (" " | "\\t" | "\\n")* + {tool_alternatives} + {tool_rules} + """ + return grammar + + +def _tool_definitions_to_schema_list( + tools: str | list[Any], +) -> list[CohereNormalizedTool]: + """ + Build the list of ``CohereNormalizedTool`` dicts expected by + ``collect_tool_schema``. + + Accepts: + - JSON string + - list of dicts with top-level ``name`` / ``parameters`` + - list of Chat Completions-style ``{"type": "function", "function": {...}}`` + - list of Pydantic models with ``model_dump()`` + """ + if isinstance(tools, str): + try: + parsed = json.loads(tools) + except json.JSONDecodeError: + return [] + if not isinstance(parsed, list): + return [] + else: + parsed = list(tools) + + out: list[CohereNormalizedTool] = [] + for raw in parsed: + t = raw.model_dump() if hasattr(raw, "model_dump") else raw + if not isinstance(t, dict): + continue + # Unwrap Chat Completions' ``{"type": "function", "function": {...}}`` + # shape; otherwise take the dict as-is. + if t.get("type") == "function" and isinstance(t.get("function"), dict): + t = t["function"] + name = t.get("name") + if not isinstance(name, str): + continue + params = t.get("parameters") + out.append( + CohereNormalizedTool( + name=name, + parameters=params if isinstance(params, dict) else {}, + ) + ) + return out + + +def _has_effective_tools( + tools: str | list[Any] | None, +) -> TypeGuard[str | list[Any]]: + """ + True when ``tools`` contains at least one tool definition to convert. + + ``ResponsesRequest`` defaults ``tools`` to ``[]``; ``ChatCompletionRequest`` + uses ``None``. Both mean "no tools" here. Strings (e.g. a JSON blob) are + treated as effective only when non-blank. + """ + if tools is None: + return False + if isinstance(tools, str): + return bool(tools.strip()) + return len(tools) > 0 + + +# Builder: produces vLLM response_format in xgrammar's canonical format. +# See xgrammar docs: type "structural_tag" with "format" = triggered_tags +# and tag content type = json_schema | grammar. +def convert_schema_to_structural_tags( + schema: dict | None = None, + tools: str | list[Any] | None = None, + model_architecture: str | None = None, +) -> str | None: + """ + Returns a response_format string accepted by xgrammar's structural tag format. + Uses the canonical shape: {"type": "structural_tag", "format": {...}} with + format.type "triggered_tags" and tag content type "json_schema" or "grammar". + + Callers that are not on an engine path (e.g. the reasoning parser) must pass + ``model_architecture`` explicitly. + """ + if model_architecture is None or model_architecture not in MODEL_TO_TAG_STYLE: + return None + style = MODEL_TO_TAG_STYLE[model_architecture] + + tags: list[dict] = [] + + def _add_tag(tag: CohereTagRegistry, content: dict) -> None: + tags.append({"begin": tag.trigger, "content": content, "end": tag.end}) + + if schema is not None: + # Add the JSON-schema tag both for schema-only requests and for the + # "tools plus JSON mode" case (North use case: follow the schema when + # the model decides not to call any tool). + _add_tag(style.json, {"type": "json_schema", "json_schema": schema}) + + if _has_effective_tools(tools): + # ``tools`` may be a JSON string (poseidon / RESPONSE_FORMAT_TOOL_DEFINITIONS) + # or a list (Chat Completions ``request.tools`` as Pydantic models or dicts). + tool_schema_list = _tool_definitions_to_schema_list(tools) + if not tool_schema_list: + raise ValueError( + "No valid tool definitions could be parsed from the request for " + "structural tag conversion." + ) + tool_grammar = collect_tool_schema(tool_schema_list) + _add_tag(style.tools, {"type": "grammar", "grammar": tool_grammar}) + + if not tags: + return None + return json.dumps( + { + "type": "structural_tag", + "format": { + "type": "triggered_tags", + "triggers": [t["begin"] for t in tags], + "tags": tags, + }, + } + ) + + +def _response_format_type( + response_format: AnyResponseFormat | dict | None, +) -> str | None: + if response_format is None: + return None + if isinstance(response_format, dict): + t = response_format.get("type") + return t if isinstance(t, str) else None + return response_format.type + + +def _maybe_parse_json_dict(value: Any) -> dict | None: + """If value is a JSON string, parse to dict; otherwise require dict.""" + if isinstance(value, dict): + return value + if isinstance(value, str): + try: + parsed = json.loads(value) + except (TypeError, json.JSONDecodeError): + return None + return parsed if isinstance(parsed, dict) else None + return None + + +def _unwrap_nested_schema(candidate: Any) -> dict | None: + """Return ``candidate`` as a dict, unwrapping a nested ``schema`` if present. + + Returns ``None`` if ``candidate`` is not (and cannot be parsed into) a dict. + """ + cand = _maybe_parse_json_dict(candidate) + if not isinstance(cand, dict): + return None + nested = cand.get("schema") + return nested if isinstance(nested, dict) else cand + + +def _schema_from_json_schema_field(js_wr: Any) -> dict | None: + """ + Extract the JSON Schema object from Chat Completions ``json_schema`` payload. + + Accepts: + - ``JsonSchemaResponseFormat`` (Pydantic) with ``schema`` / ``json_schema`` field + - dict in OpenAI shape ``{"name": ..., "schema": {...}}`` + - dict with ``json_schema`` key holding either the schema or a nested wrapper + - dict that is already a JSON Schema document (some clients omit the wrapper) + - JSON strings for any of the above + """ + if js_wr is None: + return None + + parsed_wr = _maybe_parse_json_dict(js_wr) + if parsed_wr is not None: + js_wr = parsed_wr + + if hasattr(js_wr, "model_dump"): + for by_alias in (True, False): + try: + data = js_wr.model_dump(by_alias=by_alias, exclude_none=False) + except TypeError: + data = js_wr.model_dump(by_alias=by_alias) + out = _unwrap_nested_schema(data.get("schema") or data.get("json_schema")) + if out is not None: + return out + inner_attr = getattr(js_wr, "json_schema", None) + return inner_attr if isinstance(inner_attr, dict) else None + + if isinstance(js_wr, dict): + for key in ("schema", "json_schema"): + out = _unwrap_nested_schema(js_wr.get(key)) + if out is not None: + return out + return js_wr + + return None + + +def _schema_dict_from_chat_response_format( + rf: AnyResponseFormat | dict | None, +) -> dict | None: + """JSON schema dict from Chat Completions ``request.response_format`` only.""" + if rf is None: + return None + rf_type = _response_format_type(rf) + if rf_type == "json_object": + return {"type": "object"} + if rf_type != "json_schema": + return None + js_wr = ( + rf.get("json_schema") + if isinstance(rf, dict) + else getattr(rf, "json_schema", None) + ) + return _schema_from_json_schema_field(js_wr) + + +def _schema_dict_from_structured_outputs( + so: StructuredOutputsParams | None, +) -> dict | None: + """Schema dict from ``structured_outputs`` (``json`` / ``json_object``). + + Same unwrapping as ``json_schema``. ``json`` is expected to be ``str`` or + ``dict`` (enforced by ``StructuredOutputsParams`` / request models); other + types raise ``ValueError`` only if a caller bypasses that validation. + """ + if so is None: + return None + if so.json_object: + return {"type": "object"} + raw: Any = so.json + if raw is None: + return None + + if hasattr(raw, "model_dump"): + out = _schema_from_json_schema_field(raw) + if out is None: + raise ValueError( + "structured_outputs.json model has no extractable JSON Schema." + ) + return out + + if isinstance(raw, str): + if not raw.strip(): + raise ValueError("structured_outputs.json cannot be empty.") + try: + raw = json.loads(raw) + except json.JSONDecodeError as e: + raise ValueError("structured_outputs.json must be valid JSON.") from e + if not isinstance(raw, dict): + raise ValueError("structured_outputs.json must decode to a JSON object.") + + if isinstance(raw, Mapping): + body = raw if isinstance(raw, dict) else dict(raw) + return _schema_from_json_schema_field(body) or body + + raise ValueError( + f"structured_outputs.json has unsupported type {type(raw).__name__}." + ) + + +class BaseCohereCommandReasoningParser(ReasoningParser): + def __init__( + self, + tokenizer: TokenizerLike, + *args, + streaming_opts: PyFilterOptions, + unary_opts: PyFilterOptions, + **kwargs, + ): + super().__init__(tokenizer, *args, **kwargs) + self.end_token_id = tokenizer.convert_tokens_to_ids("<|END_THINKING|>") + self.unary_opts = unary_opts + self.melody_unary = PyFilter(unary_opts) + self.melody_streaming = PyFilter(streaming_opts) + + @property + def reasoning_start_str(self) -> str | None: + return "<|START_THINKING|>" + + @property + def reasoning_end_str(self) -> str | None: + return "<|END_THINKING|>" + + def extract_reasoning_streaming( + self, + previous_text: str, + current_text: str, + delta_text: str, + previous_token_ids: Sequence[int], + current_token_ids: Sequence[int], + delta_token_ids: Sequence[int], + ) -> DeltaMessage | None: + r = self.melody_streaming.write_decoded(delta_text) + if r.content is None and r.reasoning is None and not r.tool_calls: + return None + msg = DeltaMessage() + if r.content is not None: + msg.content = r.content + if r.reasoning is not None: + msg.reasoning = r.reasoning + if r.tool_calls: + msg.tool_calls = [ + DeltaToolCall( + id=tc.id, + index=tc.index, + type="function", + function=DeltaFunctionCall(name=tc.name, arguments=tc.arguments), + ) + for tc in r.tool_calls + ] + return msg + + def extract_reasoning( + self, model_output: str, request: ChatCompletionRequest | ResponsesRequest + ) -> tuple[str | None, str | None]: + result = self.melody_unary.process_full_text(model_output) + return result.reasoning, result.content + + def extract_content_ids(self, input_ids: list[int]) -> list[int]: + token_buf: list[int] = [] + content_ids: list[int] = [] + content_filter = PyFilter(self.unary_opts) + for t in input_ids: + token_buf.append(t) + s = self.model_tokenizer.decode(token_buf, skip_special_tokens=False) + if s.endswith(REPLACEMENT_CHAR): + continue + r = content_filter.write_decoded(s) + if r.content is not None: + content_ids.extend(token_buf) + token_buf = [] + return content_ids + + def is_reasoning_end(self, input_ids: Sequence[int]) -> bool: + return any(tid == self.end_token_id for tid in reversed(input_ids)) + + def prepare_structured_tag( + self, original_tag: str | None, tool_server: ToolServer | None + ) -> str | None: + # Responses API replaces ``structural_tag`` via the reasoning parser. + # Default ``ReasoningParser.prepare_structured_tag`` returns None, which + # would clear a Cohere tag produced in ``adjust_request`` and break + # ``StructuredOutputsParams`` validation. Preserve the existing tag. + return original_tag + + def adjust_request( + self, request: ChatCompletionRequest | ResponsesRequest + ) -> ChatCompletionRequest | ResponsesRequest: + so = request.structured_outputs + if so is not None and so.structural_tag: + return request + # Schema: prefer ``response_format`` (OpenAI Chat Completions), then + # ``structured_outputs.json`` / ``json_object`` (vLLM direct). Tools stay + # on ``request.tools``. + rf = ( + request.response_format + if isinstance(request, ChatCompletionRequest) + else None + ) + if rf is not None and _response_format_type(rf) == "structural_tag": + return request + model_architecture = ( + self._model_config.architecture if self._model_config is not None else None + ) + tools = request.tools + # ``response_format`` wins if both it and ``structured_outputs`` supply JSON. + schema = _schema_dict_from_chat_response_format(rf) + if schema is None: + schema = _schema_dict_from_structured_outputs(so) + if schema is None and not _has_effective_tools(tools): + return request + if model_architecture is None: + return request + result = convert_schema_to_structural_tags( + schema=schema, + tools=tools, + model_architecture=model_architecture, + ) + if result is None: + # Unsupported architectures are not in ``MODEL_TO_TAG_STYLE``; conversion + raise ValueError( + "Failed to build structural_tag guided decoding constraints from " + "this request's JSON schema and/or tools. The configured model " + f"architecture ({model_architecture!r}) does not support Cohere " + "command structural tags, or the schema cannot be expressed in " + "that format." + ) + request.structured_outputs = StructuredOutputsParams(structural_tag=result) + # Folded JSON constraints into ``structural_tag``; drop ``response_format`` + # when it was the source so ``to_sampling_params`` does not also set ``json`` / + # ``json_object`` (mutually exclusive in ``StructuredOutputsParams``). + if isinstance(request, ChatCompletionRequest) and rf is not None: + rf_type = _response_format_type(rf) + if rf_type in ("json_schema", "json_object"): + request.response_format = None + return request + + +class CohereCommand3ReasoningParser(BaseCohereCommandReasoningParser): + def __init__(self, tokenizer: TokenizerLike, *args, **kwargs): + super().__init__( + tokenizer, + *args, + streaming_opts=PyFilterOptions().cmd3(), + unary_opts=PyFilterOptions().cmd3().no_tools(), + **kwargs, + ) + + +class CohereCommand4ReasoningParser(BaseCohereCommandReasoningParser): + def __init__(self, tokenizer: TokenizerLike, *args, **kwargs): + super().__init__( + tokenizer, + *args, + streaming_opts=PyFilterOptions().cmd4(), + unary_opts=PyFilterOptions().cmd4().no_tools(), + **kwargs, + ) diff --git a/vllm/tool_parsers/__init__.py b/vllm/tool_parsers/__init__.py index 61a11cbcc376..f64209e535b7 100644 --- a/vllm/tool_parsers/__init__.py +++ b/vllm/tool_parsers/__init__.py @@ -38,6 +38,14 @@ "deepseekv4_tool_parser", "DeepSeekV4ToolParser", ), + "cohere_command3": ( + "cohere_command_tool_parser", + "CohereCommand3ToolParser", + ), + "cohere_command4": ( + "cohere_command_tool_parser", + "CohereCommand4ToolParser", + ), "ernie45": ( "ernie45_tool_parser", "Ernie45ToolParser", diff --git a/vllm/tool_parsers/cohere_command_tool_parser.py b/vllm/tool_parsers/cohere_command_tool_parser.py new file mode 100644 index 000000000000..0b252ce3177a --- /dev/null +++ b/vllm/tool_parsers/cohere_command_tool_parser.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from collections.abc import Sequence + +try: + from cohere_melody import PyFilter, PyFilterOptions +except ImportError as e: + raise ImportError( + "The Cohere tool parser requires the `cohere_melody` " + "package, which is not installed. Install it with:\n" + " pip install cohere_melody" + ) from e + +from vllm.entrypoints.openai.chat_completion.protocol import ( + ChatCompletionRequest, +) +from vllm.entrypoints.openai.engine.protocol import ( + DeltaFunctionCall, + DeltaMessage, + DeltaToolCall, + ExtractedToolCallInformation, + FunctionCall, + ToolCall, +) +from vllm.entrypoints.openai.responses.protocol import ( + ResponsesRequest, +) +from vllm.tokenizers import TokenizerLike +from vllm.tool_parsers import ToolParser +from vllm.tool_parsers.utils import Tool + + +class BaseCohereCommandToolParser(ToolParser): + def __init__( + self, + tokenizer: TokenizerLike, + streaming_opts: PyFilterOptions, + unary_opts: PyFilterOptions, + ): + super().__init__(tokenizer) + self.melody_streaming = PyFilter(streaming_opts) + self.melody_unary = PyFilter(unary_opts) + + def adjust_request( + self, request: ChatCompletionRequest | ResponsesRequest + ) -> ChatCompletionRequest | ResponsesRequest: + request = super().adjust_request(request) + request.skip_special_tokens = False + return request + + def extract_tool_calls_streaming( + self, + previous_text: str, + current_text: str, + delta_text: str, + previous_token_ids: Sequence[int], + current_token_ids: Sequence[int], + delta_token_ids: Sequence[int], + request: ChatCompletionRequest, + ) -> DeltaMessage | None: + r = self.melody_streaming.write_decoded(delta_text) + if r.content is not None: + return DeltaMessage(content=r.content) + if r.reasoning is not None: + return DeltaMessage(reasoning=r.reasoning) + if r.tool_calls: + return DeltaMessage( + tool_calls=[ + DeltaToolCall( + id=tc.id, + index=tc.index, + type="function", + function=DeltaFunctionCall( + name=tc.name, arguments=tc.arguments + ), + ) + for tc in r.tool_calls + ] + ) + return None + + def extract_tool_calls( + self, + model_output: str, + request: ChatCompletionRequest, + ) -> ExtractedToolCallInformation: + result = self.melody_unary.process_full_text(model_output) + tool_calls = [ + ToolCall( + id=tc.id, + type="function", + function=FunctionCall(name=tc.name, arguments=tc.arguments), + ) + for tc in result.tool_calls + ] + return ExtractedToolCallInformation( + tools_called=len(tool_calls) > 0, + tool_calls=tool_calls, + content=result.content, + ) + + +class CohereCommand3ToolParser(BaseCohereCommandToolParser): + def __init__( + self, + tokenizer: TokenizerLike, + tools: list[Tool] | None = None, + ): + super().__init__( + tokenizer, + streaming_opts=PyFilterOptions().cmd3(), + unary_opts=PyFilterOptions().cmd3(), + ) + + +class CohereCommand4ToolParser(BaseCohereCommandToolParser): + def __init__( + self, + tokenizer: TokenizerLike, + tools: list[Tool] | None = None, + ): + super().__init__( + tokenizer, + streaming_opts=PyFilterOptions().cmd4(), + unary_opts=PyFilterOptions().cmd4(), + ) From 803b9d7881cd3a8482aaa1e6bf990193b55c6331 Mon Sep 17 00:00:00 2001 From: Wei Zhao <51183510+wzhao18@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:08:16 -0400 Subject: [PATCH 027/237] [Bugfix] Fix Deepseek V4 import error due to AOT compile cache loading (#41090) Signed-off-by: wzhao18 Signed-off-by: Wei Zhao <51183510+wzhao18@users.noreply.github.com> --- vllm/model_executor/models/deepseek_v4.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vllm/model_executor/models/deepseek_v4.py b/vllm/model_executor/models/deepseek_v4.py index d6edf0789f57..d41a8b666d33 100644 --- a/vllm/model_executor/models/deepseek_v4.py +++ b/vllm/model_executor/models/deepseek_v4.py @@ -1097,6 +1097,11 @@ def __init__( aux_stream_list: list[torch.cuda.Stream] | None = None, ): super().__init__() + + # Lazy import to avoid top-level tilelang dependency. + # Registers both torch.ops.vllm.mhc_pre and mhc_post + import vllm.model_executor.layers.mhc # noqa: F401 + config = vllm_config.model_config.hf_config self.hidden_size = config.hidden_size @@ -1167,11 +1172,6 @@ def hc_pre( hc_scale: torch.Tensor, hc_base: torch.Tensor, ): - # Lazy import to avoid top-level tilelang dependency. - # Registers both torch.ops.vllm.mhc_pre and mhc_post, - # so hc_post() doesn't need its own import. - import vllm.model_executor.layers.mhc # noqa: F401 - post_mix, res_mix, layer_input = torch.ops.vllm.mhc_pre( residual=x, fn=hc_fn, From d95d03c719cd69e634e567d6cad3228557151393 Mon Sep 17 00:00:00 2001 From: Fadi Arafeh <115173828+fadara01@users.noreply.github.com> Date: Wed, 29 Apr 2026 05:08:35 +0100 Subject: [PATCH 028/237] [BugFix][CPU] fix error on CPU runner shutdown (#41034) Signed-off-by: Fadi Arafeh --- vllm/v1/worker/cpu_model_runner.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vllm/v1/worker/cpu_model_runner.py b/vllm/v1/worker/cpu_model_runner.py index 61fe44d251b3..22c1b5bd0917 100644 --- a/vllm/v1/worker/cpu_model_runner.py +++ b/vllm/v1/worker/cpu_model_runner.py @@ -21,6 +21,9 @@ class CPUModelRunner(GPUModelRunner): def __init__(self, vllm_config: VllmConfig, device: torch.device): + # avoid calling accelerator APIs for methods inherited from super class + _set_torch_accelerator_to_noop() + with _torch_cuda_wrapper(): super().__init__(vllm_config, device) @@ -244,3 +247,11 @@ def _set_global_compilation_settings(config: VllmConfig): yield finally: torch_inductor_config.freezing = freezing_value + + +def _set_torch_accelerator_to_noop() -> None: + def noop(*args: Any, **kwargs: Any) -> None: + pass + + torch.accelerator.synchronize = noop + torch.accelerator.empty_cache = noop From 2ae73c758ceed55ad2f70a69b47c8a994fce5662 Mon Sep 17 00:00:00 2001 From: Jiangyun Zhu Date: Wed, 29 Apr 2026 12:18:46 +0800 Subject: [PATCH 029/237] [Bugfix] fix inductor error for dpsk v4 (#41135) Signed-off-by: zjy0516 --- .../fused_inv_rope_fp8_quant.py | 142 +++++++++++++----- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/vllm/v1/attention/ops/deepseek_v4_ops/fused_inv_rope_fp8_quant.py b/vllm/v1/attention/ops/deepseek_v4_ops/fused_inv_rope_fp8_quant.py index 97c9538889a1..84647d6120d8 100644 --- a/vllm/v1/attention/ops/deepseek_v4_ops/fused_inv_rope_fp8_quant.py +++ b/vllm/v1/attention/ops/deepseek_v4_ops/fused_inv_rope_fp8_quant.py @@ -10,6 +10,7 @@ import torch from vllm.triton_utils import tl, triton +from vllm.utils.torch_utils import direct_register_custom_op @triton.jit @@ -180,34 +181,74 @@ def fused_inv_rope_fp8_quant( fp8_dtype = torch.float8_e4m3fn fp8_max = torch.finfo(fp8_dtype).max - fp8_buf = torch.empty( - (n_groups, num_tokens, d), - dtype=fp8_dtype, - device=o.device, - ) - tma_aligned_T = get_tma_aligned_size(num_tokens, 4) if tma_aligned_scales: packed_sf_k = (num_scale_blocks + 3) // 4 - scale_buf = torch.empty( - n_groups * packed_sf_k * tma_aligned_T, - dtype=torch.int32, - device=o.device, - ).as_strided( - (n_groups, num_tokens, packed_sf_k), - (packed_sf_k * tma_aligned_T, 1, tma_aligned_T), - ) + scale_inner = packed_sf_k else: - scale_buf = torch.empty( - n_groups * num_scale_blocks * tma_aligned_T, - dtype=torch.float32, - device=o.device, - ).as_strided( - (n_groups, num_tokens, num_scale_blocks), - (num_scale_blocks * tma_aligned_T, 1, tma_aligned_T), - ) + scale_inner = num_scale_blocks + + # Run kernel through a custom op so inductor sees an opaque boundary. + # It's a pytorch bug, see https://github.com/vllm-project/vllm/issues/41106 + fp8_buf, scale_buf = torch.ops.vllm.fused_inv_rope_fp8_quant_kernel( + o, + positions, + cos_sin_cache, + heads_per_group, + quant_group_size, + chunks_per_head, + nope_dim % quant_group_size, + rope_dim // 2, + tma_aligned_scales, + fp8_max, + tma_aligned_T, + num_tokens, + n_groups, + d, + scale_inner, + ) + return fp8_buf.transpose(0, 1), scale_buf.transpose(0, 1) + - common_args = dict( +def _fused_inv_rope_fp8_quant_kernel_impl( + o: torch.Tensor, + positions: torch.Tensor, + cos_sin_cache: torch.Tensor, + heads_per_group: int, + quant_group_size: int, + chunks_per_head: int, + rope_start: int, + half_rope: int, + tma_aligned_scales: bool, + fp8_max: float, + tma_aligned_T: int, + num_tokens: int, + n_groups: int, + d: int, + scale_inner: int, +) -> tuple[torch.Tensor, torch.Tensor]: + fp8_buf = torch.empty( + (n_groups, num_tokens, d), + dtype=torch.float8_e4m3fn, + device=o.device, + ) + scale_dtype = torch.int32 if tma_aligned_scales else torch.float32 + scale_buf = torch.empty( + n_groups * scale_inner * tma_aligned_T, + dtype=scale_dtype, + device=o.device, + ).as_strided( + (n_groups, num_tokens, scale_inner), + (scale_inner * tma_aligned_T, 1, tma_aligned_T), + ) + grid = (tma_aligned_T, n_groups * heads_per_group) + _fused_inv_rope_fp8_quant_per_head[grid]( + o, + positions, + cos_sin_cache, + fp8_buf, + scale_buf, + num_tokens, heads_per_group=heads_per_group, o_stride_token=o.stride(0), o_stride_head=o.stride(1), @@ -220,23 +261,52 @@ def fused_inv_rope_fp8_quant( eps=1e-10, QUANT_GROUP_SIZE=quant_group_size, CHUNKS_PER_HEAD=chunks_per_head, - ROPE_START=nope_dim % quant_group_size, - HALF_ROPE=rope_dim // 2, + ROPE_START=rope_start, + HALF_ROPE=half_rope, TMA_ALIGNED_SCALES=tma_aligned_scales, num_stages=1, launch_pdl=False, + num_warps=1, ) + return fp8_buf, scale_buf - grid = (tma_aligned_T, n_groups * heads_per_group) - _fused_inv_rope_fp8_quant_per_head[grid]( - o, - positions, - cos_sin_cache, - fp8_buf, - scale_buf, - num_tokens, - **common_args, - num_warps=1, + +def _fused_inv_rope_fp8_quant_kernel_fake( + o: torch.Tensor, + positions: torch.Tensor, + cos_sin_cache: torch.Tensor, + heads_per_group: int, + quant_group_size: int, + chunks_per_head: int, + rope_start: int, + half_rope: int, + tma_aligned_scales: bool, + fp8_max: float, + tma_aligned_T: int, + num_tokens: int, + n_groups: int, + d: int, + scale_inner: int, +) -> tuple[torch.Tensor, torch.Tensor]: + fp8_buf = torch.empty( + (n_groups, num_tokens, d), + dtype=torch.float8_e4m3fn, + device=o.device, ) + scale_dtype = torch.int32 if tma_aligned_scales else torch.float32 + scale_buf = torch.empty( + n_groups * scale_inner * tma_aligned_T, + dtype=scale_dtype, + device=o.device, + ).as_strided( + (n_groups, num_tokens, scale_inner), + (scale_inner * tma_aligned_T, 1, tma_aligned_T), + ) + return fp8_buf, scale_buf - return fp8_buf.transpose(0, 1), scale_buf.transpose(0, 1) + +direct_register_custom_op( + op_name="fused_inv_rope_fp8_quant_kernel", + op_func=_fused_inv_rope_fp8_quant_kernel_impl, + fake_impl=_fused_inv_rope_fp8_quant_kernel_fake, +) From 8b49cf3a37eb1a267a06b0df23328909330af1e6 Mon Sep 17 00:00:00 2001 From: Wei Zhao <51183510+wzhao18@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:33:06 -0400 Subject: [PATCH 030/237] [Bugfix] Fix max_num_batched_token not captured in cuda graph (#40734) Signed-off-by: wzhao18 Signed-off-by: Wei Zhao <51183510+wzhao18@users.noreply.github.com> Co-authored-by: Wei Zhao (Engrg-Hardware 1) --- tests/compile/test_config.py | 3 +++ vllm/config/vllm.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/tests/compile/test_config.py b/tests/compile/test_config.py index bbb9cb1fcbcc..d822b68c5036 100644 --- a/tests/compile/test_config.py +++ b/tests/compile/test_config.py @@ -405,6 +405,9 @@ def test_should_split(): (None, 0, 1, False, 2048, CUDAGraphMode.NONE, 0), # truncated to nearest multiple of 8 or 16 (None, 257, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 256), + # max_num_batched_tokens <= max_cudagraph_capture_size should always be + # captured even if not landing on a 16-stride step + (None, 2048, 1, False, 257, CUDAGraphMode.FULL_AND_PIECEWISE, 257), # max from list ([1, 2, 4, 15], None, 1, False, 2048, CUDAGraphMode.FULL_AND_PIECEWISE, 15), # SP forces full-graph compilation, sizes are filtered by TP diff --git a/vllm/config/vllm.py b/vllm/config/vllm.py index f591605d08c7..56123542ce71 100644 --- a/vllm/config/vllm.py +++ b/vllm/config/vllm.py @@ -1432,6 +1432,10 @@ def _set_cudagraph_sizes(self): cudagraph_capture_sizes = [1, 2, 4] + list(range(8, 256, 8)) + list( range(256, max_graph_size + 1, 16)) + `max_num_batched_tokens` is also appended to the list if it fits + within `max_cudagraph_capture_size`, so the max batch size is captured + even when off-stride. + In the end, `vllm_config.compilation_config.cudagraph_capture_sizes` will be the final sizes to capture cudagraph (in ascending order). @@ -1520,6 +1524,12 @@ def _set_cudagraph_sizes(self): cudagraph_capture_sizes += list( range(256, max_cudagraph_capture_size + 1, 16) ) + # ensure max_num_tokens is captured if within max capture size + if ( + max_num_tokens <= max_cudagraph_capture_size + and max_num_tokens not in cudagraph_capture_sizes + ): + cudagraph_capture_sizes.append(max_num_tokens) # de-duplicate and sort the sizes cudagraph_capture_sizes = sorted(set(cudagraph_capture_sizes)) From a269744e9f733ec9bac4bb6a33f70cc5af38afc3 Mon Sep 17 00:00:00 2001 From: Jee Jee Li Date: Wed, 29 Apr 2026 13:42:35 +0800 Subject: [PATCH 031/237] [Bugfix] Fix rope (#41113) Signed-off-by: Jee Jee Li --- csrc/pos_encoding_kernels.cu | 73 +++++++++++---------- tests/kernels/core/test_rotary_embedding.py | 8 ++- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/csrc/pos_encoding_kernels.cu b/csrc/pos_encoding_kernels.cu index c45ebd34729b..d03c6a5cf0dd 100644 --- a/csrc/pos_encoding_kernels.cu +++ b/csrc/pos_encoding_kernels.cu @@ -7,23 +7,23 @@ namespace vllm { -template +template inline __device__ void apply_token_rotary_embedding( - scalar_t* __restrict__ arr, const float* __restrict__ cos_ptr, - const float* __restrict__ sin_ptr, int rot_offset, int embed_dim, + scalar_t* __restrict__ arr, const cache_t* __restrict__ cos_ptr, + const cache_t* __restrict__ sin_ptr, int rot_offset, int embed_dim, const bool inverse) { int x_index, y_index; float cos_f, sin_f; if (IS_NEOX) { x_index = rot_offset; y_index = embed_dim + rot_offset; - cos_f = VLLM_LDG(cos_ptr + x_index); - sin_f = VLLM_LDG(sin_ptr + x_index); + cos_f = static_cast(VLLM_LDG(cos_ptr + x_index)); + sin_f = static_cast(VLLM_LDG(sin_ptr + x_index)); } else { x_index = 2 * rot_offset; y_index = 2 * rot_offset + 1; - cos_f = VLLM_LDG(cos_ptr + x_index / 2); - sin_f = VLLM_LDG(sin_ptr + x_index / 2); + cos_f = static_cast(VLLM_LDG(cos_ptr + x_index / 2)); + sin_f = static_cast(VLLM_LDG(sin_ptr + x_index / 2)); } if (inverse) { sin_f = -sin_f; @@ -34,7 +34,7 @@ inline __device__ void apply_token_rotary_embedding( arr[y_index] = static_cast(y_f * cos_f + x_f * sin_f); } -template +template inline __device__ void apply_rotary_embedding( scalar_t* __restrict__ query, // [batch_size, seq_len, num_heads, // head_size] or [num_tokens, num_heads, @@ -43,14 +43,14 @@ inline __device__ void apply_rotary_embedding( // [batch_size, seq_len, num_kv_heads, // head_size] or [num_tokens, num_kv_heads, // head_size] - const float* cache_ptr, const int head_size, const int num_heads, + const cache_t* cache_ptr, const int head_size, const int num_heads, const int num_kv_heads, const int rot_dim, const int token_idx, const int64_t query_stride, const int64_t key_stride, const int64_t head_stride, const int64_t rope_dim_offset, const bool inverse) { const int embed_dim = rot_dim / 2; - const float* cos_ptr = cache_ptr; - const float* sin_ptr = cache_ptr + embed_dim; + const cache_t* cos_ptr = cache_ptr; + const cache_t* sin_ptr = cache_ptr + embed_dim; const int nq = num_heads * embed_dim; for (int i = threadIdx.x; i < nq; i += blockDim.x) { @@ -58,7 +58,7 @@ inline __device__ void apply_rotary_embedding( const int64_t token_head = token_idx * query_stride + head_idx * head_stride + rope_dim_offset; const int rot_offset = i % embed_dim; - apply_token_rotary_embedding( + apply_token_rotary_embedding( query + token_head, cos_ptr, sin_ptr, rot_offset, embed_dim, inverse); } @@ -69,13 +69,13 @@ inline __device__ void apply_rotary_embedding( const int64_t token_head = token_idx * key_stride + head_idx * head_stride + rope_dim_offset; const int rot_offset = i % embed_dim; - apply_token_rotary_embedding( + apply_token_rotary_embedding( key + token_head, cos_ptr, sin_ptr, rot_offset, embed_dim, inverse); } } } -template +template __global__ void rotary_embedding_kernel( const int64_t* __restrict__ positions, // [batch_size, seq_len] or // [num_tokens] @@ -86,15 +86,15 @@ __global__ void rotary_embedding_kernel( // [batch_size, seq_len, num_kv_heads, // head_size] or [num_tokens, num_kv_heads, // head_size] - const float* __restrict__ cos_sin_cache, // [max_position, rot_dim] fp32 + const cache_t* __restrict__ cos_sin_cache, // [max_position, rot_dim] const int rot_dim, const int64_t query_stride, const int64_t key_stride, const int64_t head_stride, const int num_heads, const int num_kv_heads, const int head_size, const int64_t rope_dim_offset, const bool inverse) { const int token_idx = blockIdx.x; int64_t pos = positions[token_idx]; - const float* cache_ptr = cos_sin_cache + pos * rot_dim; + const cache_t* cache_ptr = cos_sin_cache + pos * rot_dim; - apply_rotary_embedding( + apply_rotary_embedding( query, key, cache_ptr, head_size, num_heads, num_kv_heads, rot_dim, token_idx, query_stride, key_stride, head_stride, rope_dim_offset, inverse); @@ -168,23 +168,28 @@ void rotary_embedding( dim3 block(std::min(num_heads * rot_dim / 2, 512)); const at::cuda::OptionalCUDAGuard device_guard(device_of(query)); const cudaStream_t stream = at::cuda::getCurrentCUDAStream(); - auto cache_f32 = cos_sin_cache.to(torch::kFloat32); VLLM_DISPATCH_FLOATING_TYPES(query.scalar_type(), "rotary_embedding", [&] { - if (is_neox) { - vllm::rotary_embedding_kernel<<>>( - positions.data_ptr(), query.data_ptr(), - key.has_value() ? key->data_ptr() : nullptr, - cache_f32.data_ptr(), rot_dim, query_stride, key_stride, - head_stride, num_heads, num_kv_heads, head_size, rope_dim_offset, - inverse); - } else { - vllm::rotary_embedding_kernel - <<>>( - positions.data_ptr(), query.data_ptr(), - key.has_value() ? key->data_ptr() : nullptr, - cache_f32.data_ptr(), rot_dim, query_stride, key_stride, - head_stride, num_heads, num_kv_heads, head_size, rope_dim_offset, - inverse); - } + using query_t = scalar_t; + VLLM_DISPATCH_FLOATING_TYPES( + cos_sin_cache.scalar_type(), "rotary_embedding_cache", [&] { + using cache_t = scalar_t; + if (is_neox) { + vllm::rotary_embedding_kernel + <<>>( + positions.data_ptr(), query.data_ptr(), + key.has_value() ? key->data_ptr() : nullptr, + cos_sin_cache.data_ptr(), rot_dim, query_stride, + key_stride, head_stride, num_heads, num_kv_heads, head_size, + rope_dim_offset, inverse); + } else { + vllm::rotary_embedding_kernel + <<>>( + positions.data_ptr(), query.data_ptr(), + key.has_value() ? key->data_ptr() : nullptr, + cos_sin_cache.data_ptr(), rot_dim, query_stride, + key_stride, head_stride, num_heads, num_kv_heads, head_size, + rope_dim_offset, inverse); + } + }); }); } diff --git a/tests/kernels/core/test_rotary_embedding.py b/tests/kernels/core/test_rotary_embedding.py index 6cdd94fdc865..8410d1f1bcc6 100644 --- a/tests/kernels/core/test_rotary_embedding.py +++ b/tests/kernels/core/test_rotary_embedding.py @@ -35,6 +35,9 @@ def rotary_embedding_opcheck( @pytest.mark.parametrize("seq_len", [11, 1024]) @pytest.mark.parametrize("use_key", [True, False]) @pytest.mark.parametrize("head_stride_is_contiguous", [True, False]) +@pytest.mark.parametrize( + "dtype", [torch.float32, torch.bfloat16] +) def test_rotary_embedding_opcheck( default_vllm_config, dist_init, @@ -46,19 +49,20 @@ def test_rotary_embedding_opcheck( seq_len, use_key, head_stride_is_contiguous, + dtype, ): batch_size = 1 base = 10000 num_heads = 7 rot = RotaryEmbedding( - head_size, rotary_dim, max_position, base, is_neox_style, torch.float32 + head_size, rotary_dim, max_position, base, is_neox_style, dtype ) positions = torch.randint(0, max_position, (batch_size, seq_len), device=device) head_stride = head_size + (64 if head_stride_is_contiguous else 0) query = torch.randn( - batch_size, seq_len, num_heads, head_stride, dtype=torch.float32, device=device + batch_size, seq_len, num_heads, head_stride, dtype=dtype, device=device ) key = torch.randn_like(query) if use_key else None query = query[..., :head_size] From 8a8c9b564ef015c76cf398200b8f0891e6e51bb8 Mon Sep 17 00:00:00 2001 From: Itay Etelis <92247226+Etelis@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:52:55 +0300 Subject: [PATCH 032/237] [KV Offload] Per-job store completion for CPU offloading connector (#39186) Signed-off-by: Itay Etelis Signed-off-by: Itay Etelis <92247226+Etelis@users.noreply.github.com> Co-authored-by: Itay Etelis Co-authored-by: Or Ozeri Co-authored-by: Or Ozeri --- .../offloading_connector/test_scheduler.py | 134 ++++++++++++ .../test_worker_metadata.py | 32 +++ .../unit/offloading_connector/utils.py | 34 ++- tests/v1/kv_connector/unit/utils.py | 4 + .../kv_connector/v1/offloading/common.py | 55 ++++- .../kv_connector/v1/offloading/scheduler.py | 204 +++++++++++++----- .../kv_connector/v1/offloading/worker.py | 106 +++------ .../kv_connector/v1/offloading_connector.py | 6 + 8 files changed, 430 insertions(+), 145 deletions(-) create mode 100644 tests/v1/kv_connector/unit/offloading_connector/test_worker_metadata.py diff --git a/tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py b/tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py index 8d2c45f7bd20..3ea935b090df 100644 --- a/tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py +++ b/tests/v1/kv_connector/unit/offloading_connector/test_scheduler.py @@ -232,6 +232,9 @@ def test_request_preemption(request_runner, async_scheduling: bool): expected_stored_gpu_block_indexes=(9, 10, 11), ) + # All stores completed before request_finished -> fence index empty. + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + @pytest.mark.parametrize("async_scheduling", [True, False]) def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling: bool): @@ -292,6 +295,9 @@ def test_concurrent_lookups_of_the_same_prefix(request_runner, async_scheduling: # second request will use the GPU prefix cache assert transfer_jobs == list(runner.offloading_spec.handler.transfer_specs) + # Fence index drained: stores completed before request_finished ran. + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + @pytest.mark.parametrize("async_scheduling", [True, False]) def test_abort_loading_requests(request_runner, async_scheduling: bool): @@ -534,3 +540,131 @@ def test_do_remote_decode_stores_all_blocks(request_runner, async_scheduling: bo decoded_tokens=[EOS_TOKEN_ID], expected_stored_gpu_block_indexes=(0, 1, 2, 3, 4, 5), ) + # All stores completed before request_finished -> fence index empty. + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + + +# --------------------------------------------------------------------------- +# Tests for the per-job-store-completion design and fence invariants. +# --------------------------------------------------------------------------- + + +def test_loads_do_not_populate_fence_index(request_runner): + """Loads don't populate _block_id_to_pending_jobs (protected by + delay_free_blocks while in flight).""" + runner = request_runner( + offloaded_block_size=12, + gpu_block_size=4, + num_gpu_blocks=100, + async_scheduling=False, + ) + runner.new_request(token_ids=[0] * 12) + runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1 + runner.run(decoded_tokens=[], complete_transfers=False) + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + + +def test_fence_at_update_state_after_alloc(request_runner): + """A load reusing a finished request's pending-store block triggers + a flush via update_state_after_alloc's fence. + + num_gpu_blocks=2 forces the BlockPool to give req2 the same block + req1 just freed. + """ + runner = request_runner( + offloaded_block_size=4, + gpu_block_size=4, + num_gpu_blocks=2, + async_scheduling=False, + ) + + runner.new_request(token_ids=[0] * 4) + runner.manager.prepare_store.side_effect = ( + lambda keys, req_context: generate_store_output(keys) + ) + runner.run(decoded_tokens=[EOS_TOKEN_ID], complete_transfers=False) + assert runner.connector_scheduler._block_id_to_pending_jobs + + runner.scheduler.reset_prefix_cache() + runner.new_request(token_ids=[0] * 4) + runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 1 + runner.manager.prepare_store.side_effect = ( + lambda keys, req_context: generate_store_output([]) + ) + runner.run( + decoded_tokens=[], + complete_transfers=False, + expected_stored_gpu_block_indexes=(0,), + expected_flushed_gpu_block_indexes=(0,), + ) + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + + +def test_fence_at_build_store_jobs(request_runner): + """A new prefill (no load -> update_state_after_alloc returns early) + reusing a finished request's pending-store block is flushed by + _build_store_jobs's fence.""" + runner = request_runner( + offloaded_block_size=4, + gpu_block_size=4, + num_gpu_blocks=2, + async_scheduling=False, + ) + + runner.new_request(token_ids=[0] * 4) + runner.manager.prepare_store.side_effect = ( + lambda keys, req_context: generate_store_output(keys) + ) + runner.run(decoded_tokens=[EOS_TOKEN_ID], complete_transfers=False) + assert runner.connector_scheduler._block_id_to_pending_jobs + + runner.scheduler.reset_prefix_cache() + runner.new_request(token_ids=[1] * 4) + runner.connector_scheduler._maximal_prefix_lookup = lambda key, req_context: 0 + runner.manager.prepare_store.side_effect = ( + lambda keys, req_context: generate_store_output([]) + ) + runner.run( + decoded_tokens=[EOS_TOKEN_ID], + expected_stored_gpu_block_indexes=(0,), + expected_flushed_gpu_block_indexes=(0,), + ) + assert runner.connector_scheduler._block_id_to_pending_jobs == {} + + +@pytest.mark.parametrize("async_scheduling", [True, False]) +def test_complete_store_called_per_job(request_runner, async_scheduling: bool): + """complete_store fires per-job, not deferred to request finish. + Each call carries only that store's keys.""" + offloaded_block_size = 12 + runner = request_runner( + offloaded_block_size=offloaded_block_size, + gpu_block_size=4, + num_gpu_blocks=100, + async_scheduling=async_scheduling, + ) + runner.new_request(token_ids=[0] * offloaded_block_size) + runner.manager.prepare_store.side_effect = ( + lambda keys, req_context: generate_store_output(keys) + ) + + # First store: fires when block 0 is fully populated. + runner.run(decoded_tokens=[0, 0], expected_stored_gpu_block_indexes=(0, 1, 2)) + assert runner.manager.complete_store.call_count == 1 + first_call_keys = set(runner.manager.complete_store.call_args.args[0]) + assert len(first_call_keys) == 1 + runner.manager.complete_store.reset_mock() + + # Second store: fires when block 1 is fully populated, with different keys. + runner.run( + decoded_tokens=[0] * (offloaded_block_size + 1), + expected_stored_gpu_block_indexes=(3, 4, 5), + ) + assert runner.manager.complete_store.call_count == 1 + second_call_keys = set(runner.manager.complete_store.call_args.args[0]) + assert first_call_keys != second_call_keys + runner.manager.complete_store.reset_mock() + + # Finish: no store pending -> no further call. + runner.run(decoded_tokens=[EOS_TOKEN_ID]) + assert runner.manager.complete_store.call_count == 0 diff --git a/tests/v1/kv_connector/unit/offloading_connector/test_worker_metadata.py b/tests/v1/kv_connector/unit/offloading_connector/test_worker_metadata.py new file mode 100644 index 000000000000..ab9d676cb4ae --- /dev/null +++ b/tests/v1/kv_connector/unit/offloading_connector/test_worker_metadata.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import pytest + +from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import ( + OffloadingWorkerMetadata, +) + +pytestmark = pytest.mark.cpu_test + + +def test_aggregate_sums_counts(): + meta1 = OffloadingWorkerMetadata(completed_jobs={42: 1, 7: 1}) + meta2 = OffloadingWorkerMetadata(completed_jobs={42: 1, 7: 1}) + result = meta1.aggregate(meta2) + assert result.completed_jobs == {42: 2, 7: 2} + + +def test_aggregate_disjoint_jobs(): + meta1 = OffloadingWorkerMetadata(completed_jobs={42: 1, 7: 1}) + meta2 = OffloadingWorkerMetadata(completed_jobs={43: 1, 8: 1}) + result = meta1.aggregate(meta2) + assert result.completed_jobs == {42: 1, 7: 1, 43: 1, 8: 1} + + +def test_aggregate_multiple_workers(): + meta1 = OffloadingWorkerMetadata(completed_jobs={42: 1, 43: 1, 7: 1}) + meta2 = OffloadingWorkerMetadata(completed_jobs={42: 1, 7: 1, 8: 1}) + meta3 = OffloadingWorkerMetadata(completed_jobs={42: 1, 43: 1, 8: 1}) + result = meta1.aggregate(meta2).aggregate(meta3) + assert result.completed_jobs == {42: 3, 43: 2, 7: 2, 8: 2} diff --git a/tests/v1/kv_connector/unit/offloading_connector/utils.py b/tests/v1/kv_connector/unit/offloading_connector/utils.py index 60dc11f4ca4b..d5adcd3f7724 100644 --- a/tests/v1/kv_connector/unit/offloading_connector/utils.py +++ b/tests/v1/kv_connector/unit/offloading_connector/utils.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -import copy from collections.abc import Iterable, Iterator from dataclasses import dataclass from typing import Any @@ -19,6 +18,7 @@ from vllm.distributed.kv_transfer.kv_connector.v1 import KVConnectorRole from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import ( OffloadingConnectorMetadata, + OffloadingWorkerMetadata, ) from vllm.distributed.kv_transfer.kv_connector.v1.offloading_connector import ( OffloadingConnector, @@ -51,7 +51,6 @@ TransferResult, TransferSpec, ) -from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT, KVConnectorOutput from vllm.v1.request import Request from vllm.v1.structured_output import StructuredOutputManager @@ -369,7 +368,12 @@ def _run(self, decoded_tokens: list[int], complete_transfers: bool): prev_scheduler_output = None prev_model_runner_output = None while True: - assert self.scheduler.requests + # Strict-always-False frees the request immediately on EOS, but + # the worker may still have a deferred store queued. In production + # the next request's step drains it; in single-request tests we + # must keep stepping until the scheduler sees no in-flight jobs. + if not self.scheduler.requests and not self.connector_scheduler._jobs: + break scheduler_output = self.scheduler.schedule() self._update_gpu_block_idx() @@ -392,6 +396,10 @@ def _run(self, decoded_tokens: list[int], complete_transfers: bool): finished_sending, finished_recving = self.worker_connector.get_finished( scheduler_output.finished_req_ids ) + worker_meta = ( + self.worker_connector.build_connector_worker_meta() + or OffloadingWorkerMetadata() + ) self.worker_connector.clear_connector_metadata() @@ -400,6 +408,7 @@ def _run(self, decoded_tokens: list[int], complete_transfers: bool): finished_sending=finished_sending, finished_recving=finished_recving, token_id=token_id or 0, + kv_connector_worker_meta=worker_meta, ) prev_token_id = token_id @@ -420,7 +429,7 @@ def _run(self, decoded_tokens: list[int], complete_transfers: bool): if ( prev_token_id == EOS_TOKEN_ID and prev_token_id != token_id - and self.scheduler.requests + and (self.scheduler.requests or self.connector_scheduler._jobs) ): # continue for one more step to allow offloading to kick off continue @@ -435,26 +444,9 @@ def _run(self, decoded_tokens: list[int], complete_transfers: bool): self._parse_transfers() - # run one more step to update finished stored if EOS_TOKEN_ID in decoded_tokens: assert not self.scheduler.running - while self.scheduler.requests: - scheduler_output = self.scheduler.schedule() - - finished_sending, finished_recving = self.worker_connector.get_finished( - scheduler_output.finished_req_ids - ) - - assert not finished_recving - - model_runner_output = copy.deepcopy(EMPTY_MODEL_RUNNER_OUTPUT) - model_runner_output.kv_connector_output = KVConnectorOutput( - finished_sending=finished_sending - ) - - self.scheduler.update_from_output(scheduler_output, model_runner_output) - def run( self, decoded_tokens: list[int], diff --git a/tests/v1/kv_connector/unit/utils.py b/tests/v1/kv_connector/unit/utils.py index 5f0036807b0c..0710ffa63a81 100644 --- a/tests/v1/kv_connector/unit/utils.py +++ b/tests/v1/kv_connector/unit/utils.py @@ -24,6 +24,7 @@ KVConnectorBase_V1, KVConnectorMetadata, KVConnectorRole, + KVConnectorWorkerMetadata, ) from vllm.distributed.kv_transfer.kv_connector.v1.example_connector import ( # noqa ExampleConnector, @@ -249,6 +250,7 @@ def create_model_runner_output( invalid_block_ids: set[int] | None = None, use_eos: bool = False, token_id: int = 0, + kv_connector_worker_meta: KVConnectorWorkerMetadata | None = None, ) -> ModelRunnerOutput: """Make dummy model runner output for testing.""" @@ -266,11 +268,13 @@ def create_model_runner_output( finished_sending is None and finished_recving is None and invalid_block_ids is None + and kv_connector_worker_meta is None ) else KVConnectorOutput( finished_sending=finished_sending, finished_recving=finished_recving, invalid_block_ids=invalid_block_ids or set(), + kv_connector_worker_meta=kv_connector_worker_meta, ) ) diff --git a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/common.py b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/common.py index 06a727a27b55..c5a251a2a515 100644 --- a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/common.py +++ b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/common.py @@ -1,15 +1,60 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -from dataclasses import dataclass +from dataclasses import dataclass, field -from vllm.distributed.kv_transfer.kv_connector.v1.base import KVConnectorMetadata +from vllm.distributed.kv_transfer.kv_connector.v1.base import ( + KVConnectorMetadata, + KVConnectorWorkerMetadata, +) from vllm.v1.kv_offload.worker.worker import TransferSpec ReqId = str +@dataclass +class TransferJob: + """A transfer job bundling request context with transfer spec. + + Used for both loads and stores, keyed by scheduler-assigned job ID. + The worker reports the job ID back when the transfer finishes, + and the scheduler processes the completion. + """ + + req_id: ReqId + transfer_spec: TransferSpec + + @dataclass class OffloadingConnectorMetadata(KVConnectorMetadata): - reqs_to_load: dict[ReqId, TransferSpec] - reqs_to_store: dict[ReqId, TransferSpec] - reqs_to_flush: set[str] | None = None + # Keyed by scheduler-assigned job IDs. + load_jobs: dict[int, TransferJob] + store_jobs: dict[int, TransferJob] + jobs_to_flush: set[int] | None = None + + +@dataclass +class OffloadingWorkerMetadata(KVConnectorWorkerMetadata): + """Worker -> Scheduler metadata for completed transfer jobs. + + Each worker reports {job_id: 1} for newly completed transfer jobs + (load or store). aggregate() sums counts across workers within a step. + The scheduler accumulates across steps and processes + a transfer completion only when count reaches num_workers. + """ + + completed_jobs: dict[int, int] = field(default_factory=dict) + + def mark_completed(self, job_id: int) -> None: + """Record a transfer job completion from this worker.""" + self.completed_jobs[job_id] = 1 + + def aggregate( + self, other: "KVConnectorWorkerMetadata" + ) -> "KVConnectorWorkerMetadata": + assert isinstance(other, OffloadingWorkerMetadata) + + merged = dict(self.completed_jobs) + for job_id, v in other.completed_jobs.items(): + merged[job_id] = merged.get(job_id, 0) + v + + return OffloadingWorkerMetadata(completed_jobs=merged) diff --git a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py index 1ef99eaa4461..cb8af41cdd69 100644 --- a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py +++ b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py @@ -1,6 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -from collections import defaultdict from collections.abc import Iterable, Sequence from dataclasses import dataclass, field from itertools import islice @@ -11,7 +10,9 @@ from vllm.distributed.kv_transfer.kv_connector.v1.base import KVConnectorMetadata from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import ( OffloadingConnectorMetadata, + OffloadingWorkerMetadata, ReqId, + TransferJob, ) from vllm.logger import init_logger from vllm.utils.math_utils import cdiv @@ -26,13 +27,27 @@ ) from vllm.v1.kv_offload.mediums import GPULoadStoreSpec from vllm.v1.kv_offload.spec import OffloadingSpec -from vllm.v1.kv_offload.worker.worker import TransferSpec from vllm.v1.outputs import KVConnectorOutput from vllm.v1.request import Request logger = init_logger(__name__) +@dataclass(slots=True) +class TransferJobStatus: + """Tracks scheduler-side state for a single transfer job.""" + + req_id: ReqId + # Number of workers still pending. Starts at num_workers, + # decremented as each worker reports completion. Job is done at 0. + pending_count: int + # Offload keys this job covers; passed to manager.complete_*(). + keys: set[OffloadKey] + is_store: bool + # GPU blocks the fence tracks. Store src blocks; None for loads. + gpu_block_ids: list[int] | None = None + + class GroupOffloadConfig(NamedTuple): group_idx: int gpu_block_size: int @@ -43,10 +58,12 @@ class GroupOffloadConfig(NamedTuple): class SchedulerOffloadConfig(NamedTuple): kv_group_configs: tuple[GroupOffloadConfig, ...] block_size_factor: int + num_workers: int @classmethod def from_spec(cls, spec: OffloadingSpec) -> "SchedulerOffloadConfig": return cls( + num_workers=spec.vllm_config.parallel_config.world_size, kv_group_configs=tuple( GroupOffloadConfig( group_idx=idx, @@ -79,6 +96,9 @@ class RequestOffloadState: req_context: ReqContext = field(init=False) # number of hits in the GPU cache num_locally_computed_tokens: int = 0 + # In-flight job IDs. Per the connector's invariant, at any given time + # this contains either a single load job, or one or more store jobs. + transfer_jobs: set[int] = field(default_factory=set) def __post_init__(self) -> None: self.group_states = tuple( @@ -135,17 +155,26 @@ def __init__(self, spec: OffloadingSpec): self.lookup_groups = attention_groups self._req_status: dict[ReqId, RequestOffloadState] = {} - # requests to load for the current scheduler step - self._reqs_to_load: dict[ReqId, TransferSpec] = {} + self._current_batch_load_jobs: dict[int, TransferJob] = {} + self._current_batch_jobs_to_flush: set[int] = set() # if GPU prefix caching is enabled, # track loaded blocks to avoid redundant loads self._blocks_being_loaded: set[OffloadKey] | None = ( set() if spec.vllm_config.cache_config.enable_prefix_caching else None ) - # request ID -> set(offload keys being stored/loaded) - self._reqs_being_stored = defaultdict[ReqId, set[OffloadKey]](set) - self._reqs_being_loaded = defaultdict[ReqId, set[OffloadKey]](set) + # Job ID counter shared by loads and stores. + self._job_counter: int = 0 + self._jobs: dict[int, TransferJobStatus] = {} + + # block_id -> pending store job_ids. Populated only for finished + # requests (running-request blocks are protected by their ref_cnt). + self._block_id_to_pending_jobs: dict[int, set[int]] = {} + + def _generate_job_id(self) -> int: + job_id = self._job_counter + self._job_counter += 1 + return job_id def _maximal_prefix_lookup( self, keys: Iterable[OffloadKey], req_context: ReqContext @@ -369,23 +398,46 @@ def update_state_after_alloc( # entire KV cache so a remote decode node can consume it. group_state.next_stored_block_idx = num_blocks + # Fence dst blocks against finished-request pending stores. + if ( + self._block_id_to_pending_jobs + and not self._block_id_to_pending_jobs.keys().isdisjoint(dst_block_ids) + ): + self._current_batch_jobs_to_flush.update( + jid + for bid in dst_block_ids + for jid in self._block_id_to_pending_jobs.get(bid, ()) + ) + src_spec = self.manager.prepare_load(keys_to_load, req_status.req_context) dst_spec = GPULoadStoreSpec( dst_block_ids, group_sizes=group_sizes, block_indices=block_indices ) - self._reqs_to_load[request.request_id] = (src_spec, dst_spec) - req_blocks_being_loaded = self._reqs_being_loaded[request.request_id] - req_blocks_being_loaded.update(keys_to_load) + load_job_id = self._generate_job_id() + self._current_batch_load_jobs[load_job_id] = TransferJob( + req_id=request.request_id, + transfer_spec=(src_spec, dst_spec), + ) + # a load can only be issued when no other jobs are pending. + assert not req_status.transfer_jobs + req_status.transfer_jobs.add(load_job_id) + self._jobs[load_job_id] = TransferJobStatus( + req_id=request.request_id, + pending_count=self.config.num_workers, + keys=set(keys_to_load), + is_store=False, + ) if self._blocks_being_loaded is not None: - self._blocks_being_loaded.update(req_blocks_being_loaded) + self._blocks_being_loaded.update(keys_to_load) - def _get_reqs_to_store( - self, scheduler_output: SchedulerOutput - ) -> dict[ReqId, TransferSpec]: + def _build_store_jobs( + self, + scheduler_output: SchedulerOutput, + ) -> dict[int, TransferJob]: block_size_factor = self.config.block_size_factor - reqs_to_store: dict[ReqId, TransferSpec] = {} + store_jobs: dict[int, TransferJob] = {} # iterate over both new and cached requests for req_id, new_block_id_groups, preempted in yield_req_data(scheduler_output): req_status = self._req_status[req_id] @@ -398,6 +450,19 @@ def _get_reqs_to_store( if new_block_id_groups: req_status.update_block_id_groups(new_block_id_groups) + # Fence new blocks against in-flight stores. + if self._block_id_to_pending_jobs: + new_blocks_flat = [ + bid for new_blocks in new_block_id_groups for bid in new_blocks + ] + if not self._block_id_to_pending_jobs.keys().isdisjoint( + new_blocks_flat + ): + self._current_batch_jobs_to_flush.update( + jid + for bid in new_blocks_flat + for jid in self._block_id_to_pending_jobs.get(bid, ()) + ) num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id] num_tokens_after_batch = req.num_computed_tokens + num_scheduled_tokens @@ -491,36 +556,52 @@ def _get_reqs_to_store( ) dst_spec = store_output.store_spec - reqs_to_store[req_id] = (src_spec, dst_spec) - self._reqs_being_stored[req_id] |= keys_to_store + job_id = self._generate_job_id() + # a store can only be issued when no load is pending. + if req_status.transfer_jobs: + any_jid = next(iter(req_status.transfer_jobs)) + assert self._jobs[any_jid].is_store + req_status.transfer_jobs.add(job_id) + self._jobs[job_id] = TransferJobStatus( + req_id=req_id, + pending_count=self.config.num_workers, + keys=set(keys_to_store), + is_store=True, + gpu_block_ids=src_block_ids, + ) + + store_jobs[job_id] = TransferJob( + req_id=req_id, transfer_spec=(src_spec, dst_spec) + ) logger.debug( - "Request %s offloading %s blocks upto %d tokens", + "Request %s offloading %s blocks upto %d tokens (job %d)", req_id, len(keys_to_store), num_offloadable_tokens, + job_id, ) - return reqs_to_store + return store_jobs def build_connector_meta( self, scheduler_output: SchedulerOutput ) -> KVConnectorMetadata: - meta = OffloadingConnectorMetadata( - reqs_to_load=self._reqs_to_load, - reqs_to_store=self._get_reqs_to_store(scheduler_output), - reqs_to_flush=scheduler_output.preempted_req_ids, - ) - self._reqs_to_load = {} - - # NOTE (orozery): we should move this logic to update_connector_output - # once KVConnectorOutput allows us to report completed transfers for req_id in scheduler_output.preempted_req_ids or (): - keys = self._reqs_being_stored.get(req_id) - if keys: - self.manager.complete_store(keys) - keys.clear() + req_status = self._req_status.get(req_id) + if req_status is None or not req_status.transfer_jobs: + continue + any_jid = next(iter(req_status.transfer_jobs)) + assert self._jobs[any_jid].is_store + self._current_batch_jobs_to_flush.update(req_status.transfer_jobs) + meta = OffloadingConnectorMetadata( + load_jobs=self._current_batch_load_jobs, + store_jobs=self._build_store_jobs(scheduler_output), + jobs_to_flush=self._current_batch_jobs_to_flush, + ) + self._current_batch_load_jobs = {} + self._current_batch_jobs_to_flush = set() return meta def update_connector_output(self, connector_output: KVConnectorOutput): @@ -531,17 +612,37 @@ def update_connector_output(self, connector_output: KVConnectorOutput): connector_output (KVConnectorOutput): the worker-side connectors output. """ - for req_id in connector_output.finished_sending or []: - keys = self._reqs_being_stored.pop(req_id, None) - if keys: - self.manager.complete_store(keys) - - for req_id in connector_output.finished_recving or []: - keys = self._reqs_being_loaded.pop(req_id, None) - if keys: + meta = connector_output.kv_connector_worker_meta + if not isinstance(meta, OffloadingWorkerMetadata): + assert meta is None + meta = OffloadingWorkerMetadata() + for job_id, count in meta.completed_jobs.items(): + assert count > 0 + job_status = self._jobs[job_id] + job_status.pending_count -= count + if job_status.pending_count > 0: + continue + assert job_status.pending_count == 0 + + if job_status.is_store: + self.manager.complete_store(job_status.keys) + else: + self.manager.complete_load(job_status.keys) if self._blocks_being_loaded: - self._blocks_being_loaded.difference_update(keys) - self.manager.complete_load(keys) + self._blocks_being_loaded.difference_update(job_status.keys) + + req_status = self._req_status[job_status.req_id] + if self._block_id_to_pending_jobs and req_status.req.is_finished(): + for bid in job_status.gpu_block_ids or (): + pending = self._block_id_to_pending_jobs[bid] + pending.remove(job_id) + if not pending: + del self._block_id_to_pending_jobs[bid] + + del self._jobs[job_id] + req_status.transfer_jobs.remove(job_id) + if not req_status.transfer_jobs and req_status.req.is_finished(): + del self._req_status[job_status.req_id] def request_finished( self, @@ -558,14 +659,21 @@ def request_finished( Optional KVTransferParams to be included in the request outputs returned by the engine. """ - req_id = request.request_id - # TODO(orozery): possibly kickoff offload for last block # which may have been deferred due to async scheduling - self._req_status.pop(req_id, None) - - request_being_stored = req_id in self._reqs_being_stored - return request_being_stored, None + req_status = self._req_status.get(request.request_id) + if req_status is None: + return False, None + if not req_status.transfer_jobs: + del self._req_status[request.request_id] + return False, None + # Pending stores will outlive the request's block ownership. + # Register them so future block reuse triggers a flush. + for job_id in req_status.transfer_jobs: + job_status = self._jobs[job_id] + for bid in job_status.gpu_block_ids or (): + self._block_id_to_pending_jobs.setdefault(bid, set()).add(job_id) + return False, None def take_events(self) -> Iterable[KVCacheEvent]: """Take the KV cache events from the connector. diff --git a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/worker.py b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/worker.py index cc6d8262c7e6..78547d569df3 100644 --- a/vllm/distributed/kv_transfer/kv_connector/v1/offloading/worker.py +++ b/vllm/distributed/kv_transfer/kv_connector/v1/offloading/worker.py @@ -11,6 +11,7 @@ ) from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import ( OffloadingConnectorMetadata, + OffloadingWorkerMetadata, ReqId, ) from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import ( @@ -45,24 +46,11 @@ def __init__(self, spec: OffloadingSpec): self.spec = spec self.worker = OffloadingWorker() - self._job_counter = 0 - self.kv_connector_stats = OffloadingConnectorStats() - # req_id -> (job_id, store) - self._jobs: dict[int, tuple[ReqId, bool]] = {} - # req_id -> active job IDs - self._load_job: dict[ReqId, int] = {} - # req_id -> set(active job IDs) - self._store_jobs = defaultdict[ReqId, set[int]](set) - # list of store jobs pending submission (job_id, transfer_spec) + # job_id -> req_id for in-flight loads. + self._load_jobs: dict[int, ReqId] = {} self._unsubmitted_store_jobs: list[tuple[int, TransferSpec]] = [] - - self._finished_reqs_waiting_for_store: set[ReqId] = set() - - def _generate_job_id(self) -> int: - job_id = self._job_counter - self._job_counter = job_id + 1 - return job_id + self._connector_worker_meta = OffloadingWorkerMetadata() def _register_handlers(self, kv_caches: CanonicalKVCaches): for src_cls, dst_cls, handler in self.spec.get_handlers(kv_caches): @@ -301,10 +289,8 @@ def handle_preemptions(self, kv_connector_metadata: OffloadingConnectorMetadata) assert success self._unsubmitted_store_jobs.clear() - for req_id in kv_connector_metadata.reqs_to_flush or (): - job_ids = self._store_jobs.get(req_id) - if job_ids: - self.worker.wait(job_ids) + if kv_connector_metadata.jobs_to_flush: + self.worker.wait(kv_connector_metadata.jobs_to_flush) def start_kv_transfers(self, metadata: OffloadingConnectorMetadata): for job_id, transfer_spec in self._unsubmitted_store_jobs: @@ -312,41 +298,33 @@ def start_kv_transfers(self, metadata: OffloadingConnectorMetadata): assert success self._unsubmitted_store_jobs.clear() - for req_id, transfer_spec in metadata.reqs_to_load.items(): - job_id = self._generate_job_id() - self._jobs[job_id] = (req_id, False) - assert req_id not in self._load_job - self._load_job[req_id] = job_id - success = self.worker.transfer_async(job_id, transfer_spec) + for job_id, entry in metadata.load_jobs.items(): + self._load_jobs[job_id] = entry.req_id + success = self.worker.transfer_async(job_id, entry.transfer_spec) assert success def prepare_store_kv(self, metadata: OffloadingConnectorMetadata): - for req_id, transfer_spec in metadata.reqs_to_store.items(): - job_id = self._generate_job_id() - self._jobs[job_id] = (req_id, True) - self._store_jobs[req_id].add(job_id) - # NOTE(orozery): defer the store to the beginning of the next engine step, - # so that offloading starts AFTER transfers related to token sampling, - # thereby avoiding delays to token generation due to offloading. - self._unsubmitted_store_jobs.append((job_id, transfer_spec)) + for job_id, entry in metadata.store_jobs.items(): + # NOTE(orozery): defer the store to the beginning of the next + # engine step, so that offloading starts AFTER transfers related + # to token sampling, thereby avoiding delays to token generation. + self._unsubmitted_store_jobs.append((job_id, entry.transfer_spec)) def get_finished(self, finished_req_ids: set[str]) -> tuple[set[str], set[str]]: """ - Notifies worker-side connector ids of requests that have - finished generating tokens. - Returns a list of request IDs that finished loading or storing. - Returns: - ids of requests that have finished asynchronous transfer - tuple of (sending/saving ids, recving/loading ids). + tuple of (finished_sending, finished_recving). Stores never + emit finished_sending — the scheduler tracks store completion + via kv_connector_worker_meta.completed_jobs and fences any + block reuse via jobs_to_flush. Loads still emit + finished_recving so the base scheduler can resume requests + blocked on remote KV (and free aborted-during-load reqs). """ - finished_sending = set() - finished_recving = set() + finished_recving: set[str] = set() for transfer_result in self.worker.get_finished(): # we currently do not support job failures job_id = transfer_result.job_id assert transfer_result.success - req_id, store = self._jobs.pop(job_id) if ( transfer_result.transfer_time and transfer_result.transfer_size is not None @@ -357,31 +335,21 @@ def get_finished(self, finished_req_ids: set[str]) -> tuple[set[str], set[str]]: time=transfer_result.transfer_time, transfer_type=transfer_result.transfer_type, ) - if store: - req_jobs = self._store_jobs[req_id] - req_jobs.remove(job_id) - if req_jobs: - continue - - if req_id in self._finished_reqs_waiting_for_store: - self._finished_reqs_waiting_for_store.remove(req_id) - finished_sending.add(req_id) - del self._store_jobs[req_id] - else: - req_job = self._load_job[req_id] - assert job_id == req_job - del self._load_job[req_id] + + self._connector_worker_meta.mark_completed(job_id) + req_id = self._load_jobs.pop(job_id, None) + if req_id is not None: finished_recving.add(req_id) - for req_id in finished_req_ids: - pending_req_jobs = self._store_jobs.get(req_id) - if pending_req_jobs: - self._finished_reqs_waiting_for_store.add(req_id) - elif pending_req_jobs is not None: - finished_sending.add(req_id) - del self._store_jobs[req_id] + return set(), finished_recving - return finished_sending, finished_recving + def build_connector_worker_meta(self) -> OffloadingWorkerMetadata | None: + """Return completed transfer job IDs since the last call.""" + if not self._connector_worker_meta.completed_jobs: + return None + meta = self._connector_worker_meta + self._connector_worker_meta = OffloadingWorkerMetadata() + return meta def get_kv_connector_stats(self) -> KVConnectorStats | None: """ @@ -396,11 +364,7 @@ def get_kv_connector_stats(self) -> KVConnectorStats | None: return kv_connector_stats def shutdown(self) -> None: - # Drop deferred store jobs: there is no point in submitting - # them during shutdown. self._unsubmitted_store_jobs.clear() - self._jobs.clear() - self._load_job.clear() - self._store_jobs.clear() - self._finished_reqs_waiting_for_store.clear() + self._load_jobs.clear() + self._connector_worker_meta = OffloadingWorkerMetadata() self.worker.shutdown() diff --git a/vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py b/vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py index f11281dcf14e..05b835572c9f 100644 --- a/vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py +++ b/vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py @@ -20,6 +20,7 @@ ) from vllm.distributed.kv_transfer.kv_connector.v1.offloading.common import ( OffloadingConnectorMetadata, + OffloadingWorkerMetadata, ) from vllm.distributed.kv_transfer.kv_connector.v1.offloading.metrics import ( OffloadingConnectorStats, @@ -111,6 +112,11 @@ def get_finished(self, finished_req_ids: set[str]) -> tuple[set[str], set[str]]: assert self.connector_worker is not None return self.connector_worker.get_finished(finished_req_ids) + def build_connector_worker_meta(self) -> OffloadingWorkerMetadata | None: + if self.connector_worker is not None: + return self.connector_worker.build_connector_worker_meta() + return None + def get_num_new_matched_tokens( self, request: "Request", num_computed_tokens: int ) -> tuple[int | None, bool]: From 68dd7db81001267c846907769adc14bb32566190 Mon Sep 17 00:00:00 2001 From: rishitdholakia13 <123388671+rishitdholakia13@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:14:52 -0400 Subject: [PATCH 033/237] [Reasoning] Support for speculative decoding with thinking budget (#34668) Signed-off-by: rishitdholakia13 Signed-off-by: rishitdholakia13 <123388671+rishitdholakia13@users.noreply.github.com> Co-authored-by: Nick Hill Co-authored-by: Cyrus Leung --- .../test_thinking_token_budget.py | 185 +++++- .../v1/logits_processors/test_correctness.py | 423 +++++++++++--- tests/v1/sample/utils.py | 25 +- .../test_gpu_model_runner_streaming.py | 1 - vllm/v1/sample/logits_processor/__init__.py | 3 - vllm/v1/sample/logits_processor/builtin.py | 263 +-------- vllm/v1/sample/metadata.py | 6 + vllm/v1/sample/rejection_sampler.py | 19 +- vllm/v1/sample/sampler.py | 17 +- vllm/v1/sample/thinking_budget_state.py | 528 ++++++++++++++++++ vllm/v1/worker/gpu_input_batch.py | 32 +- vllm/v1/worker/gpu_model_runner.py | 6 +- 12 files changed, 1143 insertions(+), 365 deletions(-) create mode 100644 vllm/v1/sample/thinking_budget_state.py diff --git a/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py b/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py index d2db50082a55..d7a601114b21 100644 --- a/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py +++ b/tests/entrypoints/openai/chat_completion/test_thinking_token_budget.py @@ -1,18 +1,62 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -"""E2E tests for thinking_token_budget with reasoning models.""" +"""E2E tests for ``thinking_token_budget`` with reasoning models. + +Covers Qwen3-0.6B and Qwen3.5 FP8 + MTP. +""" + +import asyncio +import json +from typing import Literal import openai import pytest import pytest_asyncio -from tests.utils import RemoteOpenAIServer +from tests.utils import RemoteOpenAIServer, multi_gpu_only, requires_fp8 +from vllm.platforms import current_platform +from vllm.tokenizers import get_tokenizer MODEL_NAME = "Qwen/Qwen3-0.6B" +QWEN35_FP8_MTP_MODEL = "Qwen/Qwen3.5-35B-A3B-FP8" MESSAGES = [{"role": "user", "content": "What is 1+1? Be concise."}] THINK_BUDGET = 5 +REASONING_START_STR = "" +REASONING_END_STR = "" + + +def _count_reasoning_decode_token_ids_between_markers( + full_token_ids: list[int], + reasoning_start_ids: list[int], + reasoning_end_ids: list[int], +) -> int | None: + """Count decode tokens in the thinking span (after last start, before first end).""" + + if not reasoning_start_ids or not reasoning_end_ids: + raise ValueError("reasoning marker token id lists must be non-empty") + + def _last_subseq_index(haystack: list[int], needle: list[int]) -> int: + n = len(needle) + if n > len(haystack): + return -1 + for i in range(len(haystack) - n, -1, -1): + if haystack[i : i + n] == needle: + return i + return -1 + + last_start = _last_subseq_index(full_token_ids, reasoning_start_ids) + if last_start < 0: + return None + + pos_after_start = last_start + len(reasoning_start_ids) + end_n = len(reasoning_end_ids) + for j in range(pos_after_start, len(full_token_ids) - end_n + 1): + if full_token_ids[j : j + end_n] == reasoning_end_ids: + return j - pos_after_start + return len(full_token_ids) - pos_after_start + @pytest.fixture(scope="module") def server(): @@ -48,6 +92,51 @@ def server_with_auto_reasoning_config(): yield remote_server +@pytest.fixture(scope="module") +def server_qwen35_fp8_mtp_tp2(): + """Qwen3.5-35B FP8 with MTP speculative decoding and tensor parallel size 2.""" + if current_platform.device_count() < 2: + pytest.skip("Need at least 2 GPUs for --tensor-parallel-size 2") + if not current_platform.supports_fp8(): + pytest.skip("FP8 is not supported on this platform") + + spec_cfg = { + "method": "mtp", + "num_speculative_tokens": 2, + "max_model_len": 32768, + } + args = [ + "--tensor-parallel-size", + "2", + "--max-model-len", + "32768", + "--speculative-config", + json.dumps(spec_cfg), + "--reasoning-parser", + "qwen3", + "--reasoning-config", + json.dumps( + { + "reasoning_start_str": REASONING_START_STR, + "reasoning_end_str": REASONING_END_STR, + } + ), + ] + # With 4+ GPUs, run TP=2 on physical devices 2,3 so module-scoped 0.6B servers + # on 0,1 do not exhaust memory on the same devices as this worker. + env_dict = None + if current_platform.device_count() >= 4: + env_dict = {"CUDA_VISIBLE_DEVICES": "2,3"} + + with RemoteOpenAIServer( + QWEN35_FP8_MTP_MODEL, + args, + max_wait_seconds=3000, + env_dict=env_dict, + ) as remote_server: + yield remote_server + + @pytest_asyncio.fixture async def client(request, server, server_with_auto_reasoning_config): server_map = { @@ -89,8 +178,10 @@ async def test_thinking_token_budget_mixed_requests(client: openai.AsyncOpenAI): async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI): """Test that thinking_token_budget limits the number of reasoning tokens. - In streaming mode each reasoning delta corresponds to one token, so - counting non-empty reasoning_content chunks gives the exact token count. + Counts non-empty streaming ``delta.reasoning`` chunks (coarse proxy; each + chunk may represent multiple decode tokens — see + ``_count_reasoning_decode_token_ids_between_markers`` and the Qwen3.5 MTP + test for id-based checks). """ reasoning_token_count = 0 @@ -110,3 +201,89 @@ async def test_thinking_token_budget_limits_reasoning(client: openai.AsyncOpenAI f"reasoning tokens ({reasoning_token_count}) exceeded " f"thinking_token_budget ({THINK_BUDGET})" ) + + +@pytest.mark.asyncio +@multi_gpu_only(num_gpus=2) +@requires_fp8 +async def test_thinking_token_budget_qwen35_fp8_mtp_concurrent_mixed_budget_and_plain( + server_qwen35_fp8_mtp_tp2, +): + """Concurrent chat requests: some with ``thinking_token_budget``, some without. + + Exercises the scheduler / input processor under a mixed batch on the same + Qwen3.5 FP8 + MTP (TP=2) server. Budgeted calls are checked with + ``_count_reasoning_decode_token_ids_between_markers`` on full token ids. + """ + + _batch_spec: list[tuple[Literal["budget"], int] | tuple[Literal["plain"], None]] = [ + ("budget", 1), + ("budget", 12), + ("plain", None), + ("budget", 20), + ("budget", 14), + ("plain", None), + ("plain", None), + ("budget", 12), + ("plain", None), + ] + + tokenizer = get_tokenizer(tokenizer_name=QWEN35_FP8_MTP_MODEL) + start_ids = list(tokenizer.encode(REASONING_START_STR, add_special_tokens=False)) + end_ids = list(tokenizer.encode(REASONING_END_STR, add_special_tokens=False)) + + async with server_qwen35_fp8_mtp_tp2.get_async_client() as client: + + async def budgeted_call(expected_budget: int): + return await client.chat.completions.create( + model=QWEN35_FP8_MTP_MODEL, + messages=MESSAGES, + max_tokens=256, + stream=False, + extra_body={ + "thinking_token_budget": expected_budget, + "return_token_ids": True, + }, + ) + + async def plain_call(): + return await client.chat.completions.create( + model=QWEN35_FP8_MTP_MODEL, + messages=MESSAGES, + max_tokens=256, + stream=False, + ) + + coros = [] + for row in _batch_spec: + if row[0] == "budget": + b = row[1] + assert isinstance(b, int) + coros.append(budgeted_call(b)) + else: + coros.append(plain_call()) + results = await asyncio.gather(*coros) + + for i, (response, (kind, expected_budget)) in enumerate( + zip(results, _batch_spec, strict=True) + ): + msg = response.choices[0].message + assert msg.content or getattr(msg, "reasoning", None), ( + f"index {i} ({kind}): empty message" + ) + + if kind == "budget": + assert expected_budget is not None + assert response.prompt_token_ids is not None + assert response.choices[0].token_ids is not None + full_ids = list(response.prompt_token_ids) + list( + response.choices[0].token_ids + ) + n_reason = _count_reasoning_decode_token_ids_between_markers( + full_ids, start_ids, end_ids + ) + assert n_reason is not None, f"index {i}: missing reasoning start in ids" + assert n_reason == expected_budget, ( + f"index {i}: reasoning decode token ids ({n_reason}) != " + f"thinking_token_budget ({expected_budget})" + ) diff --git a/tests/v1/logits_processors/test_correctness.py b/tests/v1/logits_processors/test_correctness.py index 9ee6a70abe4c..1326b96346a7 100644 --- a/tests/v1/logits_processors/test_correctness.py +++ b/tests/v1/logits_processors/test_correctness.py @@ -30,10 +30,13 @@ MinPLogitsProcessor, MinTokensLogitsProcessor, MoveDirectionality, - ThinkingTokenBudgetLogitsProcessor, build_logitsprocs, ) from vllm.v1.sample.metadata import SamplingMetadata +from vllm.v1.sample.thinking_budget_state import ( + ThinkingBudgetStateHolder, + maybe_create_thinking_budget_state_holder, +) PIN_MEMORY_AVAILABLE = is_pin_memory_available() MAX_NUM_REQS = 256 @@ -48,8 +51,10 @@ MIN_TOKENS_LEN_THRESHOLD = 5 REQS_PER_LOGITPROC = 50 STR_NO_LOGITPROC = "none" +# Thinking budget uses ``ThinkingBudgetStateHolder`` (not a logits processor). +STR_THINKING_BUDGET = "thinking_budget" -# ThinkingTokenBudgetLogitsProcessor testing constants +# Thinking token budget testing constants THINKING_TOKEN_BUDGET = 5 THINK_START_TOKEN_ID = 999 THINK_END_TOKEN_ID = 998 @@ -80,15 +85,8 @@ def __init__(self, workload_index: int, logitproc_type: LogitprocType): if num_tokens > 0: # Use diverse random tokens self.out_tokens = [random.randint(1, 950) for _ in range(num_tokens)] - # Set first token for ThinkingTokenBudget testing - is_thinking_processor = ( - logitproc_type is ThinkingTokenBudgetLogitsProcessor - or ( - hasattr(logitproc_type, "__name__") - and logitproc_type.__name__ == "ThinkingTokenBudgetLogitsProcessor" - ) - ) - if is_thinking_processor: + # Think-start seed for ``STR_THINKING_BUDGET`` rows. + if logitproc_type == STR_THINKING_BUDGET: self.out_tokens[0] = THINK_START_TOKEN_ID else: self.out_tokens = [] @@ -102,7 +100,7 @@ def __str__(self): class MockReasoningConfig: - """Mock reasoning config for testing ThinkingTokenBudgetLogitsProcessor.""" + """Minimal reasoning config for ``ThinkingBudgetStateHolder`` tests.""" reasoning_start_token_ids = [THINK_START_TOKEN_ID] reasoning_end_token_ids = [THINK_END_TOKEN_ID] @@ -137,6 +135,18 @@ def _generate_fake_sampling_metadata( is_pin_memory=PIN_MEMORY_AVAILABLE, is_pooling_model=False, ) + num_spec = ( + vllm_config.speculative_config.num_speculative_tokens + if vllm_config.speculative_config + else 0 + ) + thinking_holder = maybe_create_thinking_budget_state_holder( + vllm_config.reasoning_config, + vllm_config.scheduler_config.max_num_seqs, + num_spec, + device, + PIN_MEMORY_AVAILABLE, + ) fake_sampling_metadata = SamplingMetadata( temperature=torch.full((batch_size,), 0.0), all_greedy=True, @@ -156,6 +166,7 @@ def _generate_fake_sampling_metadata( allowed_token_ids_mask=None, bad_words_token_ids={}, logitsprocs=logitsprocs, + thinking_budget_state_holder=thinking_holder, ) return fake_sampling_metadata @@ -187,7 +198,7 @@ def _sampling_params_from_logitproc(logitproc_type: LogitprocType) -> SamplingPa def _generate_mixed_logitsprocs_batch_params( reqs_per_logitproc: int, - logitsprocs_types: list[str], + logitsprocs_types: list[LogitprocType], ) -> list[LogitsProcsRequestParams]: """Define key params for a batch of requests with a different logitproc enabled per request. @@ -450,23 +461,21 @@ def _thinking_budget_validate( request_params: LogitsProcsRequestParams, step_idx: int, ) -> None: - """Validate thinking token budget processor behavior""" - # Get the ThinkingTokenBudgetLogitsProcessor instance - tb_processor: ThinkingTokenBudgetLogitsProcessor = next( - test_fakes.get_logitsprocs_by_cls(ThinkingTokenBudgetLogitsProcessor) - ) + """Validate ``ThinkingBudgetStateHolder`` thinking-budget behavior. - # Get current request state - state = tb_processor._state.get(batch_index) + State is keyed by **batch slot** (same index space as logits rows), matching + ``sync_batch`` / sampler integration (see PR #34668 discussion). + """ + holder = test_fakes.sampling_metadata.thinking_budget_state_holder + assert holder is not None + state = holder._state.get(batch_index) params = request_params.params - # Validate thinking token budget configuration if hasattr(params, "thinking_token_budget") and params.thinking_token_budget: - # State should exist for requests with thinking_token_budget if state is None: _raise_error_invalid( msg_suffix=( - f"Expected state for batch {batch_index} " + f"Expected holder state for batch slot {batch_index} " f"with thinking_token_budget={params.thinking_token_budget}" ), batch_index=batch_index, @@ -474,10 +483,8 @@ def _thinking_budget_validate( step_idx=step_idx, ) - # Validate budget matches what was set expected_budget = params.thinking_token_budget actual_budget = state["thinking_token_budget"] - if actual_budget != expected_budget: _raise_error_invalid( msg_suffix=( @@ -488,13 +495,9 @@ def _thinking_budget_validate( step_idx=step_idx, ) - # Check if we're in thinking mode and validate token counting output_tokens = request_params.out_tokens - - # Find if thinking has started in output tokens + start_tokens = holder.think_start_token_ids thinking_started = False - start_tokens = tb_processor.reasoning_start_token_ids - if len(start_tokens) > 0: for i in range(len(output_tokens) - len(start_tokens) + 1): if output_tokens[i : i + len(start_tokens)] == start_tokens: @@ -502,61 +505,42 @@ def _thinking_budget_validate( break if thinking_started: - # If budget is exceeded, validate end token forcing think_count = state["think_count"] budget = state["thinking_token_budget"] + if think_count >= budget and not state["in_end"]: + _raise_error_invalid( + msg_suffix=( + f"Budget exceeded ({think_count} >= {budget}) but " + "in_end is false" + ), + batch_index=batch_index, + request_params=request_params, + step_idx=step_idx, + ) - if think_count >= budget: - if not state["in_end"]: + end_tokens = holder.think_end_token_ids + if ( + think_count >= budget + and state["in_end"] + and len(end_tokens) > 0 + and holder.has_tracked_requests() + ): + expected_end_token_id = end_tokens[ + min(state["end_count"], len(end_tokens) - 1) + ] + # Holder bumps forced vocab positions to 1e9 (does not -inf others). + forced_logit = float(logits_new[batch_index, expected_end_token_id]) + if forced_logit < 1.0e8: _raise_error_invalid( msg_suffix=( - f"Budget exceeded ({think_count} >= " - f"{budget}) but not " - "forcing end tokens" + f"Expected forced end token {expected_end_token_id} " + f"with large logit, got {forced_logit}" ), batch_index=batch_index, request_params=request_params, step_idx=step_idx, ) - # Validate that only end tokens are allowed - end_tokens = tb_processor.reasoning_end_token_ids - if len(end_tokens) > 0: - expected_end_token_id = end_tokens[ - min(state["end_count"], len(end_tokens) - 1) - ] - - # Check logits masking - batch_logits = logits_new[batch_index] - for token_id in range(len(batch_logits)): - logit_value = batch_logits[token_id] - - if token_id == expected_end_token_id: - # End token should not be masked - if logit_value == -float("inf"): - _raise_error_invalid( - msg_suffix=( - f"End token {token_id} should not be " - "masked but is" - ), - batch_index=batch_index, - request_params=request_params, - step_idx=step_idx, - ) - else: - # All other tokens should be masked when forcing end - if logit_value != -float("inf"): - _raise_error_invalid( - msg_suffix=( - f"Token {token_id} should be masked " - f"when forcing end tokens, but " - f"logit={logit_value}" - ), - batch_index=batch_index, - request_params=request_params, - step_idx=step_idx, - ) - def _none_validate( test_fakes: LogitsprocsTestFakes, @@ -604,7 +588,7 @@ class LogitsprocTestHelpers(NamedTuple): MinTokensLogitsProcessor: LogitsprocTestHelpers( gen_request_fxn=_min_tokens_params, eval_fxn=_min_tokens_validate ), - ThinkingTokenBudgetLogitsProcessor: LogitsprocTestHelpers( + STR_THINKING_BUDGET: LogitsprocTestHelpers( gen_request_fxn=_thinking_budget_params, eval_fxn=_thinking_budget_validate ), } @@ -614,20 +598,17 @@ def _get_test_cases() -> list[list[str]]: """Each test case is a set of logitsprocs""" logitsprocs_types = list(logitsprocs_test_mapping.keys()) - # Isolate ThinkingTokenBudgetLogitsProcessor from all other processors - # to avoid unexpected modification of logits interference - thinking_processor = ThinkingTokenBudgetLogitsProcessor + # Isolate thinking-budget handling from other processors to avoid cross-talk. + thinking_id: LogitprocType = STR_THINKING_BUDGET other_processors = [ - p - for p in logitsprocs_types - if p != STR_NO_LOGITPROC and p != thinking_processor + p for p in logitsprocs_types if p != STR_NO_LOGITPROC and p != thinking_id ] return ( [[STR_NO_LOGITPROC]] + [[logitproc_type, STR_NO_LOGITPROC] for logitproc_type in other_processors] + [other_processors] - + [[thinking_processor]] + + [[thinking_id]] ) @@ -802,12 +783,23 @@ def _assert_valid( ) +def _slot_outputs_for_metadata( + persistent_batch: list[LogitsProcsRequestParams], pad_len: int +) -> list[list[int]]: + """Per-batch-slot output token ids aligned with ``SamplingMetadata`` rows.""" + rows: list[list[int]] = [[] for _ in range(pad_len)] + for i, req in enumerate(persistent_batch): + if i < pad_len: + rows[i] = list(req.out_tokens) + return rows + + @create_new_process_for_each_test() @pytest.mark.parametrize("device", DEVICES) @pytest.mark.parametrize("reqs_per_logitproc", [REQS_PER_LOGITPROC]) @pytest.mark.parametrize("logitsprocs_under_test", _get_test_cases()) def test_logitsprocs( - device: str, reqs_per_logitproc: int, logitsprocs_under_test: list[str] + device: str, reqs_per_logitproc: int, logitsprocs_under_test: list[LogitprocType] ): random.seed(40) torch.set_default_device(device) @@ -855,9 +847,10 @@ def test_logitsprocs( # Apply fake batch update to logitsprocs fake_update_logitsprocs_state(test_fakes, batch_update) - # Emulate application of logits processors in engine + # Emulate application of logits processors + thinking holder (sampler order). slice_idxs = [req.workload_index for req in persistent_batch] - logits_w_lp = fake_apply_logitsprocs(test_fakes, slice_idxs).cpu() + slot_rows = _slot_outputs_for_metadata(persistent_batch, workload_size) + logits_w_lp = fake_apply_logitsprocs(test_fakes, slice_idxs, slot_rows).cpu() _assert_valid( batch_size=batch_size, @@ -869,3 +862,263 @@ def test_logitsprocs( ) step_idx += 1 + + +class MockReasoningNoEndTokens: + """Reasoning config with no end token ids (disables enforcement in holder).""" + + reasoning_start_token_ids = [THINK_START_TOKEN_ID] + reasoning_end_token_ids: list[int] = [] + + +def test_maybe_create_thinking_budget_holder_without_reasoning(): + cfg = VllmConfig() + assert cfg.reasoning_config is None + assert ( + maybe_create_thinking_budget_state_holder( + None, + cfg.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + is None + ) + + +def test_thinking_budget_holder_has_tracked_after_sync_add(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + assert not h.has_tracked_requests() + h.sync_batch( + BatchUpdate( + batch_size=1, + removed=(), + added=[ + ( + 0, + SamplingParams(thinking_token_budget=3), + None, + [THINK_START_TOKEN_ID], + ) + ], + moved=(), + ) + ) + assert h.has_tracked_requests() + assert h._state[0]["thinking_token_budget"] == 3 + + +def test_thinking_budget_holder_sync_remove_clears_state(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=1, + removed=(), + added=[ + ( + 0, + SamplingParams(thinking_token_budget=3), + None, + [], + ) + ], + moved=(), + ) + ) + assert h.has_tracked_requests() + h.sync_batch(BatchUpdate(batch_size=0, removed=(0,), added=(), moved=())) + assert not h.has_tracked_requests() + + +def test_thinking_budget_holder_sync_add_without_budget_drops_row(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=1, + removed=(), + added=[(0, SamplingParams(), None, [])], + moved=(), + ) + ) + assert not h.has_tracked_requests() + + +def test_thinking_budget_holder_swap_exchanges_state(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=2, + removed=(), + added=[ + ( + 0, + SamplingParams(thinking_token_budget=3), + None, + [], + ), + ( + 1, + SamplingParams(thinking_token_budget=7), + None, + [], + ), + ], + moved=(), + ) + ) + b0, b1 = h._state[0]["thinking_token_budget"], h._state[1]["thinking_token_budget"] + h.sync_batch( + BatchUpdate( + batch_size=2, + removed=(), + added=(), + moved=[(0, 1, MoveDirectionality.SWAP)], + ) + ) + assert h._state[0]["thinking_token_budget"] == b1 + assert h._state[1]["thinking_token_budget"] == b0 + + +def test_thinking_budget_holder_unidirectional_move(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=2, + removed=(), + added=[ + ( + 1, + SamplingParams(thinking_token_budget=4), + None, + [], + ), + ], + moved=(), + ) + ) + assert 1 in h._state and 0 not in h._state + h.sync_batch( + BatchUpdate( + batch_size=2, + removed=(), + added=(), + moved=[(1, 0, MoveDirectionality.UNIDIRECTIONAL)], + ) + ) + assert 0 in h._state and 1 not in h._state + assert h._state[0]["thinking_token_budget"] == 4 + + +def test_thinking_budget_holder_update_state_repeat_indices_last_row_wins(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningConfig() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=1, + removed=(), + added=[ + ( + 0, + SamplingParams(thinking_token_budget=5), + None, + [THINK_START_TOKEN_ID], + ) + ], + moved=(), + ) + ) + out_lists = [[THINK_START_TOKEN_ID], [THINK_START_TOKEN_ID, 10, 11, 12, 13, 14]] + h.update_state( + out_lists, + None, + torch.tensor([0, 0], dtype=torch.long), + ) + assert h._state[0]["output_tok_ids"] == out_lists[1] + + +def test_thinking_budget_holder_spec_mode_tensor_layout(): + h = ThinkingBudgetStateHolder( + MockReasoningConfig(), + 8, + 2, + torch.device("cpu"), + False, + ) + assert h.in_spec_mode + assert h.mask.shape[0] == 8 * (2 + 1) + + +def test_thinking_budget_holder_empty_end_tokens_disables_row(): + vc = VllmConfig() + vc.reasoning_config = MockReasoningNoEndTokens() + h = ThinkingBudgetStateHolder( + vc.reasoning_config, + vc.scheduler_config.max_num_seqs, + 0, + torch.device("cpu"), + False, + ) + h.sync_batch( + BatchUpdate( + batch_size=1, + removed=(), + added=[ + ( + 0, + SamplingParams(thinking_token_budget=5), + None, + [THINK_START_TOKEN_ID], + ) + ], + moved=(), + ) + ) + h.update_state([[THINK_START_TOKEN_ID, 1]], None, None) + assert h._state[0]["thinking_token_budget"] == -1 diff --git a/tests/v1/sample/utils.py b/tests/v1/sample/utils.py index a0abb3b4c6ce..907be3614b9c 100644 --- a/tests/v1/sample/utils.py +++ b/tests/v1/sample/utils.py @@ -198,22 +198,43 @@ def get_logitsprocs(self) -> Iterator[LogitsProcessor]: def fake_update_logitsprocs_state( test_fakes: LogitsprocsTestFakes, - batch_update: BatchUpdate, + batch_update: BatchUpdate | None, ) -> None: """Imitate logits processors persistent batch state update in engine core""" for logitproc in test_fakes.get_logitsprocs(): logitproc.update_state(batch_update) + holder = test_fakes.sampling_metadata.thinking_budget_state_holder + if holder is not None: + holder.sync_batch(batch_update) def fake_apply_logitsprocs( test_fakes: LogitsprocsTestFakes, slice_indices: list[int], + slot_output_token_ids: list[list[int]] | None = None, ) -> torch.Tensor: - """Imitate application of logits processors in engine core""" + """Imitate application of logits processors in engine core. + + When ``thinking_budget_state_holder`` has tracked requests, this mirrors + :meth:`Sampler.apply_logits_processors` by refreshing per-slot + ``output_token_ids`` (if ``slot_output_token_ids`` is provided), then + ``update_state`` + ``apply_to_logits`` on the holder after built-in logits + processors. + """ logits = test_fakes.logits[torch.tensor(slice_indices, dtype=torch.long)].clone() for processor in test_fakes.get_logitsprocs(): logits = processor.apply(logits) + + md = test_fakes.sampling_metadata + holder = md.thinking_budget_state_holder + if holder is not None and holder.has_tracked_requests(): + if slot_output_token_ids is not None: + for i, toks in enumerate(slot_output_token_ids): + if i < len(md.output_token_ids): + md.output_token_ids[i] = list(toks) + holder.update_state(md.output_token_ids, md.spec_token_ids, None) + logits = holder.apply_to_logits(logits, False, md.spec_token_ids) return logits diff --git a/tests/v1/streaming_input/test_gpu_model_runner_streaming.py b/tests/v1/streaming_input/test_gpu_model_runner_streaming.py index 0ed7b6cb3efc..946ca99507df 100644 --- a/tests/v1/streaming_input/test_gpu_model_runner_streaming.py +++ b/tests/v1/streaming_input/test_gpu_model_runner_streaming.py @@ -39,7 +39,6 @@ def mock_model_runner_with_input_batch(): vocab_size=32000, block_sizes=[16], kernel_block_sizes=[16], - is_spec_decode=False, logitsprocs=None, is_pooling_model=False, ) diff --git a/vllm/v1/sample/logits_processor/__init__.py b/vllm/v1/sample/logits_processor/__init__.py index fb4a046fc057..2cb89e1ea950 100644 --- a/vllm/v1/sample/logits_processor/__init__.py +++ b/vllm/v1/sample/logits_processor/__init__.py @@ -18,7 +18,6 @@ LogitBiasLogitsProcessor, MinPLogitsProcessor, MinTokensLogitsProcessor, - ThinkingTokenBudgetLogitsProcessor, process_dict_updates, ) from vllm.v1.sample.logits_processor.interface import ( @@ -51,7 +50,6 @@ MinTokensLogitsProcessor, LogitBiasLogitsProcessor, MinPLogitsProcessor, - ThinkingTokenBudgetLogitsProcessor, ] @@ -356,5 +354,4 @@ def apply(self, logits: torch.Tensor) -> torch.Tensor: "STR_POOLING_REJECTS_LOGITSPROCS", "LOGITSPROCS_GROUP", "AdapterLogitsProcessor", - "ThinkingTokenBudgetLogitsProcessor", ] diff --git a/vllm/v1/sample/logits_processor/builtin.py b/vllm/v1/sample/logits_processor/builtin.py index 1739452b44a0..11a52711d671 100644 --- a/vllm/v1/sample/logits_processor/builtin.py +++ b/vllm/v1/sample/logits_processor/builtin.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections.abc import Callable, Sequence -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, TypeVar import numpy as np import torch @@ -291,267 +291,6 @@ def apply_with_spec_decode( return logits -class ThinkingTokenBudgetLogitsProcessor(LogitsProcessor): - """Limits the number of tokens allowed inside a 'thinking' section.""" - - def __init__( - self, vllm_config: "VllmConfig", device: torch.device, is_pin_memory: bool - ): - reasoning_config = vllm_config.reasoning_config - max_num_reqs = vllm_config.scheduler_config.max_num_seqs - - # Check if thinking is enabled - self.is_enabled = reasoning_config is not None and reasoning_config.enabled - - self.reasoning_start_token_ids = getattr( - reasoning_config, "reasoning_start_token_ids", [] - ) - self.reasoning_end_token_ids = getattr( - reasoning_config, "reasoning_end_token_ids", [] - ) - - self.pin_memory = is_pin_memory - self.device = device - # Per-request state tracking for thinking token management - # Key: request_index, Value: state dict containing: - # "in_think": bool - currently in thinking mode - # "in_end": bool - currently forcing end tokens output - # "check_count_down": int - steps remaining until next think - # start/end token parsing - # "think_count": int - number of thinking tokens generated - # "end_count": int - number of end tokens forced so far - # "thinking_token_budget": int - max allowed thinking tokens - # "output_tok_ids": list[int] - generated output tokens - # "prev_output_length": int - previous output length for - # incremental processing - self._state: dict[int, dict[str, Any]] = {} - - # Preallocate reusable tensors - self.mask = torch.zeros(max_num_reqs, dtype=torch.bool, device=device) - self.force_token_ids = torch.full( - (max_num_reqs,), -1, dtype=torch.long, device=device - ) - - @staticmethod - def _find_last_sequence_index(target_list: list[int], token_ids: list[int]) -> int: - """ - Returns the index of the last occurrence of token_ids in target_list. - - Args: - target_list (list[int]): The list of token IDs. - token_ids (list[int]): The sequence of token IDs to find. - """ - if not token_ids: - return -1 - for i in range(len(target_list) - len(token_ids), -1, -1): - if target_list[i : i + len(token_ids)] == token_ids: - return i - return -1 - - def _init_state_entry( - self, prompt_tok_ids: list[int] | None, thinking_token_budget: int - ) -> dict[str, Any]: - """Initializes the tracking state for a given sequence index.""" - if prompt_tok_ids is None: - last_start = -1 - last_end = -1 - in_think = False - think_count = 0 - else: - last_start = self._find_last_sequence_index( - prompt_tok_ids, self.reasoning_start_token_ids - ) - last_end = self._find_last_sequence_index( - prompt_tok_ids, self.reasoning_end_token_ids - ) - in_think = last_start > last_end - if in_think: - think_count = len(prompt_tok_ids) - ( - last_start + len(self.reasoning_start_token_ids) - ) - else: - think_count = 0 - - return { - "in_think": in_think, # Currently in thinking mode - "in_end": in_think and thinking_token_budget == 0, - "check_count_down": thinking_token_budget, - "think_count": think_count, # Number of tokens in thinking section - "end_count": 0, # Number of end tokens forced so far - "prompt_tok_ids": prompt_tok_ids, - "output_tok_ids": [], - "thinking_token_budget": thinking_token_budget, - "prev_output_length": 0, - # Track previous output length for incremental updates - } - - def _update_think_state(self, state: dict[str, Any]): - """Updates the state based on newly generated output tokens.""" - if not state.get("in_end", False) and state.get("check_count_down", 0) > 0: - state["check_count_down"] -= 1 - return - - output = state.get("output_tok_ids", []) - if not output: - return - - # Track previous output length for incremental processing - prev_length = state.get("prev_output_length", 0) - current_length = len(output) - - if current_length <= prev_length: - return - - # Process only newly added tokens - new_tokens = output[prev_length:] - state["prev_output_length"] = current_length - - # Check if new tokens contain think start or end sequences - start_len = len(self.reasoning_start_token_ids) - end_len = len(self.reasoning_end_token_ids) - - # Look for think sequences in recent tokens (including boundary) - # Check overlapping regions where sequences might span boundaries - check_start_idx = max(0, prev_length - max(start_len, end_len) + 1) - recent_tokens = output[check_start_idx:] - - # Find any think start/end sequences in recent tokens - recent_start_pos = self._find_last_sequence_index( - recent_tokens, self.reasoning_start_token_ids - ) - recent_end_pos = self._find_last_sequence_index( - recent_tokens, self.reasoning_end_token_ids - ) - - # Update state based on recent sequences - if not state["in_end"]: - if recent_start_pos >= 0 and recent_end_pos >= 0: - if recent_start_pos > recent_end_pos: - # Case: ......... - entering think mode - absolute_start_pos = check_start_idx + recent_start_pos - new_think_count = current_length - (absolute_start_pos + start_len) - state["in_think"] = True - state["think_count"] = new_think_count - else: - # Case: ......... - exiting think mode - state["in_think"] = False - state["think_count"] = 0 - elif recent_start_pos >= 0: - # Found think start - entering think mode - absolute_start_pos = check_start_idx + recent_start_pos - new_think_count = current_length - (absolute_start_pos + start_len) - state["in_think"] = True - state["think_count"] = new_think_count - elif recent_end_pos >= 0: - # Found think end - exiting think mode - state["in_think"] = False - state["think_count"] = 0 - elif state["in_think"]: - # Continue thinking mode, increment count by new tokens - state["think_count"] += len(new_tokens) - - # Set countdown based on current state - if state["in_think"]: - remaining_budget = max( - 0, state["thinking_token_budget"] - state["think_count"] - ) - state["check_count_down"] = max(0, remaining_budget - 1) - else: - state["check_count_down"] = state["thinking_token_budget"] - - # Check if need to transition to end mode - if ( - state["in_think"] - and state["think_count"] >= state["thinking_token_budget"] - ): - state["in_think"] = False - state["in_end"] = True - state["end_count"] = 0 - state["check_count_down"] = state["thinking_token_budget"] - else: - # In end mode - state["end_count"] += 1 - if state["end_count"] >= len(self.reasoning_end_token_ids): - state.update( - { - "in_end": False, - "end_count": 0, - "check_count_down": state["thinking_token_budget"], - } - ) - - def is_argmax_invariant(self) -> bool: - """This logits processor can change the outcome of - greedy sampling by forcing that the thinking section - ends after a certain number of tokens.""" - return False - - def update_state(self, batch_update: BatchUpdate | None): - if not self.is_enabled: - return - if batch_update: - for index, params, prompt_tok_ids, output_tok_ids in batch_update.added: - thinking_token_budget = params.thinking_token_budget - - if thinking_token_budget is not None: - self._state[index] = self._init_state_entry( - prompt_tok_ids, thinking_token_budget - ) - self._state[index]["output_tok_ids"] = output_tok_ids - else: - # Remove state if no thinking budget - self._state.pop(index, None) - - for index in batch_update.removed: - self._state.pop(index, {}) - - for i1, i2, direction in batch_update.moved: - if direction == MoveDirectionality.SWAP: - state1 = self._state.pop(i1, None) - state2 = self._state.pop(i2, None) - if state1 is not None: - self._state[i2] = state1 - if state2 is not None: - self._state[i1] = state2 - else: - state = self._state.pop(i1, None) - if state is not None: - self._state[i2] = state - - for state in self._state.values(): - self._update_think_state(state) - - def apply(self, logits: torch.Tensor) -> torch.Tensor: - if not self.is_enabled or not self._state: - return logits - - batch_size = logits.size(0) - self.mask[:batch_size] = False - - for i in range(batch_size): - state = self._state.get(i) - if state and state["in_end"]: - self.mask[i] = True - self.force_token_ids[i] = self.reasoning_end_token_ids[ - state["end_count"] - ] - - # Check in CPU first not to sync with GPU - has_active_thinking = any( - state.get("in_end", False) for state in self._state.values() - ) - - if has_active_thinking: - current_mask = self.mask[:batch_size] - active_indices = current_mask.nonzero(as_tuple=False).view(-1) - if len(active_indices) > 0: - force_tokens = self.force_token_ids[active_indices] - # Apply a large value for the end thinking token id index - logits[active_indices, force_tokens] = 1e9 - - return logits - - def process_dict_updates( req_entries: dict[int, T], batch_update: BatchUpdate | None, diff --git a/vllm/v1/sample/metadata.py b/vllm/v1/sample/metadata.py index 4682cde1098b..fa4ceac8e71e 100644 --- a/vllm/v1/sample/metadata.py +++ b/vllm/v1/sample/metadata.py @@ -1,11 +1,14 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project +from __future__ import annotations + from dataclasses import dataclass import torch from vllm.v1.sample.logits_processor import LogitsProcessors +from vllm.v1.sample.thinking_budget_state import ThinkingBudgetStateHolder @dataclass @@ -47,3 +50,6 @@ class SamplingMetadata: # Speculative token ids spec_token_ids: list[list[int]] | None = None + # When non-None, use ``holder.has_tracked_requests()`` to see if this batch applies + # thinking-token-budget logits (holder may exist with an empty tracking set). + thinking_budget_state_holder: ThinkingBudgetStateHolder | None = None diff --git a/vllm/v1/sample/rejection_sampler.py b/vllm/v1/sample/rejection_sampler.py index 2b63893c0496..678654cb78a4 100644 --- a/vllm/v1/sample/rejection_sampler.py +++ b/vllm/v1/sample/rejection_sampler.py @@ -290,16 +290,24 @@ def apply_logits_processors( any_penalties_or_bad_words = ( sampling_metadata.bad_words_token_ids or has_penalties ) + holder = sampling_metadata.thinking_budget_state_holder + needs_thinking = holder is not None and holder.has_tracked_requests() output_token_ids = sampling_metadata.output_token_ids - if any_penalties_or_bad_words: + if any_penalties_or_bad_words or needs_thinking: output_token_ids = self._combine_outputs_with_spec_tokens( output_token_ids, sampling_metadata.spec_token_ids, ) # Calculate indices of target logits. - if sampling_metadata.allowed_token_ids_mask is not None or has_penalties: + repeat_indices: torch.Tensor | None = None + need_repeat_indices = ( + sampling_metadata.allowed_token_ids_mask is not None + or has_penalties + or needs_thinking + ) + if need_repeat_indices: num_requests = len(metadata.num_draft_tokens) num_draft_tokens = torch.tensor(metadata.num_draft_tokens, device="cpu") original_indices = torch.arange(num_requests, device="cpu") @@ -327,7 +335,12 @@ def apply_logits_processors( logits = processor.apply_with_spec_decode( logits, metadata.num_draft_tokens ) - + if holder is not None and holder.has_tracked_requests(): + logits = holder.apply_to_logits( + logits, + predict_bonus_token=False, + spec_token_ids=sampling_metadata.spec_token_ids, + ) return logits @staticmethod diff --git a/vllm/v1/sample/sampler.py b/vllm/v1/sample/sampler.py index 5341351352e3..a77eafba2556 100644 --- a/vllm/v1/sample/sampler.py +++ b/vllm/v1/sample/sampler.py @@ -364,9 +364,13 @@ def apply_logits_processors( any_penalties_or_bad_words = ( bool(bad_words_token_ids) or not sampling_metadata.no_penalties ) + holder = sampling_metadata.thinking_budget_state_holder + needs_thinking_combine = holder is not None and holder.has_tracked_requests() output_token_ids = sampling_metadata.output_token_ids - if predict_bonus_token and any_penalties_or_bad_words: + if predict_bonus_token and ( + any_penalties_or_bad_words or needs_thinking_combine + ): # Combine base outputs with spec tokens when speculative decoding # is enabled. output_token_ids = self._combine_outputs_with_spec_tokens( @@ -388,6 +392,17 @@ def apply_logits_processors( # Apply penalties (e.g., freq_penalties). logits = self.apply_penalties(logits, sampling_metadata, output_token_ids) + if holder is not None and holder.has_tracked_requests(): + holder.update_state( + output_token_ids, + sampling_metadata.spec_token_ids, + repeat_indices=None, + ) + logits = holder.apply_to_logits( + logits, + predict_bonus_token, + sampling_metadata.spec_token_ids, + ) return logits @staticmethod diff --git a/vllm/v1/sample/thinking_budget_state.py b/vllm/v1/sample/thinking_budget_state.py new file mode 100644 index 000000000000..74599a1e8c55 --- /dev/null +++ b/vllm/v1/sample/thinking_budget_state.py @@ -0,0 +1,528 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Per-batch thinking token budget state; applied after penalties at sample time.""" + +from typing import TYPE_CHECKING, Any + +import torch + +from vllm.v1.sample.logits_processor.interface import ( + BatchUpdate, + MoveDirectionality, +) + +if TYPE_CHECKING: + from vllm.config.reasoning import ReasoningConfig + + +def maybe_create_thinking_budget_state_holder( + reasoning_config: "ReasoningConfig | None", + max_num_seqs: int, + num_spec_tokens: int, + device: torch.device, + is_pin_memory: bool, +) -> "ThinkingBudgetStateHolder | None": + if reasoning_config is None: + return None + return ThinkingBudgetStateHolder( + reasoning_config, max_num_seqs, num_spec_tokens, device, is_pin_memory + ) + + +class ThinkingBudgetStateHolder: + """Tracks thinking sections and forces end tokens when budget is exceeded.""" + + think_start_token_ids: list[int] + think_end_token_ids: list[int] + + def __init__( + self, + reasoning_config: "ReasoningConfig | None", + max_num_seqs: int, + num_spec_tokens: int, + device: torch.device, + is_pin_memory: bool, + ): + _ = is_pin_memory # API parity with logits processors + max_num_reqs = max_num_seqs + self.in_spec_mode = num_spec_tokens > 0 + self.num_spec_tokens = num_spec_tokens + + # No separate enable flag: a non-``None`` ``reasoning_config`` is the switch. + self.is_enabled = reasoning_config is not None + + if reasoning_config is None: + self.think_start_token_ids = [] + self.think_end_token_ids = [] + else: + rs = reasoning_config.reasoning_start_token_ids + re = reasoning_config.reasoning_end_token_ids + self.think_start_token_ids = rs if rs else [] + self.think_end_token_ids = re if re else [] + + self.device = device + self._state: dict[int, dict[str, Any]] = {} + self.cu_num_tokens: dict[int, int] = {} + + if self.num_spec_tokens > 0: + self.mask = torch.zeros( + max_num_reqs * (self.num_spec_tokens + 1), + dtype=torch.bool, + device=device, + ) + self.force_token_ids = torch.full( + (max_num_reqs * (self.num_spec_tokens + 1),), + -1, + dtype=torch.long, + device=device, + ) + else: + self.mask = torch.zeros(max_num_reqs, dtype=torch.bool, device=device) + self.force_token_ids = torch.full( + (max_num_reqs,), -1, dtype=torch.long, device=device + ) + + def has_tracked_requests(self) -> bool: + """True when ``sync_batch`` has state for a ``thinking_token_budget`` row. + + Used to decide whether sampling needs output-token rows and spec combining; + distinct from merely having a holder instance (reasoning may be on with no + budgeted requests in this batch). + """ + return bool(self._state) + + def sync_batch(self, batch_update: BatchUpdate | None) -> None: + """Add/remove/move per-request state only (no _update_think_state).""" + if not self.is_enabled or not batch_update: + return + for index in batch_update.removed: + self._state.pop(index, None) + + for index, params, prompt_tok_ids, output_tok_ids in batch_update.added: + thinking_token_budget = params.thinking_token_budget + if thinking_token_budget is not None: + self._state[index] = self._init_state_entry( + prompt_tok_ids, thinking_token_budget + ) + self._state[index]["output_tok_ids"] = output_tok_ids + self._state[index]["spec_token_ids"] = [] + else: + self._state.pop(index, None) + + for i1, i2, direction in batch_update.moved: + if direction == MoveDirectionality.SWAP: + state1 = self._state.get(i1) + state2 = self._state.get(i2) + if state1 is not None: + self._state[i2] = state1 + if state2 is not None: + self._state[i1] = state2 + else: + state = self._state.pop(i1, None) + if state is not None: + self._state[i2] = state + + def update_state( + self, + output_token_ids: list[list[int]], + spec_token_ids: list[list[int]] | None, + repeat_indices: torch.Tensor | None = None, + ) -> None: + """Refresh output/spec from sampling rows and recompute think state.""" + if not self.is_enabled or not self._state: + return + + spec_lists = spec_token_ids or [] + last_row_for_req: dict[int, int] | None = None + if repeat_indices is not None: + last_row_for_req = {} + rpt = repeat_indices.cpu().tolist() + for batch_row, req_i in enumerate(rpt): + last_row_for_req[req_i] = batch_row + + for seq_idx, state in list(self._state.items()): + if last_row_for_req is not None: + output_row: int | None = last_row_for_req.get(seq_idx) + if output_row is None or output_row >= len(output_token_ids): + continue + state["output_tok_ids"] = output_token_ids[output_row] + elif seq_idx >= len(output_token_ids): + continue + else: + state["output_tok_ids"] = output_token_ids[seq_idx] + if seq_idx < len(spec_lists): + state["spec_token_ids"] = list(spec_lists[seq_idx]) + else: + state["spec_token_ids"] = [] + state["in_spec_mode"] = self.in_spec_mode + state["force_index"] = [] + if len(state["output_tok_ids"]) > 0: + spec_len = len(state["spec_token_ids"]) + # Only strip draft suffix when there are spec tokens; ``[:-0]`` would + # clear the whole list (Python treats stop index 0 as "up to empty"). + if spec_len > 0 and len(state["output_tok_ids"]) >= spec_len: + state["output_tok_ids"] = state["output_tok_ids"][:-spec_len] + self._update_think_state(state) + + def apply_to_logits( + self, + logits: torch.Tensor, + predict_bonus_token: bool, + spec_token_ids: list[list[int]] | None, + ) -> torch.Tensor: + """Mask and bump logits for forced end-of-thinking tokens.""" + if not self.is_enabled or not self._state: + return logits + spec_lists = spec_token_ids or [] + return self._apply_forcing_to_logits(logits, predict_bonus_token, spec_lists) + + @staticmethod + def _find_last_sequence_index(target_list: list[int], token_ids: list[int]) -> int: + if not token_ids: + return -1 + for i in range(len(target_list) - len(token_ids), -1, -1): + if target_list[i : i + len(token_ids)] == token_ids: + return i + return -1 + + def _init_state_entry( + self, prompt_tok_ids: list[int] | None, thinking_token_budget: int + ) -> dict[str, Any]: + if prompt_tok_ids is None: + last_start = -1 + last_end = -1 + in_think = False + think_count = 0 + start_thinking = -1 + countdown = thinking_token_budget + continue_thinking = False + in_end = False + else: + start_thinking = -1 + countdown = thinking_token_budget + continue_thinking = False + in_end = False + last_start = self._find_last_sequence_index( + prompt_tok_ids, self.think_start_token_ids + ) + last_end = self._find_last_sequence_index( + prompt_tok_ids, self.think_end_token_ids + ) + in_think = last_start > last_end + # load metrics such as think count, start thinking + # if request is in thinking mode, already + if in_think: + think_count = len(prompt_tok_ids) - ( + last_start + len(self.think_start_token_ids) + ) + start_thinking = len(prompt_tok_ids) - think_count - 1 + countdown -= think_count + continue_thinking = True + # check if the token is exhausted within prompt + token_exhausted = thinking_token_budget - think_count + in_end = token_exhausted <= 0 + else: + think_count = 0 + + return { + "in_think": in_think, + "in_end": in_end, + "check_count_down": countdown, + "think_count": think_count, + "end_count": 0, + "prompt_tok_ids": prompt_tok_ids, + "output_tok_ids": [], + "thinking_token_budget": thinking_token_budget, + "prev_output_length": 0, + "spec_token_ids": [], + "force_index": [], + "start_thinking": start_thinking, + "end_thinking": -1, + "in_spec_mode": False, + "bonus_token_forced": False, + "continue_thinking": continue_thinking, + } + + def _update_think_state(self, state: dict[str, Any]) -> None: + if state.get("thinking_token_budget", -1) == -1: + return + if len(self.think_end_token_ids) == 0: + state["thinking_token_budget"] = -1 + state["in_end"] = False + state["force_index"] = [] + return + + if state["start_thinking"] == -1: + start_thinking = self._find_last_sequence_index( + state.get("output_tok_ids", []), self.think_start_token_ids + ) + state["start_thinking"] = start_thinking + if state["end_thinking"] == -1: + end_thinking = self._find_last_sequence_index( + state.get("output_tok_ids", []), self.think_end_token_ids + ) + state["end_thinking"] = end_thinking + + if state["start_thinking"] == -1: + return + + if state["continue_thinking"]: + sampled_tokens_from_previous_step = len( + state.get("output_tok_ids", []) + ) - state.get("prev_output_length", 0) + else: + if state["prev_output_length"] == 0: + sampled_tokens_from_previous_step = len( + state.get("output_tok_ids", []) + ) - len(self.think_start_token_ids) + else: + sampled_tokens_from_previous_step = ( + len(state.get("output_tok_ids", [])) - state["prev_output_length"] + ) + current_step_countdown = ( + state["check_count_down"] - sampled_tokens_from_previous_step + ) + predicted_countdown = current_step_countdown - len(state["spec_token_ids"]) - 1 + # We only proceed further if we have counted down the thinking budget + # to 0 or less and when we are in the "in think" mode. + if ( + not state.get("in_end", False) + and predicted_countdown >= 0 + and state["start_thinking"] > -1 + ): + state["check_count_down"] = current_step_countdown + state["prev_output_length"] = len(state.get("output_tok_ids", [])) + return + output = state.get("output_tok_ids", []) + if not output: + # When in_end was set at init (budget=0, prompt already in think), + # we must force the first generated token to be the end token; + # otherwise apply() sees in_end=True but force_index=[] and + # allows an extra thinking token. + if state.get("in_end", False): + state["force_index"] = [0] + return + + # Track previous output length for incremental processing + prev_length = state.get("prev_output_length", 0) + current_length = len(output) + + if current_length <= prev_length: + if state.get("in_end", False): + remaining_budget = state["thinking_token_budget"] - state["think_count"] + spec_len = len(state["spec_token_ids"]) + if spec_len > 0: + if 0 < remaining_budget < spec_len: + state["force_index"] = [remaining_budget] + elif remaining_budget <= 0: + state["force_index"] = [0] + else: + state["force_index"] = [spec_len] + else: + state["force_index"] = [0] + return + + state["prev_output_length"] = current_length + + start_len = len(self.think_start_token_ids) + absolute_start_pos = state["start_thinking"] + + if state["continue_thinking"] and state["end_thinking"] > -1: + absolute_end_pos = state["end_thinking"] + len( + state.get("prompt_tok_ids") or [] + ) + else: + absolute_end_pos = state["end_thinking"] + # Update state based on recent sequences + # This is the case where we are in end mode, but the rejection sampler + # rejected a token before the end token, + # so we need to go back to think mode and wait for the next end token + # eg with 999: [2,4,5,999] -> [3,-1,-1,-1] + if state["in_end"] and state["end_count"] == 0: + new_tokens = output[prev_length:] + stopping_thinking = ( + self.think_end_token_ids[state["end_count"]] in new_tokens + ) + if not stopping_thinking: + state["in_think"] = True + state["in_end"] = False + state["end_count"] = 0 + state["bonus_token_forced"] = False + + if not state["in_end"]: + if absolute_start_pos >= 0 and absolute_end_pos >= 0: + # Case: ......... - entering think mode + if absolute_start_pos > absolute_end_pos: + new_think_count = current_length - (absolute_start_pos + start_len) + state["in_think"] = True + state["think_count"] = new_think_count + else: + # Case: ......... - exiting think mode + state["in_think"] = False + state["think_count"] = 0 + + elif absolute_start_pos >= 0 and not state["continue_thinking"]: + # Found think start - entering think mode + new_think_count = current_length - (absolute_start_pos + start_len) + state["in_think"] = True + state["think_count"] = new_think_count + + elif absolute_end_pos >= 0: + # Found think end - exiting think mode + state["in_think"] = False + state["think_count"] = 0 + + elif state["in_think"]: + # Continue thinking mode, increment count by new tokens + prompt_tok_ids = state.get("prompt_tok_ids") or [] + think_tokens_in_prompt = len(prompt_tok_ids) - ( + absolute_start_pos + start_len + ) + state["think_count"] = ( + len(state["output_tok_ids"]) + think_tokens_in_prompt + ) + if state["in_think"]: + remaining_budget = max( + 0, state["thinking_token_budget"] - state["think_count"] + ) + state["check_count_down"] = remaining_budget + else: + state["check_count_down"] = state["thinking_token_budget"] + + total_thinking_tokens = ( + state["think_count"] + len(state["spec_token_ids"]) + 1 + ) + # Check if need to transition to end mode + # If we have more thinking tokens than the budget, + # we need to transition to end mode + if ( + state["in_think"] + and total_thinking_tokens > state["thinking_token_budget"] + ): + # Calculate force_index: position within spec_token_ids where + # forcing starts. If we're already over budget without spec + # tokens, force from position 0. Force from the position + # where budget is exceeded. + state["in_think"] = False + state["in_end"] = True + state["end_count"] = 0 + state["check_count_down"] = state["thinking_token_budget"] + remaining_budget = state["thinking_token_budget"] - state["think_count"] + spec_len = len(state["spec_token_ids"]) + if 0 < remaining_budget < spec_len: + state["force_index"] = [remaining_budget] + + elif remaining_budget <= 0: + state["force_index"] = [0] + + else: + # remaining_budget >= spec_len: all spec tokens are within + # budget; force the bonus token position + state["force_index"] = [len(state["spec_token_ids"])] + + else: + state["force_index"] = [] + if len(state["spec_token_ids"]) > 0: + for i, token_id in enumerate(state["spec_token_ids"]): + if state["end_count"] + 1 < len(self.think_end_token_ids): + if token_id == self.think_end_token_ids[state["end_count"] + 1]: + state["end_count"] += 1 + else: + state["end_count"] += 1 + state["force_index"] = [i] + break + else: + state["end_count"] += 1 + if len(state["force_index"]) == 0: + state["end_count"] += 1 + state["force_index"] = [len(state["spec_token_ids"])] + else: + state["end_count"] += 1 + state["force_index"] = [0] + if state["end_count"] >= len(self.think_end_token_ids): + state.update( + { + "in_end": False, + "end_count": 0, + "check_count_down": state["thinking_token_budget"], + } + ) + + def _apply_forcing_to_logits( + self, + logits: torch.Tensor, + predict_bonus_token: bool, + spec_token_ids_for_layout: list[list[int]], + ) -> torch.Tensor: + self.mask[:] = False + cumulative_total = 0 + self.cu_num_tokens.clear() + + n_layout = len(spec_token_ids_for_layout) + if self._state: + n_layout = max(n_layout, max(self._state.keys()) + 1) + + for index in range(n_layout): + self.cu_num_tokens[index] = cumulative_total + spec_tokens = ( + spec_token_ids_for_layout[index] + if index < len(spec_token_ids_for_layout) + else [] + ) + if self.in_spec_mode: + cumulative_total += len(spec_tokens) if not predict_bonus_token else 1 + else: + cumulative_total += 1 + + for seq_idx in sorted(self._state.keys()): + if seq_idx not in self.cu_num_tokens: + continue + state = self._state[seq_idx] + if state.get("in_end", False): + # logits processor in spec mode are called twice + # once for bonus token logits and + # second time for the target logits + # in case the force index is bonus token index + # we change the force index to 0 + if predict_bonus_token: + if state.get("force_index") and state["force_index"][0] < len( + state["spec_token_ids"] + ): + continue + else: + state["force_index"] = [0] + # continue enforcing the end thinking tokens + if state["end_count"] > 0: + state["bonus_token_forced"] = False + if state and not state["bonus_token_forced"]: + force_index = state.get("force_index", []) + if len(force_index) == 0: + continue + end_count = state.get("end_count", 0) + for force_idx in force_index: + if end_count < len(self.think_end_token_ids): + mask_idx = self.cu_num_tokens[seq_idx] + force_idx + if mask_idx < len(self.mask) and mask_idx < logits.shape[0]: + self.mask[mask_idx] = True + self.force_token_ids[mask_idx] = ( + self.think_end_token_ids[end_count] + ) + if predict_bonus_token: + if state["end_count"] > 0: + state["bonus_token_forced"] = False + state["force_index"] = [] + else: + state["bonus_token_forced"] = True + + has_active_thinking = any( + state.get("in_end", False) for state in self._state.values() + ) + + if has_active_thinking: + active_indices = self.mask.nonzero(as_tuple=False).view(-1) + + if len(active_indices) > 0: + force_tokens = self.force_token_ids[active_indices] + logits[active_indices, force_tokens] = 1e9 + + return logits diff --git a/vllm/v1/worker/gpu_input_batch.py b/vllm/v1/worker/gpu_input_batch.py index 89e63f3def7a..5930a1aabaf7 100644 --- a/vllm/v1/worker/gpu_input_batch.py +++ b/vllm/v1/worker/gpu_input_batch.py @@ -8,6 +8,7 @@ import numpy as np import torch +from vllm.config.reasoning import ReasoningConfig from vllm.lora.request import LoRARequest from vllm.multimodal.inputs import MultiModalFeatureSpec from vllm.pooling_params import PoolingParams @@ -22,6 +23,9 @@ MoveDirectionality, ) from vllm.v1.sample.metadata import SamplingMetadata +from vllm.v1.sample.thinking_budget_state import ( + maybe_create_thinking_budget_state_holder, +) from vllm.v1.utils import copy_slice from vllm.v1.worker.block_table import MultiGroupBlockTable @@ -92,12 +96,20 @@ def __init__( max_num_blocks_per_req: list[int] | None = None, logitsprocs: LogitsProcessors | None = None, logitsprocs_need_output_token_ids: bool = False, - is_spec_decode: bool = False, + num_spec_tokens: int = 0, is_pooling_model: bool = False, cp_kv_cache_interleave_size: int = 1, + reasoning_config: ReasoningConfig | None = None, ): + self.thinking_budget_state_holder = maybe_create_thinking_budget_state_holder( + reasoning_config, + max_num_reqs, + num_spec_tokens, + device, + pin_memory, + ) + self.thinking_token_budget_reqs: set[str] = set() self.is_pooling_model = is_pooling_model - self.is_spec_decode = is_spec_decode self.max_num_reqs = max_num_reqs self.max_model_len = max_model_len self.max_num_batched_tokens = max_num_batched_tokens @@ -540,6 +552,7 @@ def remove_request(self, req_id: str) -> int | None: # False means we don't fill with -inf. self.allowed_token_ids_mask_cpu_tensor[req_index].fill_(False) self.bad_words_token_ids.pop(req_index, None) + self.thinking_token_budget_reqs.discard(req_id) return req_index def swap_states(self, i1: int, i2: int) -> None: @@ -800,6 +813,8 @@ def refresh_metadata(self): # reset batch update tracking. # Update sampling metadata if batch state is changed. batch_update = self.batch_update_builder.get_and_reset(self.num_reqs) + if self.thinking_budget_state_holder is not None and batch_update: + self.thinking_budget_state_holder.sync_batch(batch_update) for logit_proc in self.logitsprocs.all: logit_proc.update_state(batch_update) if batch_update: @@ -853,10 +868,15 @@ def _make_sampling_metadata(self) -> SamplingMetadata: # Only set output_token_ids if required by the current requests' # sampling parameters. + holder = self.thinking_budget_state_holder + thinking_budget_tracks_reqs = ( + holder is not None and holder.has_tracked_requests() + ) needs_output_token_ids = ( not self.no_penalties or bool(self.bad_words_token_ids) or self.logitsprocs_need_output_token_ids + or not thinking_budget_tracks_reqs ) output_token_ids = ( cast(list[list[int]], self.req_output_token_ids) @@ -902,6 +922,7 @@ def _make_sampling_metadata(self) -> SamplingMetadata: allowed_token_ids_mask=allowed_token_ids_mask, bad_words_token_ids=self.bad_words_token_ids, logitsprocs=self.logitsprocs, + thinking_budget_state_holder=self.thinking_budget_state_holder, ) def get_pooling_params(self) -> list[PoolingParams]: @@ -1076,6 +1097,13 @@ def no_penalties(self) -> bool: and len(self.repetition_penalties_reqs) == 0 ) + @property + def no_thinking_budget(self) -> bool: + return ( + self.thinking_budget_state_holder is None + or len(self.thinking_token_budget_reqs) == 0 + ) + @property def max_num_logprobs(self) -> int | None: return max(self.num_logprobs.values()) if self.num_logprobs else None diff --git a/vllm/v1/worker/gpu_model_runner.py b/vllm/v1/worker/gpu_model_runner.py index caf3bfdfc3a8..e2af34eecb96 100644 --- a/vllm/v1/worker/gpu_model_runner.py +++ b/vllm/v1/worker/gpu_model_runner.py @@ -629,7 +629,7 @@ def __init__( vocab_size=self.model_config.get_vocab_size(), block_sizes=[placeholder_block_size], kernel_block_sizes=[placeholder_block_size], - is_spec_decode=bool(self.vllm_config.speculative_config), + num_spec_tokens=self.num_spec_tokens, logitsprocs=build_logitsprocs( self.vllm_config, self.device, @@ -645,6 +645,7 @@ def __init__( or self.vllm_config.reasoning_config is not None, is_pooling_model=self.is_pooling_model, cp_kv_cache_interleave_size=self.parallel_config.cp_kv_cache_interleave_size, + reasoning_config=self.vllm_config.reasoning_config, ) # Separate cuda stream for overlapping transfer of sampled token ids from @@ -6504,10 +6505,11 @@ def may_reinitialize_input_batch( block_sizes=block_sizes, kernel_block_sizes=kernel_block_sizes, max_num_blocks_per_req=max_num_blocks, - is_spec_decode=bool(self.vllm_config.speculative_config), + num_spec_tokens=self.num_spec_tokens, logitsprocs=self.input_batch.logitsprocs, logitsprocs_need_output_token_ids=self.input_batch.logitsprocs_need_output_token_ids, is_pooling_model=self.is_pooling_model, + reasoning_config=self.vllm_config.reasoning_config, ) assert self._init_block_sizes == block_sizes, ( From 92879e12ba130e12bcc2728939eba86b2644122f Mon Sep 17 00:00:00 2001 From: Chauncey Date: Wed, 29 Apr 2026 15:32:37 +0800 Subject: [PATCH 034/237] [CI] fix test_rotary_embedding_opcheck format error (#41202) Signed-off-by: chaunceyjiang --- tests/kernels/core/test_rotary_embedding.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/kernels/core/test_rotary_embedding.py b/tests/kernels/core/test_rotary_embedding.py index 8410d1f1bcc6..1cbb5dbd1881 100644 --- a/tests/kernels/core/test_rotary_embedding.py +++ b/tests/kernels/core/test_rotary_embedding.py @@ -35,9 +35,7 @@ def rotary_embedding_opcheck( @pytest.mark.parametrize("seq_len", [11, 1024]) @pytest.mark.parametrize("use_key", [True, False]) @pytest.mark.parametrize("head_stride_is_contiguous", [True, False]) -@pytest.mark.parametrize( - "dtype", [torch.float32, torch.bfloat16] -) +@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16]) def test_rotary_embedding_opcheck( default_vllm_config, dist_init, From e48cb85185d792f5b4a595c2af3cbc37ac742aac Mon Sep 17 00:00:00 2001 From: Shengqi Chen Date: Wed, 29 Apr 2026 15:37:14 +0800 Subject: [PATCH 035/237] [CI/Build] Auto-detect manylinux ABI tag for nightly wheels (#41149) Signed-off-by: Shengqi Chen Co-authored-by: Claude --- .buildkite/release-pipeline.yaml | 12 +- .buildkite/scripts/detect-manylinux-tag.py | 142 ++++++++++++++++++ .../generate-and-upload-nightly-index.sh | 15 +- .buildkite/scripts/lib/manylinux.sh | 127 ++++++++++++++++ .buildkite/scripts/lib/select-python.sh | 41 +++++ .buildkite/scripts/upload-nightly-wheels.sh | 22 +-- .buildkite/scripts/upload-rocm-wheels.sh | 41 ++--- .gitignore | 1 + 8 files changed, 352 insertions(+), 49 deletions(-) create mode 100644 .buildkite/scripts/detect-manylinux-tag.py create mode 100644 .buildkite/scripts/lib/manylinux.sh create mode 100644 .buildkite/scripts/lib/select-python.sh diff --git a/.buildkite/release-pipeline.yaml b/.buildkite/release-pipeline.yaml index 8fce15680173..ffe5f4f2bd41 100644 --- a/.buildkite/release-pipeline.yaml +++ b/.buildkite/release-pipeline.yaml @@ -27,7 +27,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64_CU129}\" --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_31" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" @@ -40,7 +40,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64}\" --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_35" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" @@ -53,7 +53,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg GIT_REPO_CHECK=1 --build-arg VLLM_BUILD_ACL=ON --tag vllm-ci:build-image --target vllm-build --progress plain -f docker/Dockerfile.cpu ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_35" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" @@ -66,7 +66,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86_CU129}\" --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_31" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" @@ -79,7 +79,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86}\" --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag vllm-ci:build-image --target build --progress plain -f docker/Dockerfile ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_35" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" @@ -92,7 +92,7 @@ steps: - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg GIT_REPO_CHECK=1 --build-arg VLLM_CPU_X86=true --tag vllm-ci:build-image --target vllm-build --progress plain -f docker/Dockerfile.cpu ." - "mkdir artifacts" - "docker run --rm -v $(pwd)/artifacts:/artifacts_host vllm-ci:build-image bash -c 'cp -r dist /artifacts_host && chmod -R a+rw /artifacts_host'" - - "bash .buildkite/scripts/upload-nightly-wheels.sh manylinux_2_35" + - "bash .buildkite/scripts/upload-nightly-wheels.sh" env: DOCKER_BUILDKIT: "1" diff --git a/.buildkite/scripts/detect-manylinux-tag.py b/.buildkite/scripts/detect-manylinux-tag.py new file mode 100644 index 000000000000..40fa6c6ffbb7 --- /dev/null +++ b/.buildkite/scripts/detect-manylinux-tag.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Detect the manylinux platform tag for a wheel and rename it in place. + +vLLM's build images produce wheels with the generic ``linux_`` platform +tag, which installers like ``pip`` won't accept off PyPI/our index. We need to +rewrite the platform tag to the appropriate ``manylinux___`` +before uploading. + +Historically the tag was hard-coded per build (``manylinux_2_31`` for the +Ubuntu 20.04-based image, ``manylinux_2_35`` for the Ubuntu 22.04-based +images). That is brittle: bumping the base image silently produces wheels +labelled with the wrong glibc requirement. This script asks ``auditwheel`` +to derive the tag from the symbol versions actually referenced by the +binaries inside the wheel, so the label tracks reality. + +We can't simply call ``auditwheel repair`` -- it tries to graft external +shared libraries into the wheel and fails on vLLM's CUDA/cuBLAS dependencies. +Instead we use ``auditwheel.wheel_abi.analyze_wheel_abi`` directly, which is +the same call that powers ``auditwheel show``, and read off +``winfo.sym_policy.name``. + +Usage: + detect-manylinux-tag.py + +The wheel is renamed in place; the new path is printed on stdout. All +diagnostics go to stderr so callers can capture stdout safely. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from auditwheel.error import ( + AuditwheelError, + NonPlatformWheelError, + WheelToolsError, +) +from auditwheel.wheel_abi import analyze_wheel_abi +from auditwheel.wheeltools import get_wheel_architecture, get_wheel_libc + + +def detect_platform_tag(wheel_path: Path) -> str: + """Return the most precise platform tag the wheel is consistent with. + + Mirrors ``auditwheel show`` but returns ``sym_policy`` rather than + ``overall_policy``: we only care about the glibc symbol versions used, + not about other policy axes (ISA extensions, blacklist, etc.) that + ``overall_policy`` folds in. + """ + fn = wheel_path.name + + try: + arch = get_wheel_architecture(fn) + except (WheelToolsError, NonPlatformWheelError): + # Architecture isn't deducible from the filename; let auditwheel + # infer it from the ELF binaries inside the wheel. + arch = None + + try: + libc = get_wheel_libc(fn) + except WheelToolsError: + # An unrepaired wheel uses ``linux_``, which doesn't encode + # libc. Let auditwheel infer it from the ELF binaries. + libc = None + + winfo = analyze_wheel_abi( + libc, + arch, + wheel_path, + frozenset(), + disable_isa_ext_check=False, + allow_graft=False, + ) + return winfo.sym_policy.name + + +def rename_wheel(wheel_path: Path, new_platform_tag: str) -> Path: + """Rename the wheel in place, replacing only its platform tag.""" + # Wheel filename per PEP 427: + # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl + # The platform tag is always the last ``-``-separated token before + # ``.whl``. Compound tags like ``manylinux_2_31_x86_64`` use ``_`` as the + # internal separator, so ``-``-splitting is unambiguous. + parts = wheel_path.stem.split("-") + if len(parts) < 5: + raise ValueError(f"Unrecognised wheel filename: {wheel_path.name}") + parts[-1] = new_platform_tag + new_path = wheel_path.with_name("-".join(parts) + ".whl") + if new_path != wheel_path: + wheel_path.rename(new_path) + return new_path + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Detect a wheel's manylinux platform tag with " + "auditwheel and rename the wheel in place." + ) + parser.add_argument( + "wheel", + type=Path, + help="Path to the wheel to inspect and rename.", + ) + args = parser.parse_args() + + wheel_path: Path = args.wheel + if not wheel_path.is_file(): + print(f"error: {wheel_path} is not a file", file=sys.stderr) + return 1 + + # Catch the things that ``analyze_wheel_abi`` and ``rename_wheel`` can + # raise: any subclass of ``AuditwheelError`` (pure-Python wheels, + # invalid libc, malformed wheels), filesystem errors, or our own + # ``ValueError`` for an unrecognised wheel filename. Print a single + # ``ERROR_TYPE: message`` line to stderr instead of a Python + # traceback, which is much friendlier in CI logs. + try: + new_tag = detect_platform_tag(wheel_path) + print(f"detected platform tag: {new_tag}", file=sys.stderr) + new_path = rename_wheel(wheel_path, new_tag) + except (AuditwheelError, ValueError, OSError) as e: + print( + f"error: failed to retag {wheel_path.name}: {type(e).__name__}: {e}", + file=sys.stderr, + ) + return 2 + + if new_path != wheel_path: + print(f"renamed {wheel_path.name} -> {new_path.name}", file=sys.stderr) + else: + print(f"wheel already tagged {new_tag}", file=sys.stderr) + + print(new_path) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.buildkite/scripts/generate-and-upload-nightly-index.sh b/.buildkite/scripts/generate-and-upload-nightly-index.sh index 88c4f5173139..502ed0609310 100755 --- a/.buildkite/scripts/generate-and-upload-nightly-index.sh +++ b/.buildkite/scripts/generate-and-upload-nightly-index.sh @@ -10,20 +10,13 @@ set -ex BUCKET="vllm-wheels" INDICES_OUTPUT_DIR="indices" DEFAULT_VARIANT_ALIAS="cu130" # align with vLLM_MAIN_CUDA_VERSION in vllm/envs.py -PYTHON="${PYTHON_PROG:-python3}" # try to read from env var, otherwise use python3 SUBPATH=$BUILDKITE_COMMIT S3_COMMIT_PREFIX="s3://$BUCKET/$SUBPATH/" -# detect if python3.12+ is available -has_new_python=$($PYTHON -c "print(1 if __import__('sys').version_info >= (3,12) else 0)") -if [[ "$has_new_python" -eq 0 ]]; then - # use new python from docker - docker pull python:3-slim - PYTHON="docker run --rm -u $(id -u):$(id -g) -v $(pwd):/app -w /app python:3-slim python3" -fi - -echo "Using python interpreter: $PYTHON" -echo "Python version: $($PYTHON --version)" +# Select python3 (>= 3.12) -- local if available, else a docker fallback. +# shellcheck source=lib/select-python.sh +source .buildkite/scripts/lib/select-python.sh +select_python # ======== generate and upload indices ======== diff --git a/.buildkite/scripts/lib/manylinux.sh b/.buildkite/scripts/lib/manylinux.sh new file mode 100644 index 000000000000..bde2dfe0a3dc --- /dev/null +++ b/.buildkite/scripts/lib/manylinux.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# +# Shared helper for rewriting a wheel's platform tag from the generic +# ``linux_`` to the correct ``manylinux___``. +# After sourcing, call ``apply_manylinux_tag `` on each wheel +# that still carries the generic tag; the renamed path is printed on +# stdout (logs go to stderr). +# +# Why a pinned Docker container instead of using whatever Python +# happens to be on the agent: +# - vLLM's release agents are heterogeneous -- they don't agree on +# a Python minor version, and we can't rely on a particular +# ``auditwheel`` being installed. +# - ``detect-manylinux-tag.py`` reads ``auditwheel.wheel_abi`` and +# ``Policy.sym_policy``, which are *internal* APIs without a +# stability promise. Pinning both Python and auditwheel makes the +# detected tag a function of the inputs alone, and shifts version +# bumps from "implicit drift" to "deliberate, retested change". +# - Other release scripts (``generate-and-upload-nightly-index.sh``, +# ``upload-rocm-wheels.sh``) already use the python:3-slim image +# when the agent's interpreter is too old; this is the same idea +# made stricter. +# +# To keep the per-wheel cost down (the ROCm upload retags ~10 wheels +# each run), we install auditwheel into a long-lived helper container +# once on source, then ``docker exec`` into it for each call. +# +# Trap behaviour: +# - Sourcing installs an EXIT trap that calls ``manylinux_cleanup`` to +# tear down the helper container. Any EXIT trap that was already in +# place when this file was sourced is captured and run AFTER our +# cleanup, so we don't silently clobber it. +# - If a caller sets a new EXIT trap *after* sourcing, that trap will +# replace ours; in that case the caller should call +# ``manylinux_cleanup`` from their own handler. + +if [[ -n "${_MANYLINUX_LIB_SOURCED:-}" ]]; then + return 0 +fi +_MANYLINUX_LIB_SOURCED=1 + +# Pin both sides. Bump these deliberately and re-run a representative +# wheel from each build target through the detection. +_MANYLINUX_PYTHON_IMAGE="python:3.12-slim" +_MANYLINUX_AUDITWHEEL_VERSION="6.6.0" + +# Resolve our own directory (and the sibling detect script) using the +# canonical, symlink-resolved path. The container mounts cwd at the +# same absolute path on both sides, so all paths we hand to it -- the +# script, the wheel -- must canonicalise to a location under cwd. +_MANYLINUX_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +_MANYLINUX_DETECT_SCRIPT="$(cd "${_MANYLINUX_LIB_DIR}/.." && pwd -P)/detect-manylinux-tag.py" +_MANYLINUX_CWD="$(pwd -P)" + +docker pull --quiet "$_MANYLINUX_PYTHON_IMAGE" >/dev/null + +# Spin up a long-lived helper container so we install auditwheel once +# and then ``docker exec`` into it for each wheel. +# +# The container runs as root so ``pip install`` can write into the +# system site-packages; individual ``docker exec`` calls below pin +# themselves to the host UID so any file rename happens with host +# ownership, not root. +_MANYLINUX_CONTAINER="$(docker run -d --rm \ + -v "$_MANYLINUX_CWD:$_MANYLINUX_CWD" \ + -w "$_MANYLINUX_CWD" \ + "$_MANYLINUX_PYTHON_IMAGE" \ + sleep infinity)" +docker exec "$_MANYLINUX_CONTAINER" \ + pip install --quiet --disable-pip-version-check \ + --root-user-action=ignore \ + "auditwheel==${_MANYLINUX_AUDITWHEEL_VERSION}" + +# Public cleanup -- safe to call multiple times. +manylinux_cleanup() { + if [[ -n "${_MANYLINUX_CONTAINER:-}" ]]; then + docker rm -f "$_MANYLINUX_CONTAINER" >/dev/null 2>&1 || true + _MANYLINUX_CONTAINER="" + fi +} + +# Capture any EXIT trap that was already in place so we can chain to +# it rather than overwrite it. ``trap -p EXIT`` prints the handler in +# eval-able form (``trap -- 'CMD' EXIT``) or nothing if unset; we +# strip the wrapper to recover ``CMD``. Handles the common case -- +# CMDs without embedded single quotes -- and degrades gracefully (we +# still run our own cleanup) for the pathological case. +_manylinux_prev_exit_trap_cmd="" +_manylinux_existing_exit_trap="$(trap -p EXIT)" +if [[ -n "$_manylinux_existing_exit_trap" ]]; then + _tmp="${_manylinux_existing_exit_trap#trap -- \'}" + _manylinux_prev_exit_trap_cmd="${_tmp%\' EXIT}" + unset _tmp +fi +unset _manylinux_existing_exit_trap + +_manylinux_run_exit_chain() { + manylinux_cleanup + if [[ -n "$_manylinux_prev_exit_trap_cmd" ]]; then + eval "$_manylinux_prev_exit_trap_cmd" + fi +} +trap _manylinux_run_exit_chain EXIT + +# Detect the manylinux platform tag for a single wheel and rename it +# in place, printing the renamed wheel path on stdout. Returns +# non-zero on failure (which under ``set -e`` propagates to caller). +# +# The wheel must be reachable via a path under the host cwd so it's +# visible inside the helper container; in CI the wheels always live +# under ``artifacts/`` so this is fine. +apply_manylinux_tag() { + local wheel="$1" + local abs_wheel + abs_wheel="$(realpath "$wheel")" + local new_wheel + new_wheel="$(docker exec -u "$(id -u):$(id -g)" \ + "$_MANYLINUX_CONTAINER" \ + python "$_MANYLINUX_DETECT_SCRIPT" "$abs_wheel")" + if [[ -z "$new_wheel" || ! -f "$new_wheel" ]]; then + echo "apply_manylinux_tag: detect-manylinux-tag.py did not produce a valid wheel path for $wheel" >&2 + return 1 + fi + printf '%s\n' "$new_wheel" +} diff --git a/.buildkite/scripts/lib/select-python.sh b/.buildkite/scripts/lib/select-python.sh new file mode 100644 index 000000000000..bc53030a2b50 --- /dev/null +++ b/.buildkite/scripts/lib/select-python.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# +# Pick a Python interpreter for buildkite scripts: prefer a local +# ``python3`` if it is recent enough (>= 3.12), otherwise fall back to +# a one-shot Docker container running ``python:3-slim``. After +# ``select_python`` returns, ``$PYTHON`` is set in the caller's shell +# and is safe to use as a command (e.g. ``$PYTHON some_script.py``). +# +# The 3.12 threshold matches what the existing nightly-index work +# expects -- typing features used by ``generate-nightly-index.py``. +# This helper does not pin the *minor* version; if you need stricter +# reproducibility (e.g. relying on auditwheel internals), invoke +# Docker yourself with a pinned tag rather than calling this. + +if [[ -n "${_SELECT_PYTHON_LIB_SOURCED:-}" ]]; then + return 0 +fi +_SELECT_PYTHON_LIB_SOURCED=1 + +# Sets ``PYTHON`` in the caller's shell and exports it. Idempotent -- +# calling twice is safe and the second call simply re-runs the probe. +select_python() { + local py="${PYTHON_PROG:-python3}" + local has_new_python + has_new_python=$("$py" -c \ + "print(1 if __import__('sys').version_info >= (3,12) else 0)" \ + 2>/dev/null || echo 0) + if [[ "$has_new_python" -eq 0 ]]; then + # ``-u $(id -u):$(id -g)`` so files created via the container + # end up owned by the host user, not root. + docker pull python:3-slim + PYTHON="docker run --rm -u $(id -u):$(id -g) -v $(pwd):/app -w /app python:3-slim python3" + else + PYTHON="$py" + fi + export PYTHON + echo "Using python interpreter: $PYTHON" + echo "Python version: $($PYTHON --version)" +} diff --git a/.buildkite/scripts/upload-nightly-wheels.sh b/.buildkite/scripts/upload-nightly-wheels.sh index cc72cda7d505..8cef31908809 100644 --- a/.buildkite/scripts/upload-nightly-wheels.sh +++ b/.buildkite/scripts/upload-nightly-wheels.sh @@ -2,14 +2,18 @@ set -ex -# Upload a single wheel to S3 (rename linux -> manylinux). +# Upload a single wheel to S3, after detecting and applying the appropriate +# manylinux platform tag with auditwheel. # Index generation is handled separately by generate-and-upload-nightly-index.sh. +# shellcheck source=lib/manylinux.sh +source .buildkite/scripts/lib/manylinux.sh + BUCKET="vllm-wheels" SUBPATH=$BUILDKITE_COMMIT S3_COMMIT_PREFIX="s3://$BUCKET/$SUBPATH/" -# ========= collect, rename & upload the wheel ========== +# ========= locate the wheel ========== # Assume wheels are in artifacts/dist/*.whl wheel_files=(artifacts/dist/*.whl) @@ -21,19 +25,9 @@ if [[ ${#wheel_files[@]} -ne 1 ]]; then fi wheel="${wheel_files[0]}" -# default build image uses ubuntu 20.04, which corresponds to manylinux_2_31 -# we also accept params as manylinux tag -# refer to https://github.com/mayeut/pep600_compliance?tab=readme-ov-file#acceptable-distros-to-build-wheels -manylinux_version="${1:-manylinux_2_31}" +# ========= detect manylinux tag and rename ========== -# Rename 'linux' to the appropriate manylinux version in the wheel filename -if [[ "$wheel" != *"linux"* ]]; then - echo "Error: Wheel filename does not contain 'linux': $wheel" - exit 1 -fi -new_wheel="${wheel/linux/$manylinux_version}" -mv -- "$wheel" "$new_wheel" -wheel="$new_wheel" +wheel="$(apply_manylinux_tag "$wheel")" echo "Renamed wheel to: $wheel" # Extract the version from the wheel diff --git a/.buildkite/scripts/upload-rocm-wheels.sh b/.buildkite/scripts/upload-rocm-wheels.sh index a42848a16ffe..1f3655631204 100755 --- a/.buildkite/scripts/upload-rocm-wheels.sh +++ b/.buildkite/scripts/upload-rocm-wheels.sh @@ -20,10 +20,6 @@ BUCKET="${S3_BUCKET:-vllm-wheels}" ROCM_SUBPATH="rocm/${BUILDKITE_COMMIT}" S3_COMMIT_PREFIX="s3://$BUCKET/$ROCM_SUBPATH/" INDICES_OUTPUT_DIR="rocm-indices" -PYTHON="${PYTHON_PROG:-python3}" - -# ROCm uses manylinux_2_35 (Ubuntu 22.04 based) -MANYLINUX_VERSION="manylinux_2_35" echo "========================================" echo "ROCm Wheel Upload Configuration" @@ -34,19 +30,21 @@ echo "Commit: $BUILDKITE_COMMIT" echo "Branch: $BUILDKITE_BRANCH" echo "========================================" -# ======== Part 0: Setup Python ======== +# ======== Part 0: Setup Python and helpers ======== -# Detect if python3.12+ is available -has_new_python=$($PYTHON -c "print(1 if __import__('sys').version_info >= (3,12) else 0)" 2>/dev/null || echo 0) -if [[ "$has_new_python" -eq 0 ]]; then - # Use new python from docker - # Use --user to ensure files are created with correct ownership (not root) - docker pull python:3-slim - PYTHON="docker run --rm --user $(id -u):$(id -g) -v $(pwd):/app -w /app python:3-slim python3" -fi +# Pick a Python interpreter for index generation -- local if recent +# enough, else a one-shot docker fallback. +# shellcheck source=lib/select-python.sh +source .buildkite/scripts/lib/select-python.sh +select_python -echo "Using python interpreter: $PYTHON" -echo "Python version: $($PYTHON --version)" +# Set up auditwheel-in-a-container for the manylinux retagging step. +# Distinct from select_python: ``manylinux.sh`` deliberately pins both +# the Python and auditwheel versions (the script reads auditwheel +# internals) and so always runs in a known-good container regardless +# of what's on the agent. +# shellcheck source=lib/manylinux.sh +source .buildkite/scripts/lib/manylinux.sh # ======== Part 1: Collect and prepare wheels ======== @@ -63,11 +61,18 @@ if [ "$WHEEL_COUNT" -eq 0 ]; then exit 1 fi -# Rename linux to manylinux in wheel filenames +# Detect the appropriate manylinux platform tag for any wheel that still +# carries the generic ``linux_`` tag, and rename it in place. We use +# auditwheel via ``apply_manylinux_tag`` (see lib/manylinux.sh) rather than +# a hard-coded ``manylinux_2_35`` string so that the label tracks the actual +# glibc symbol versions used by the binaries (and stays correct if the +# rocm_base image is rebased). +# +# The ``linux``/``manylinux`` filter below skips both pre-tagged wheels +# (e.g. upstream torch) and pure-Python ``-any.whl`` wheels. for wheel in all-rocm-wheels/*.whl; do if [[ "$wheel" == *"linux"* ]] && [[ "$wheel" != *"manylinux"* ]]; then - new_wheel="${wheel/linux/$MANYLINUX_VERSION}" - mv -- "$wheel" "$new_wheel" + new_wheel="$(apply_manylinux_tag "$wheel")" echo "Renamed: $(basename "$wheel") -> $(basename "$new_wheel")" fi done diff --git a/.gitignore b/.gitignore index 134bbc5cc893..e53d19b35340 100644 --- a/.gitignore +++ b/.gitignore @@ -237,6 +237,7 @@ ep_kernels_workspace/ # Allow tracked library source folders under submodules (e.g., benchmarks/lib) !vllm/benchmarks/lib/ +!.buildkite/scripts/lib/ # Generated gRPC protobuf files (compiled at build time from vllm_engine.proto) vllm/grpc/vllm_engine_pb2.py From ef70057ca76688fc786c7fdee926ce2bd129b2c0 Mon Sep 17 00:00:00 2001 From: haosdent Date: Wed, 29 Apr 2026 16:28:45 +0800 Subject: [PATCH 036/237] [CI][CPU] Split CPU-Distributed Tests into per-scenario labels (#41203) Signed-off-by: haosdent --- .buildkite/hardware_tests/cpu.yaml | 17 ++++- .../run-cpu-distributed-smoke-test.sh | 71 +++++++++---------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/.buildkite/hardware_tests/cpu.yaml b/.buildkite/hardware_tests/cpu.yaml index 9b1044443780..ba8fd497c39e 100644 --- a/.buildkite/hardware_tests/cpu.yaml +++ b/.buildkite/hardware_tests/cpu.yaml @@ -69,11 +69,11 @@ steps: pytest -x -v -s tests/quantization/test_compressed_tensors.py::test_compressed_tensors_w8a8_logprobs pytest -x -v -s tests/quantization/test_cpu_wna16.py" -- label: CPU-Distributed Tests +- label: CPU-Distributed Tests (PP+TP) depends_on: [] device: intel_cpu no_plugin: true - source_file_dependencies: + source_file_dependencies: &cpu_distributed_deps - csrc/cpu/shm.cpp - vllm/v1/worker/cpu_worker.py - vllm/v1/worker/gpu_worker.py @@ -82,10 +82,21 @@ steps: - vllm/platforms/cpu.py - vllm/distributed/parallel_state.py - vllm/distributed/device_communicators/cpu_communicator.py + - .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh + commands: + - | + bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 10m " + bash .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh tp_pp" + +- label: CPU-Distributed Tests (DP+TP) + depends_on: [] + device: intel_cpu + no_plugin: true + source_file_dependencies: *cpu_distributed_deps commands: - | bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 10m " - bash .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh" + bash .buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh dp_tp" - label: CPU-Multi-Modal Model Tests %N depends_on: [] diff --git a/.buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh b/.buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh index f12bb524d4cb..8ac27ed6583a 100644 --- a/.buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh +++ b/.buildkite/scripts/hardware_ci/run-cpu-distributed-smoke-test.sh @@ -3,42 +3,37 @@ set -euox pipefail export VLLM_CPU_CI_ENV=0 export VLLM_CPU_KVCACHE_SPACE=1 # avoid OOM -echo "--- PP+TP" -vllm serve meta-llama/Llama-3.2-3B-Instruct -tp=2 -pp=2 --max-model-len=4096 & -server_pid=$! -timeout 600 bash -c "until curl localhost:8000/v1/models > /dev/null 2>&1; do sleep 1; done" || exit 1 -vllm bench serve \ - --backend vllm \ - --dataset-name random \ - --model meta-llama/Llama-3.2-3B-Instruct \ - --num-prompts 20 \ - --result-dir ./test_results \ - --result-filename tp_pp.json \ - --save-result \ - --endpoint /v1/completions -kill -s SIGTERM $server_pid; wait $server_pid || true -failed_req=$(jq '.failed' ./test_results/tp_pp.json) -if [ "$failed_req" -ne 0 ]; then - echo "Some requests were failed!" - exit 1 -fi +MODE=${1:-all} -echo "--- DP+TP" -vllm serve meta-llama/Llama-3.2-3B-Instruct -tp=2 -dp=2 --max-model-len=4096 & -server_pid=$! -timeout 600 bash -c "until curl localhost:8000/v1/models > /dev/null 2>&1; do sleep 1; done" || exit 1 -vllm bench serve \ - --backend vllm \ - --dataset-name random \ - --model meta-llama/Llama-3.2-3B-Instruct \ - --num-prompts 20 \ - --result-dir ./test_results \ - --result-filename dp_pp.json \ - --save-result \ - --endpoint /v1/completions -kill -s SIGTERM $server_pid; wait $server_pid || true -failed_req=$(jq '.failed' ./test_results/dp_pp.json) -if [ "$failed_req" -ne 0 ]; then - echo "Some requests were failed!" - exit 1 -fi +run_scenario() { + local label="$1" result_file="$2" + shift 2 + echo "--- $label" + vllm serve meta-llama/Llama-3.2-3B-Instruct "$@" --max-model-len=4096 & + local server_pid=$! + timeout 600 bash -c "until curl localhost:8000/v1/models > /dev/null 2>&1; do sleep 1; done" || exit 1 + vllm bench serve \ + --backend vllm \ + --dataset-name random \ + --model meta-llama/Llama-3.2-3B-Instruct \ + --num-prompts 20 \ + --result-dir ./test_results \ + --result-filename "$result_file" \ + --save-result \ + --endpoint /v1/completions + kill -s SIGTERM "$server_pid"; wait "$server_pid" || true + if [ "$(jq '.failed' "./test_results/$result_file")" -ne 0 ]; then + echo "Some requests were failed in $label!" + exit 1 + fi +} + +case "$MODE" in + tp_pp) run_scenario "PP+TP" tp_pp.json -tp=2 -pp=2 ;; + dp_tp) run_scenario "DP+TP" dp_tp.json -tp=2 -dp=2 ;; + all) + run_scenario "PP+TP" tp_pp.json -tp=2 -pp=2 + run_scenario "DP+TP" dp_tp.json -tp=2 -dp=2 + ;; + *) echo "ERROR: unknown mode '$MODE' (expected: tp_pp | dp_tp | all)" >&2; exit 1 ;; +esac From 3885d340a4779c54662b10892555ae6928b3a090 Mon Sep 17 00:00:00 2001 From: Chauncey Date: Wed, 29 Apr 2026 17:11:27 +0800 Subject: [PATCH 037/237] [Frontend]Responses API supports Tool/Function calling with streaming with named tool/function (#41110) Signed-off-by: chaunceyjiang --- .../openai/responses/test_function_call.py | 4 +- .../openai/chat_completion/serving.py | 56 +++++++------------ vllm/parser/abstract_parser.py | 49 ++++++++++------ vllm/tool_parsers/streaming.py | 8 +-- 4 files changed, 57 insertions(+), 60 deletions(-) diff --git a/tests/entrypoints/openai/responses/test_function_call.py b/tests/entrypoints/openai/responses/test_function_call.py index 515f31b399ee..8ca43feaca4f 100644 --- a/tests/entrypoints/openai/responses/test_function_call.py +++ b/tests/entrypoints/openai/responses/test_function_call.py @@ -323,7 +323,7 @@ async def test_function_calling_with_streaming_expected_arguments( @pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize( "tool_choice", - ["auto", "required"], + ["auto", "required", {"type": "function", "name": "get_current_weather"}], ) async def test_function_calling_with_streaming_types( client: openai.AsyncOpenAI, model_name: str, tool_choice @@ -462,7 +462,7 @@ async def test_function_calling_with_streaming_types( @pytest.mark.parametrize("model_name", [MODEL_NAME]) @pytest.mark.parametrize( "tool_choice", - ["required", "auto"], + ["required", "auto", {"type": "function", "name": "get_weather"}], ) async def test_function_calling_with_streaming_forced_tool_choice( client: openai.AsyncOpenAI, model_name: str, tool_choice: str diff --git a/vllm/entrypoints/openai/chat_completion/serving.py b/vllm/entrypoints/openai/chat_completion/serving.py index f001aac15a68..b8ad54adb5a6 100644 --- a/vllm/entrypoints/openai/chat_completion/serving.py +++ b/vllm/entrypoints/openai/chat_completion/serving.py @@ -70,7 +70,10 @@ from vllm.renderers import ChatParams from vllm.sampling_params import BeamSearchParams, SamplingParams from vllm.tokenizers import TokenizerLike -from vllm.tool_parsers.streaming import extract_required_tool_call_streaming +from vllm.tool_parsers.streaming import ( + extract_named_tool_call_streaming, + extract_required_tool_call_streaming, +) from vllm.utils.collection_utils import as_list from vllm.utils.mistral import is_mistral_tokenizer, is_mistral_tool_parser @@ -773,43 +776,24 @@ async def chat_completion_stream_generator( delta_text = previous_text + delta_text current_text = "" - if function_name_returned[i]: - delta_tool_call = DeltaToolCall( - function=DeltaFunctionCall(arguments=delta_text), - index=i, - ) - else: - # Generate ID based on tokenizer type - if is_mistral_tokenizer(tokenizer): - from vllm.tool_parsers.mistral_tool_parser import ( - MistralToolCall, - ) - - tool_call_id = MistralToolCall.generate_random_id() - else: - tool_call_id = make_tool_call_id( - id_type=self.tool_call_id_type, - func_name=tool_choice_function_name, - idx=history_tool_call_cnt, - ) - delta_tool_call = DeltaToolCall( - id=tool_call_id, - type="function", - function=DeltaFunctionCall( - name=tool_choice_function_name, - arguments=delta_text, - ), - index=i, + delta_message, function_name_returned[i] = ( + extract_named_tool_call_streaming( + delta_text=delta_text, + function_name=tool_choice_function_name, + function_name_returned=function_name_returned[i], + tool_call_idx=history_tool_call_cnt, + tool_call_id_type=self.tool_call_id_type, + tokenizer=tokenizer, + tool_call_array_index=i, ) - function_name_returned[i] = True - history_tool_call_cnt += 1 - - delta_message = DeltaMessage( - tool_calls=[ - delta_tool_call, - ] ) - tools_streamed[i] = True + if ( + delta_message + and delta_message.tool_calls + and delta_message.tool_calls[0].id is not None + ): + history_tool_call_cnt += 1 + tools_streamed[i] = True # Skip when tool_choice_uses_parser so it falls through # to the auto tool_parser branches below. diff --git a/vllm/parser/abstract_parser.py b/vllm/parser/abstract_parser.py index 5943e5aafbc4..e7f83686dbef 100644 --- a/vllm/parser/abstract_parser.py +++ b/vllm/parser/abstract_parser.py @@ -38,7 +38,10 @@ from vllm.reasoning.abs_reasoning_parsers import ReasoningParser from vllm.tokenizers import TokenizerLike from vllm.tool_parsers.abstract_tool_parser import ToolParser -from vllm.tool_parsers.streaming import extract_required_tool_call_streaming +from vllm.tool_parsers.streaming import ( + extract_named_tool_call_streaming, + extract_required_tool_call_streaming, +) from vllm.tool_parsers.utils import Tool from vllm.utils import random_uuid @@ -423,6 +426,17 @@ def extract_response_outputs( return outputs + def _get_function_name( + self, request: ChatCompletionRequest | ResponsesRequest + ) -> str: + if request.tool_choice and isinstance(request.tool_choice, ToolChoiceFunction): + return request.tool_choice.name + if request.tool_choice and isinstance( + request.tool_choice, ChatCompletionNamedToolChoiceParam + ): + return request.tool_choice.function.name + raise ValueError("Invalid tool_choice for function name extraction.") + def _parse_tool_calls( self, request: ResponsesRequest, @@ -440,21 +454,14 @@ def _parse_tool_calls( """ function_calls: list[FunctionCall] = [] - if request.tool_choice and isinstance(request.tool_choice, ToolChoiceFunction): - # Forced Function Call (Responses API style) - assert content is not None - function_calls.append( - FunctionCall(name=request.tool_choice.name, arguments=content) - ) - return function_calls, None # Clear content since tool is called. - if request.tool_choice and isinstance( - request.tool_choice, ChatCompletionNamedToolChoiceParam + request.tool_choice, + (ToolChoiceFunction, ChatCompletionNamedToolChoiceParam), ): - # Forced Function Call (Chat Completion API style) + # Forced Function Call assert content is not None function_calls.append( - FunctionCall(name=request.tool_choice.function.name, arguments=content) + FunctionCall(name=self._get_function_name(request), arguments=content) ) return function_calls, None # Clear content since tool is called. @@ -572,7 +579,7 @@ def _extract_tool_calls_streaming( previous_token_ids: Sequence[int], current_token_ids: Sequence[int], delta_token_ids: Sequence[int], - request: ChatCompletionRequest, + request: ChatCompletionRequest | ResponsesRequest, # The following parameters are used for "required" tool choice parsing and are # tracked in StreamState for streaming parsing. tool_call_idx: int | None = None, @@ -580,9 +587,19 @@ def _extract_tool_calls_streaming( function_name_returned: bool = False, ) -> tuple[DeltaMessage | None, bool]: if request.tool_choice and isinstance( - request.tool_choice, ChatCompletionNamedToolChoiceParam + request.tool_choice, + (ToolChoiceFunction, ChatCompletionNamedToolChoiceParam), ): - return None, False + delta_message, function_name_returned = extract_named_tool_call_streaming( + delta_text=delta_text, + function_name=self._get_function_name(request), + function_name_returned=function_name_returned, + tool_call_idx=tool_call_idx, + tool_call_id_type=tool_call_id_type, + tokenizer=self.model_tokenizer, + ) + return delta_message, function_name_returned + if request.tool_choice == "required": delta_message, function_name_returned = ( extract_required_tool_call_streaming( @@ -602,7 +619,7 @@ def _extract_tool_calls_streaming( previous_token_ids, current_token_ids, delta_token_ids, - request, + request, # type: ignore[arg-type] ), False def is_reasoning_end(self, input_ids: list[int]) -> bool: diff --git a/vllm/tool_parsers/streaming.py b/vllm/tool_parsers/streaming.py index fc903328e334..7f6638dcb94e 100644 --- a/vllm/tool_parsers/streaming.py +++ b/vllm/tool_parsers/streaming.py @@ -67,10 +67,9 @@ def extract_named_tool_call_streaming( tool_call_idx: int | None, tool_call_id_type: str, tokenizer: "TokenizerLike", - tool_call_array_index: int, -) -> tuple[DeltaMessage, bool, bool]: + tool_call_array_index: int = 0, +) -> tuple[DeltaMessage | None, bool]: """Build a streaming tool-call delta for forced named tool choice.""" - created_new_tool_call = False if function_name_returned: delta_tool_call = DeltaToolCall( function=DeltaFunctionCall(arguments=delta_text), @@ -95,12 +94,9 @@ def extract_named_tool_call_streaming( index=tool_call_array_index, ) function_name_returned = True - created_new_tool_call = True - return ( DeltaMessage(tool_calls=[delta_tool_call]), function_name_returned, - created_new_tool_call, ) From 762022cafb1afc4c51ce706c043e2f1f5826003a Mon Sep 17 00:00:00 2001 From: Chauncey Date: Wed, 29 Apr 2026 17:55:07 +0800 Subject: [PATCH 038/237] [Bugfix] DSV32/V4 add missing type conversion for non-streaming tool calls (#41198) Signed-off-by: chaunceyjiang --- .../test_deepseekv32_tool_parser.py | 24 +++++++++++++++++++ vllm/tool_parsers/deepseekv32_tool_parser.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/tool_parsers/test_deepseekv32_tool_parser.py b/tests/tool_parsers/test_deepseekv32_tool_parser.py index 6145253d9f90..c547795e7bf2 100644 --- a/tests/tool_parsers/test_deepseekv32_tool_parser.py +++ b/tests/tool_parsers/test_deepseekv32_tool_parser.py @@ -188,6 +188,30 @@ def test_multiple_tools(self, parser): "location": "NYC" } + def test_type_conversion_in_non_streaming(self): + """Non-streaming extraction must convert params using the tool schema.""" + tool = ChatCompletionToolsParam( + function=FunctionDefinition( + name="toggle", + parameters={ + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "count": {"type": "integer"}, + }, + }, + ), + ) + parser = make_parser(tools=[tool]) + model_output = build_tool_call("toggle", {"enabled": "true", "count": "42"}) + result = parser.extract_tool_calls(model_output, None) + assert result.tools_called + assert len(result.tool_calls) == 1 + args = json.loads(result.tool_calls[0].function.arguments) + assert args == {"enabled": True, "count": 42} + assert isinstance(args["enabled"], bool) + assert isinstance(args["count"], int) + # --------------------------------------------------------------------------- # Tests: extract_tool_calls_streaming diff --git a/vllm/tool_parsers/deepseekv32_tool_parser.py b/vllm/tool_parsers/deepseekv32_tool_parser.py index b8623592365c..02182e22935a 100644 --- a/vllm/tool_parsers/deepseekv32_tool_parser.py +++ b/vllm/tool_parsers/deepseekv32_tool_parser.py @@ -191,12 +191,13 @@ def extract_tool_calls( tool_call_match ): param_dict = self._parse_invoke_params(invoke_content) + params = self._convert_params_with_schema(invoke_name, param_dict) tool_calls.append( ToolCall( type="function", function=FunctionCall( name=invoke_name, - arguments=json.dumps(param_dict, ensure_ascii=False), + arguments=json.dumps(params, ensure_ascii=False), ), ) ) From 3f1a4bb639a9b65e2634a6529c90da36944d6472 Mon Sep 17 00:00:00 2001 From: Alec <35311602+alec-flowers@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:07:41 -0700 Subject: [PATCH 039/237] build: embed image provenance metadata in vLLM containers (#40653) Signed-off-by: Alec Flowers Co-authored-by: OpenAI Codex --- .buildkite/image_build/image_build.sh | 1 + .buildkite/release-pipeline.yaml | 116 ++++++++++++- .../scripts/docker-build-metadata-args.sh | 54 +++++++ .buildkite/test_areas/docker.yaml | 16 ++ docker/Dockerfile | 16 ++ docker/Dockerfile.cpu | 1 + docker/docker-bake.hcl | 30 +++- .../tools/test_docker_build_metadata_args.py | 152 ++++++++++++++++++ 8 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 .buildkite/scripts/docker-build-metadata-args.sh create mode 100644 .buildkite/test_areas/docker.yaml create mode 100644 tests/tools/test_docker_build_metadata_args.py diff --git a/.buildkite/image_build/image_build.sh b/.buildkite/image_build/image_build.sh index 00ae34bba6d7..10c03c3e1773 100755 --- a/.buildkite/image_build/image_build.sh +++ b/.buildkite/image_build/image_build.sh @@ -192,6 +192,7 @@ export BUILDKITE_COMMIT export PARENT_COMMIT export IMAGE_TAG export IMAGE_TAG_LATEST +export COMMIT="${COMMIT:-${BUILDKITE_COMMIT}}" export CACHE_FROM export CACHE_FROM_BASE_BRANCH export CACHE_FROM_MAIN diff --git a/.buildkite/release-pipeline.yaml b/.buildkite/release-pipeline.yaml index ffe5f4f2bd41..74227da45c71 100644 --- a/.buildkite/release-pipeline.yaml +++ b/.buildkite/release-pipeline.yaml @@ -121,7 +121,19 @@ steps: queue: cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86}\" --build-arg INSTALL_KV_CONNECTORS=true --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m) --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.0.2 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_X86}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)" # re-tag to default image tag and push, just in case arm64 build fails - "docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m) public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT" @@ -134,7 +146,19 @@ steps: queue: arm64_cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64}\" --build-arg INSTALL_KV_CONNECTORS=true --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m) --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.0.2 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu22.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)" - label: "Build release image - x86_64 - CUDA 12.9" @@ -144,7 +168,18 @@ steps: queue: cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86_CU129}\" --build-arg INSTALL_KV_CONNECTORS=true --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu129) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=12.9.1 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_X86_CU129}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129" # re-tag to default image tag and push, just in case arm64 build fails - "docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129 public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu129" @@ -157,7 +192,18 @@ steps: queue: arm64_cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64_CU129}\" --build-arg INSTALL_KV_CONNECTORS=true --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu129) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=12.9.1 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64_CU129}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129" - label: "Build release image - x86_64 - CUDA 13.0 - Ubuntu 24.04" @@ -167,7 +213,21 @@ steps: queue: cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg UBUNTU_VERSION=24.04 --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86}\" --build-arg INSTALL_KV_CONNECTORS=true --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu24.04 --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-ubuntu2404 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh ubuntu2404) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.0.2 \ + --build-arg UBUNTU_VERSION=24.04 \ + --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_X86}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu24.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-ubuntu2404" - "docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-ubuntu2404 public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-ubuntu2404" - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-ubuntu2404" @@ -179,7 +239,21 @@ steps: queue: arm64_cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=13.0.2 --build-arg UBUNTU_VERSION=24.04 --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64}\" --build-arg INSTALL_KV_CONNECTORS=true --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu24.04 --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-ubuntu2404 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh ubuntu2404) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=13.0.2 \ + --build-arg UBUNTU_VERSION=24.04 \ + --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --build-arg BUILD_BASE_IMAGE=nvidia/cuda:13.0.2-devel-ubuntu24.04 \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-ubuntu2404" - label: "Build release image - x86_64 - CUDA 12.9 - Ubuntu 24.04" @@ -189,7 +263,20 @@ steps: queue: cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg UBUNTU_VERSION=24.04 --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_X86_CU129}\" --build-arg INSTALL_KV_CONNECTORS=true --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129-ubuntu2404 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu129-ubuntu2404) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=12.9.1 \ + --build-arg UBUNTU_VERSION=24.04 \ + --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_X86_CU129}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129-ubuntu2404" - "docker tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129-ubuntu2404 public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu129-ubuntu2404" - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-cu129-ubuntu2404" @@ -201,7 +288,20 @@ steps: queue: arm64_cpu_queue_release commands: - "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7" - - "DOCKER_BUILDKIT=1 docker build --build-arg max_jobs=16 --build-arg USE_SCCACHE=1 --build-arg GIT_REPO_CHECK=1 --build-arg CUDA_VERSION=12.9.1 --build-arg UBUNTU_VERSION=24.04 --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 --build-arg torch_cuda_arch_list=\"${CUDA_ARCH_AARCH64_CU129}\" --build-arg INSTALL_KV_CONNECTORS=true --tag public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129-ubuntu2404 --target vllm-openai --progress plain -f docker/Dockerfile ." + - | + DOCKER_BUILDKIT=1 docker build \ + $(bash .buildkite/scripts/docker-build-metadata-args.sh cu129-ubuntu2404) \ + --build-arg max_jobs=16 \ + --build-arg USE_SCCACHE=1 \ + --build-arg GIT_REPO_CHECK=1 \ + --build-arg CUDA_VERSION=12.9.1 \ + --build-arg UBUNTU_VERSION=24.04 \ + --build-arg GDRCOPY_OS_VERSION=Ubuntu24_04 \ + --build-arg torch_cuda_arch_list="${CUDA_ARCH_AARCH64_CU129}" \ + --build-arg INSTALL_KV_CONNECTORS=true \ + --target vllm-openai \ + --progress plain \ + -f docker/Dockerfile . - "docker push public.ecr.aws/q9t5s3a7/vllm-release-repo:$BUILDKITE_COMMIT-$(uname -m)-cu129-ubuntu2404" - block: "Build release image for x86_64 CPU" diff --git a/.buildkite/scripts/docker-build-metadata-args.sh b/.buildkite/scripts/docker-build-metadata-args.sh new file mode 100644 index 000000000000..9aa6fa9314f7 --- /dev/null +++ b/.buildkite/scripts/docker-build-metadata-args.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Emit docker build flags for release image provenance metadata. +# Keep this helper best-effort: missing Buildkite metadata should fall back to +# local/default values instead of blocking the Docker build. + +# Variant examples: "", "cu129", "ubuntu2404", "cu129-ubuntu2404". +variant="${1:-}" +variant_suffix="${variant:+-${variant}}" + +image_name="${VLLM_DOCKER_IMAGE_NAME:-vllm/vllm-openai}" +staging_repo="${VLLM_STAGING_IMAGE_REPO:-public.ecr.aws/q9t5s3a7/vllm-release-repo}" +build_commit="${VLLM_BUILD_COMMIT:-${BUILDKITE_COMMIT:-unknown}}" +build_pipeline="${VLLM_BUILD_PIPELINE:-${BUILDKITE_PIPELINE_ID:-${BUILDKITE_PIPELINE_SLUG:-local}}}" +build_url="${VLLM_BUILD_URL:-${BUILDKITE_BUILD_URL:-}}" +tag_commit="${BUILDKITE_COMMIT:-${build_commit}}" + +if [[ -n "${BUILDKITE:-}" || -n "${BUILDKITE_COMMIT:-}" ]]; then + release_version="${RELEASE_VERSION:-}" + if command -v buildkite-agent >/dev/null 2>&1; then + release_version="${release_version:-$(buildkite-agent meta-data get release-version 2>/dev/null)}" + fi + release_version="${release_version#v}" + release_version="${release_version:-${tag_commit}}" + + staging_image_ref="${staging_repo}:${tag_commit}-$(uname -m)${variant_suffix}" + + if [[ "${NIGHTLY:-}" == "1" ]]; then + if [[ -z "${variant}" ]]; then + image_tag="${image_name}:nightly-${tag_commit}" + elif [[ "${variant}" == cu* ]]; then + cuda_variant="${variant%%-*}" + remaining_variant="${variant#${cuda_variant}}" + image_tag="${image_name}:${cuda_variant}-nightly-${tag_commit}${remaining_variant}" + else + image_tag="${image_name}:nightly-${tag_commit}${variant_suffix}" + fi + else + image_tag="${image_name}:v${release_version}${variant_suffix}" + fi +else + image_tag="${VLLM_IMAGE_TAG:-local/vllm-openai:dev}" + staging_image_ref="${image_tag}" +fi + +emit_arg() { + printf -- "--build-arg %s=%s " "$1" "$2" +} + +emit_arg VLLM_BUILD_COMMIT "${build_commit}" +emit_arg VLLM_BUILD_PIPELINE "${build_pipeline}" +emit_arg VLLM_BUILD_URL "${build_url}" +# This is the intended public tag. The final digest is only known after push. +emit_arg VLLM_IMAGE_TAG "${image_tag}" +printf -- "--tag %s " "${staging_image_ref}" diff --git a/.buildkite/test_areas/docker.yaml b/.buildkite/test_areas/docker.yaml new file mode 100644 index 000000000000..9bf96221abe0 --- /dev/null +++ b/.buildkite/test_areas/docker.yaml @@ -0,0 +1,16 @@ +group: Docker +depends_on: + - image-build-cpu +steps: +- label: Docker Build Metadata + timeout_in_minutes: 10 + device: cpu-small + source_file_dependencies: + - .buildkite/release-pipeline.yaml + - .buildkite/scripts/docker-build-metadata-args.sh + - docker/Dockerfile + - docker/Dockerfile.cpu + - docker/docker-bake.hcl + - tests/tools/test_docker_build_metadata_args.py + commands: + - pytest -v -s tools/test_docker_build_metadata_args.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 3d652a5dea62..a6b291407713 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -763,6 +763,10 @@ FROM vllm-base AS vllm-openai-base ARG TARGETPLATFORM ARG INSTALL_KV_CONNECTORS=false ARG CUDA_VERSION +ARG VLLM_BUILD_COMMIT +ARG VLLM_BUILD_PIPELINE +ARG VLLM_BUILD_URL +ARG VLLM_IMAGE_TAG ARG PIP_INDEX_URL UV_INDEX_URL ARG PIP_EXTRA_INDEX_URL UV_EXTRA_INDEX_URL @@ -799,6 +803,18 @@ RUN --mount=type=cache,target=/root/.cache/uv \ fi ENV VLLM_USAGE_SOURCE production-docker-image +ENV VLLM_BUILD_COMMIT=${VLLM_BUILD_COMMIT:-unknown} \ + VLLM_BUILD_PIPELINE=${VLLM_BUILD_PIPELINE:-local} \ + VLLM_BUILD_URL=${VLLM_BUILD_URL:-} \ + VLLM_IMAGE_TAG=${VLLM_IMAGE_TAG:-local/vllm-openai:dev} +LABEL org.opencontainers.image.source="https://github.com/vllm-project/vllm" \ + org.opencontainers.image.revision="${VLLM_BUILD_COMMIT}" \ + org.opencontainers.image.version="${VLLM_IMAGE_TAG}" \ + org.opencontainers.image.url="${VLLM_BUILD_URL}" \ + ai.vllm.build.commit="${VLLM_BUILD_COMMIT}" \ + ai.vllm.build.pipeline="${VLLM_BUILD_PIPELINE}" \ + ai.vllm.build.url="${VLLM_BUILD_URL}" \ + ai.vllm.image.tag="${VLLM_IMAGE_TAG}" # define sagemaker first, so it is not default from `docker build` FROM vllm-openai-base AS vllm-sagemaker diff --git a/docker/Dockerfile.cpu b/docker/Dockerfile.cpu index 72ee002d90a4..d15ced8e0111 100644 --- a/docker/Dockerfile.cpu +++ b/docker/Dockerfile.cpu @@ -192,6 +192,7 @@ ADD ./tests/ ./tests/ ADD ./examples/ ./examples/ ADD ./benchmarks/ ./benchmarks/ ADD ./vllm/collect_env.py . +ADD ./docker/ ./docker/ ADD ./.buildkite/ ./.buildkite/ # install development dependencies (for testing) diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl index 785d598d6080..94ca8397561a 100644 --- a/docker/docker-bake.hcl +++ b/docker/docker-bake.hcl @@ -27,6 +27,22 @@ variable "COMMIT" { default = "" } +variable "VLLM_BUILD_COMMIT" { + default = "unknown" +} + +variable "VLLM_BUILD_PIPELINE" { + default = "local" +} + +variable "VLLM_BUILD_URL" { + default = "" +} + +variable "VLLM_IMAGE_TAG" { + default = "local/vllm-openai:dev" +} + # Groups group "default" { @@ -46,6 +62,10 @@ target "_common" { max_jobs = MAX_JOBS nvcc_threads = NVCC_THREADS torch_cuda_arch_list = TORCH_CUDA_ARCH_LIST + VLLM_BUILD_COMMIT = VLLM_BUILD_COMMIT != "unknown" ? VLLM_BUILD_COMMIT : (COMMIT != "" ? COMMIT : "unknown") + VLLM_BUILD_PIPELINE = VLLM_BUILD_PIPELINE + VLLM_BUILD_URL = VLLM_BUILD_URL + VLLM_IMAGE_TAG = VLLM_IMAGE_TAG } } @@ -56,10 +76,16 @@ target "_labels" { "org.opencontainers.image.title" = "vLLM" "org.opencontainers.image.description" = "vLLM: A high-throughput and memory-efficient inference and serving engine for LLMs" "org.opencontainers.image.licenses" = "Apache-2.0" - "org.opencontainers.image.revision" = COMMIT + "org.opencontainers.image.revision" = VLLM_BUILD_COMMIT != "unknown" ? VLLM_BUILD_COMMIT : (COMMIT != "" ? COMMIT : "unknown") + "org.opencontainers.image.version" = VLLM_IMAGE_TAG + "org.opencontainers.image.url" = VLLM_BUILD_URL + "ai.vllm.build.commit" = VLLM_BUILD_COMMIT != "unknown" ? VLLM_BUILD_COMMIT : (COMMIT != "" ? COMMIT : "unknown") + "ai.vllm.build.pipeline" = VLLM_BUILD_PIPELINE + "ai.vllm.build.url" = VLLM_BUILD_URL + "ai.vllm.image.tag" = VLLM_IMAGE_TAG } annotations = [ - "index,manifest:org.opencontainers.image.revision=${COMMIT}", + "index,manifest:org.opencontainers.image.revision=${VLLM_BUILD_COMMIT != "unknown" ? VLLM_BUILD_COMMIT : (COMMIT != "" ? COMMIT : "unknown")}", ] } diff --git a/tests/tools/test_docker_build_metadata_args.py b/tests/tools/test_docker_build_metadata_args.py new file mode 100644 index 000000000000..fa2eac558f53 --- /dev/null +++ b/tests/tools/test_docker_build_metadata_args.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import os +import shlex +import subprocess +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +HELPER = REPO_ROOT / ".buildkite" / "scripts" / "docker-build-metadata-args.sh" + + +def run_helper( + *args: str, + env: dict[str, str] | None = None, + path: str | None = None, +) -> list[str]: + helper_env = {"PATH": path or os.environ["PATH"]} + if env: + helper_env.update(env) + result = subprocess.run( + ["bash", str(HELPER), *args], + check=True, + env=helper_env, + stdout=subprocess.PIPE, + text=True, + ) + return shlex.split(result.stdout) + + +def option_values(args: list[str], option: str) -> list[str]: + return [args[i + 1] for i, arg in enumerate(args[:-1]) if arg == option] + + +def build_args(args: list[str]) -> dict[str, str]: + values = {} + for value in option_values(args, "--build-arg"): + key, arg_value = value.split("=", 1) + values[key] = arg_value + return values + + +def test_release_metadata_args_prefer_pipeline_id() -> None: + args = run_helper( + "cu130-ubuntu2404", + env={ + "BUILDKITE": "1", + "BUILDKITE_COMMIT": "abc123", + "BUILDKITE_PIPELINE_ID": "pipe-uuid", + "BUILDKITE_PIPELINE_SLUG": "release", + "BUILDKITE_BUILD_URL": "https://buildkite.example/vllm/builds/1", + "RELEASE_VERSION": "v0.20.0", + }, + ) + + assert build_args(args) == { + "VLLM_BUILD_COMMIT": "abc123", + "VLLM_BUILD_PIPELINE": "pipe-uuid", + "VLLM_BUILD_URL": "https://buildkite.example/vllm/builds/1", + "VLLM_IMAGE_TAG": "vllm/vllm-openai:v0.20.0-cu130-ubuntu2404", + } + expected_tag = ( + "public.ecr.aws/q9t5s3a7/vllm-release-repo:" + f"abc123-{os.uname().machine}-cu130-ubuntu2404" + ) + assert option_values(args, "--tag") == [expected_tag] + + +def test_nightly_metadata_args_fall_back_to_pipeline_slug() -> None: + args = run_helper( + "ubuntu2404", + env={ + "BUILDKITE": "1", + "BUILDKITE_COMMIT": "def456", + "BUILDKITE_PIPELINE_SLUG": "release", + "BUILDKITE_BUILD_URL": "https://buildkite.example/vllm/builds/2", + "NIGHTLY": "1", + }, + ) + + assert build_args(args) == { + "VLLM_BUILD_COMMIT": "def456", + "VLLM_BUILD_PIPELINE": "release", + "VLLM_BUILD_URL": "https://buildkite.example/vllm/builds/2", + "VLLM_IMAGE_TAG": "vllm/vllm-openai:nightly-def456-ubuntu2404", + } + expected_tag = ( + "public.ecr.aws/q9t5s3a7/vllm-release-repo:" + f"def456-{os.uname().machine}-ubuntu2404" + ) + assert option_values(args, "--tag") == [expected_tag] + + +def test_local_metadata_args_use_local_overrides() -> None: + args = run_helper( + env={ + "VLLM_IMAGE_TAG": "local/test:dev", + "VLLM_BUILD_COMMIT": "localsha", + "VLLM_BUILD_PIPELINE": "local-pipeline", + "VLLM_BUILD_URL": "https://buildkite.example/local", + }, + ) + + assert build_args(args) == { + "VLLM_BUILD_COMMIT": "localsha", + "VLLM_BUILD_PIPELINE": "local-pipeline", + "VLLM_BUILD_URL": "https://buildkite.example/local", + "VLLM_IMAGE_TAG": "local/test:dev", + } + assert option_values(args, "--tag") == ["local/test:dev"] + + +def test_release_version_lookup_failure_falls_back_to_commit( + tmp_path: Path, +) -> None: + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + buildkite_agent = fake_bin / "buildkite-agent" + buildkite_agent.write_text("#!/bin/sh\nexit 1\n") + buildkite_agent.chmod(0o755) + + args = run_helper( + "cu129", + env={ + "BUILDKITE": "1", + "BUILDKITE_COMMIT": "fallback123", + "BUILDKITE_PIPELINE_SLUG": "release", + }, + path=f"{fake_bin}:{os.environ['PATH']}", + ) + + assert build_args(args)["VLLM_IMAGE_TAG"] == ("vllm/vllm-openai:vfallback123-cu129") + + +def test_vllm_openai_image_embeds_metadata_contract() -> None: + dockerfile = (REPO_ROOT / "docker" / "Dockerfile").read_text() + + for expected in ( + "ARG VLLM_BUILD_COMMIT", + "ARG VLLM_BUILD_PIPELINE", + "ARG VLLM_BUILD_URL", + "ARG VLLM_IMAGE_TAG", + "VLLM_BUILD_COMMIT=${VLLM_BUILD_COMMIT:-unknown}", + "VLLM_BUILD_PIPELINE=${VLLM_BUILD_PIPELINE:-local}", + "VLLM_BUILD_URL=${VLLM_BUILD_URL:-}", + "VLLM_IMAGE_TAG=${VLLM_IMAGE_TAG:-local/vllm-openai:dev}", + 'ai.vllm.build.commit="${VLLM_BUILD_COMMIT}"', + 'ai.vllm.build.pipeline="${VLLM_BUILD_PIPELINE}"', + 'ai.vllm.build.url="${VLLM_BUILD_URL}"', + 'ai.vllm.image.tag="${VLLM_IMAGE_TAG}"', + ): + assert expected in dockerfile From 6d7d4da99e41c4ccc0d52d74e2bf36d1ff31034d Mon Sep 17 00:00:00 2001 From: Jiangyun Zhu Date: Wed, 29 Apr 2026 18:08:55 +0800 Subject: [PATCH 040/237] [Bugfix] BailingMoeV2.5: rotate full qk_rope_head_dim in MLA RoPE (#41185) Signed-off-by: zjy0516 --- vllm/model_executor/models/bailing_moe_linear.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vllm/model_executor/models/bailing_moe_linear.py b/vllm/model_executor/models/bailing_moe_linear.py index 55ea1bad44db..bcf1200bd1b4 100644 --- a/vllm/model_executor/models/bailing_moe_linear.py +++ b/vllm/model_executor/models/bailing_moe_linear.py @@ -205,13 +205,19 @@ def __init__( self.q_a_layernorm = None self.q_b_proj = None - rope_parameters = _build_rope_parameters(config) + rope_parameters = _build_rope_parameters(config) or {} + # MLA rotates the full qk_rope_head_dim, + # partial_rotary_factor is for the linear-attn head only. + rope_parameters = { + k: v for k, v in rope_parameters.items() if k != "partial_rotary_factor" + } + rope_parameters["rope_dim"] = self.qk_rope_head_dim max_position = getattr(config, "max_position_embeddings", 8192) self.rotary_emb = get_rope( head_size=self.qk_rope_head_dim, max_position=max_position, is_neox_style=False, - rope_parameters=rope_parameters or None, + rope_parameters=rope_parameters, ) # Build MLAModules for MultiHeadLatentAttentionWrapper From 5371d6fb4023a1a08021135e46e9354ba0923e50 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Singh <9626333+SKRohit@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:47:51 +0530 Subject: [PATCH 041/237] Fix PP in Gemma4 (#40786) Signed-off-by: Rohit kumar Singh Co-authored-by: Cyrus Leung --- vllm/model_executor/models/gemma4.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/vllm/model_executor/models/gemma4.py b/vllm/model_executor/models/gemma4.py index bb91fd601e70..b724fa71968c 100644 --- a/vllm/model_executor/models/gemma4.py +++ b/vllm/model_executor/models/gemma4.py @@ -1144,11 +1144,6 @@ def _make_empty_intermediate_tensors( dtype=dtype, device=device, ), - "residual": torch.zeros( - (batch_size, hidden_size), - dtype=dtype, - device=device, - ), } if ple_dim and ple_dim > 0: tensors["per_layer_inputs"] = torch.zeros( @@ -1312,13 +1307,12 @@ def forward( per_layer_inputs = self.project_per_layer_inputs( hidden_states, per_layer_embeds ) - residual = None else: assert intermediate_tensors is not None hidden_states = intermediate_tensors["hidden_states"] - residual = intermediate_tensors["residual"] - per_layer_inputs = intermediate_tensors.get("per_layer_inputs") - + if per_layer_inputs is not None: + per_layer_inputs = intermediate_tensors["per_layer_inputs"] + residual = None aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual) for layer_idx, layer in enumerate( islice(self.layers, self.start_layer, self.end_layer) @@ -1342,13 +1336,12 @@ def forward( aux_hidden_states, layer_idx + 1, hidden_states, residual ) if not get_pp_group().is_last_rank: - return IntermediateTensors( - { - "hidden_states": hidden_states, - "residual": residual, - "per_layer_inputs": per_layer_inputs, - } - ) + tensors: dict[str, torch.Tensor] = { + "hidden_states": hidden_states, + } + if per_layer_inputs is not None: + tensors["per_layer_inputs"] = per_layer_inputs + return IntermediateTensors(tensors) # Gemma4 incorporates residual into hidden_states directly # Apply norm without residual fusion when possible. if residual is None: From 37e288214bc3fa89d974b4d323373f2b2878d604 Mon Sep 17 00:00:00 2001 From: Ronen Schaffer Date: Wed, 29 Apr 2026 13:50:42 +0300 Subject: [PATCH 042/237] [KV Offload] Tighten `keys` type from `Iterable` to `Sequence` in `OffloadingManager` (#41200) Signed-off-by: Ronen Schaffer --- vllm/v1/kv_offload/abstract.py | 8 ++++---- vllm/v1/kv_offload/cpu/manager.py | 14 ++++++-------- vllm/v1/kv_offload/reuse_manager.py | 9 ++++----- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/vllm/v1/kv_offload/abstract.py b/vllm/v1/kv_offload/abstract.py index 8f809ceaa08a..be54611356b1 100644 --- a/vllm/v1/kv_offload/abstract.py +++ b/vllm/v1/kv_offload/abstract.py @@ -27,7 +27,7 @@ """ from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from dataclasses import dataclass from typing import Any, NewType @@ -109,7 +109,7 @@ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None: @abstractmethod def prepare_load( self, - keys: Iterable[OffloadKey], + keys: Sequence[OffloadKey], req_context: ReqContext, ) -> LoadStoreSpec: """ @@ -128,7 +128,7 @@ def prepare_load( """ pass - def touch(self, keys: Iterable[OffloadKey]): + def touch(self, keys: Sequence[OffloadKey]): """ Mark the given blocks as recently used. This could in practice mean moving them to the end of an LRU list. @@ -150,7 +150,7 @@ def complete_load(self, keys: Iterable[OffloadKey]): @abstractmethod def prepare_store( self, - keys: Iterable[OffloadKey], + keys: Sequence[OffloadKey], req_context: ReqContext, ) -> PrepareStoreOutput | None: """ diff --git a/vllm/v1/kv_offload/cpu/manager.py b/vllm/v1/kv_offload/cpu/manager.py index fcfaa919a3b3..fb03846105db 100644 --- a/vllm/v1/kv_offload/cpu/manager.py +++ b/vllm/v1/kv_offload/cpu/manager.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from typing import Literal from vllm.v1.kv_offload.abstract import ( @@ -90,7 +90,7 @@ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None: def prepare_load( self, - keys: Iterable[OffloadKey], + keys: Sequence[OffloadKey], req_context: ReqContext, ) -> LoadStoreSpec: blocks = [] @@ -102,7 +102,7 @@ def prepare_load( blocks.append(block) return self._get_load_store_spec(keys, blocks) - def touch(self, keys: Iterable[OffloadKey]) -> None: + def touch(self, keys: Sequence[OffloadKey]) -> None: self._policy.touch(keys) def complete_load(self, keys: Iterable[OffloadKey]) -> None: @@ -114,13 +114,11 @@ def complete_load(self, keys: Iterable[OffloadKey]) -> None: def prepare_store( self, - keys: Iterable[OffloadKey], + keys: Sequence[OffloadKey], req_context: ReqContext, ) -> PrepareStoreOutput | None: - keys_list = list(keys) - # filter out blocks that are already stored - keys_to_store = [k for k in keys_list if self._policy.get(k) is None] + keys_to_store = [k for k in keys if self._policy.get(k) is None] if not keys_to_store: return PrepareStoreOutput( @@ -135,7 +133,7 @@ def prepare_store( if num_blocks_to_evict > 0: # Blocks from the original input are excluded from eviction candidates: # a block that was already stored must remain in the cache after this call. - protected = set(keys_list) + protected = set(keys) evicted = self._policy.evict(num_blocks_to_evict, protected) if evicted is None: return None diff --git a/vllm/v1/kv_offload/reuse_manager.py b/vllm/v1/kv_offload/reuse_manager.py index 96b8f969e758..751f3a93df8e 100644 --- a/vllm/v1/kv_offload/reuse_manager.py +++ b/vllm/v1/kv_offload/reuse_manager.py @@ -8,7 +8,7 @@ """ from collections import OrderedDict -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from vllm.v1.kv_offload.abstract import ( LoadStoreSpec, @@ -79,7 +79,7 @@ def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None: return self._backing.lookup(key, req_context) def prepare_store( - self, keys: Iterable[OffloadKey], req_context: ReqContext + self, keys: Sequence[OffloadKey], req_context: ReqContext ) -> PrepareStoreOutput | None: """Filter out blocks below threshold, then delegate to backing. @@ -87,7 +87,6 @@ def prepare_store( ``prepare_store`` so that blocks that would be skipped do not consume any CPU offload capacity. """ - keys = list(keys) eligible = [ key for key in keys if self.counts.get(key, 0) >= self.store_threshold ] @@ -102,11 +101,11 @@ def prepare_store( # ------------------------------------------------------------------ def prepare_load( - self, keys: Iterable[OffloadKey], req_context: ReqContext + self, keys: Sequence[OffloadKey], req_context: ReqContext ) -> LoadStoreSpec: return self._backing.prepare_load(keys, req_context) - def touch(self, keys: Iterable[OffloadKey]) -> None: + def touch(self, keys: Sequence[OffloadKey]) -> None: return self._backing.touch(keys) def complete_load(self, keys: Iterable[OffloadKey]) -> None: From 33f36d42605a476a09ed75936e7c931cb8b432c5 Mon Sep 17 00:00:00 2001 From: Bugen Zhao Date: Wed, 29 Apr 2026 19:03:47 +0800 Subject: [PATCH 043/237] [DSV4] Support `max` reasoning effort (#40982) Signed-off-by: Bugen Zhao --- .../openai/chat_completion/test_chat.py | 25 +++++++ .../openai/parser/test_harmony_utils.py | 7 ++ tests/tokenizers_/test_deepseek_v4.py | 67 ++++++++++++++++++- .../openai/chat_completion/protocol.py | 14 +++- .../openai/parser/harmony_utils.py | 9 ++- vllm/tokenizers/deepseek_v4.py | 10 ++- 6 files changed, 126 insertions(+), 6 deletions(-) diff --git a/tests/entrypoints/openai/chat_completion/test_chat.py b/tests/entrypoints/openai/chat_completion/test_chat.py index 80f54f6800ae..6703095aec4a 100644 --- a/tests/entrypoints/openai/chat_completion/test_chat.py +++ b/tests/entrypoints/openai/chat_completion/test_chat.py @@ -1002,6 +1002,31 @@ def test_chat_completion_request_n_parameter_default(): assert sampling_params.n == 1, f"Expected n=1 (default), got n={sampling_params.n}" +def test_chat_completion_request_accepts_model_specific_reasoning_effort(): + request = ChatCompletionRequest( + model="test-model", + messages=[{"role": "user", "content": "Hello"}], + reasoning_effort="max", + ) + + chat_params = request.build_chat_params( + default_template=None, + default_template_content_format="auto", + ) + + assert request.reasoning_effort == "max" + assert chat_params.chat_template_kwargs["reasoning_effort"] == "max" + + +def test_chat_completion_request_rejects_unknown_reasoning_effort(): + with pytest.raises(ValueError, match="Input should be"): + ChatCompletionRequest( + model="test-model", + messages=[{"role": "user", "content": "Hello"}], + reasoning_effort="extra_high", + ) + + def test_chat_completion_request_n_parameter_various_values(): """Test n parameter with various values.""" for n_value in [1, 2, 5, 10]: diff --git a/tests/entrypoints/openai/parser/test_harmony_utils.py b/tests/entrypoints/openai/parser/test_harmony_utils.py index 21b53dff1507..69b9f101ea28 100644 --- a/tests/entrypoints/openai/parser/test_harmony_utils.py +++ b/tests/entrypoints/openai/parser/test_harmony_utils.py @@ -843,6 +843,13 @@ def test_all_standard_channels_present(self) -> None: f"{channel} missing when with_custom_tools={with_tools}" ) + def test_unsupported_reasoning_effort_raises_clear_error(self) -> None: + with pytest.raises( + ValueError, + match="reasoning_effort='max' is not supported by Harmony", + ): + get_system_message(reasoning_effort="max") + class TestResponseInputToHarmonyReasoningItem: """Tests for response_input_to_harmony handling of reasoning input items. diff --git a/tests/tokenizers_/test_deepseek_v4.py b/tests/tokenizers_/test_deepseek_v4.py index 9f3b88cf658d..f8099a546342 100644 --- a/tests/tokenizers_/test_deepseek_v4.py +++ b/tests/tokenizers_/test_deepseek_v4.py @@ -182,7 +182,7 @@ def test_deepseek_v4_renders_parsed_history_tool_arguments(): assert 'parameter name="arguments"' not in prompt -@pytest.mark.parametrize("reasoning_effort", ["none", "low", "medium", "high"]) +@pytest.mark.parametrize("reasoning_effort", ["minimal", "low", "medium", "high"]) def test_deepseek_v4_accepts_openai_reasoning_effort_values(reasoning_effort): prompt = _tokenizer().apply_chat_template( [{"role": "user", "content": "Hello"}], @@ -195,6 +195,58 @@ def test_deepseek_v4_accepts_openai_reasoning_effort_values(reasoning_effort): assert "Reasoning Effort: Absolute maximum" not in prompt +def test_deepseek_v4_none_reasoning_effort_disables_thinking(): + prompt = _tokenizer().apply_chat_template( + [{"role": "user", "content": "Hello"}], + tokenize=False, + enable_thinking=True, + reasoning_effort="none", + ) + + assert prompt == ("<|begin▁of▁sentence|><|User|>Hello<|Assistant|>") + + +@pytest.mark.parametrize( + ("reasoning_effort", "expected_mode", "expected_effort"), + [ + ("none", "chat", None), + ("minimal", "thinking", "high"), + ("low", "thinking", "high"), + ("medium", "thinking", "high"), + ("high", "thinking", "high"), + ("xhigh", "thinking", "max"), + ("max", "thinking", "max"), + ("unexpected", "thinking", "high"), + ], +) +def test_deepseek_v4_maps_compatible_thinking_reasoning_effort_values( + monkeypatch: pytest.MonkeyPatch, + reasoning_effort, + expected_mode, + expected_effort, +): + captured_kwargs = [] + + def fake_encode_messages(messages, **kwargs): + captured_kwargs.append(kwargs) + return "prompt" + + monkeypatch.setattr( + "vllm.tokenizers.deepseek_v4.encode_messages", + fake_encode_messages, + ) + + _tokenizer().apply_chat_template( + [{"role": "user", "content": "Hello"}], + tokenize=False, + enable_thinking=True, + reasoning_effort=reasoning_effort, + ) + + assert captured_kwargs[-1]["thinking_mode"] == expected_mode + assert captured_kwargs[-1]["reasoning_effort"] == expected_effort + + def test_deepseek_v4_preserves_reference_max_reasoning_effort(): prompt = _tokenizer().apply_chat_template( [{"role": "user", "content": "Hello"}], @@ -208,6 +260,19 @@ def test_deepseek_v4_preserves_reference_max_reasoning_effort(): ) +def test_deepseek_v4_maps_xhigh_to_reference_max_reasoning_effort(): + prompt = _tokenizer().apply_chat_template( + [{"role": "user", "content": "Hello"}], + tokenize=False, + enable_thinking=True, + reasoning_effort="xhigh", + ) + + assert prompt.startswith( + "<|begin▁of▁sentence|>Reasoning Effort: Absolute maximum" + ) + + @pytest.mark.parametrize( ("case_id", "kwargs"), [ diff --git a/vllm/entrypoints/openai/chat_completion/protocol.py b/vllm/entrypoints/openai/chat_completion/protocol.py index 01d2df88d69b..9f3619923540 100644 --- a/vllm/entrypoints/openai/chat_completion/protocol.py +++ b/vllm/entrypoints/openai/chat_completion/protocol.py @@ -182,7 +182,19 @@ class ChatCompletionRequest(OpenAIBaseModel): | ChatCompletionNamedToolChoiceParam | None ) = "none" - reasoning_effort: Literal["none", "low", "medium", "high"] | None = None + reasoning_effort: ( + Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"] | None + ) = Field( + default=None, + description=( + "Constrains effort on reasoning for reasoning models. " + "Currently supported values are none, minimal, low, medium, " + "high, xhigh, and max. Reducing reasoning effort can result in " + "faster responses and fewer tokens used on reasoning in a response. " + "Note that 'max' is specific to the DeepSeek V4 series and is not " + "part of the standard OpenAI API specification." + ), + ) thinking_token_budget: int | None = None include_reasoning: bool = True parallel_tool_calls: bool | None = True diff --git a/vllm/entrypoints/openai/parser/harmony_utils.py b/vllm/entrypoints/openai/parser/harmony_utils.py index 7550a02867a6..33e68b4c40e8 100644 --- a/vllm/entrypoints/openai/parser/harmony_utils.py +++ b/vllm/entrypoints/openai/parser/harmony_utils.py @@ -3,7 +3,6 @@ import datetime from collections.abc import Iterable, Sequence -from typing import Literal from openai.types.responses.tool import Tool from openai_harmony import ( @@ -66,7 +65,7 @@ def get_encoding(): def get_system_message( model_identity: str | None = None, - reasoning_effort: Literal["high", "medium", "low"] | None = None, + reasoning_effort: str | None = None, start_date: str | None = None, browser_description: str | None = None, python_description: str | None = None, @@ -84,6 +83,12 @@ def get_system_message( ) sys_msg_content = sys_msg_content.with_model_identity(new_identity) if reasoning_effort is not None: + if reasoning_effort not in REASONING_EFFORT: + supported_values = ", ".join(REASONING_EFFORT) + raise ValueError( + f"reasoning_effort={reasoning_effort!r} is not supported by " + f"Harmony. Supported values are: {supported_values}." + ) sys_msg_content = sys_msg_content.with_reasoning_effort( REASONING_EFFORT[reasoning_effort] ) diff --git a/vllm/tokenizers/deepseek_v4.py b/vllm/tokenizers/deepseek_v4.py index 76725dab16a1..2a6aaaf73975 100644 --- a/vllm/tokenizers/deepseek_v4.py +++ b/vllm/tokenizers/deepseek_v4.py @@ -40,10 +40,16 @@ def apply_chat_template( messages.insert(0, {"role": "system"}) messages[0]["tools"] = tools # type: ignore[typeddict-unknown-key] - # The V4 reference currently accepts only "max", "high", or None. reasoning_effort = kwargs.get("reasoning_effort") - if reasoning_effort not in ("max", "high"): + if not isinstance(reasoning_effort, str): reasoning_effort = None + elif reasoning_effort == "none": + thinking_mode = "chat" + reasoning_effort = None + elif reasoning_effort in ("max", "xhigh"): + reasoning_effort = "max" + else: + reasoning_effort = "high" encode_config = dict( thinking_mode=thinking_mode, From 11b69129e2221b64302fb672552c0bc04dddece5 Mon Sep 17 00:00:00 2001 From: Jared Wen Date: Wed, 29 Apr 2026 19:35:50 +0800 Subject: [PATCH 044/237] [Frontend] Add `defer_loading` and `tool_reference` support for Anthropic and OpenAI APIs (#40190) Signed-off-by: JaredforReal Signed-off-by: sfeng33 <4florafeng@gmail.com> Signed-off-by: chaunceyjiang Co-authored-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: Chauncey --- vllm/entrypoints/anthropic/protocol.py | 4 ++ vllm/entrypoints/anthropic/serving.py | 21 ++++++++ vllm/entrypoints/chat_utils.py | 53 +++++++++++++++++-- .../openai/chat_completion/protocol.py | 16 +++++- vllm/entrypoints/openai/engine/protocol.py | 9 ++++ 5 files changed, 97 insertions(+), 6 deletions(-) diff --git a/vllm/entrypoints/anthropic/protocol.py b/vllm/entrypoints/anthropic/protocol.py index 52eb77b51167..f3c4dd7f3e32 100644 --- a/vllm/entrypoints/anthropic/protocol.py +++ b/vllm/entrypoints/anthropic/protocol.py @@ -39,6 +39,7 @@ class AnthropicContentBlock(BaseModel): "image", "tool_use", "tool_result", + "tool_reference", "thinking", "redacted_thinking", ] @@ -52,6 +53,8 @@ class AnthropicContentBlock(BaseModel): input: dict[str, Any] | None = None content: str | list[dict[str, Any]] | None = None is_error: bool | None = None + # For tool_reference content + tool_name: str | None = None # For thinking content thinking: str | None = None signature: str | None = None @@ -72,6 +75,7 @@ class AnthropicTool(BaseModel): name: str description: str | None = None input_schema: dict[str, Any] + defer_loading: bool | None = None @field_validator("input_schema") @classmethod diff --git a/vllm/entrypoints/anthropic/serving.py b/vllm/entrypoints/anthropic/serving.py index 939f5a7ed4c5..867ee73948ff 100644 --- a/vllm/entrypoints/anthropic/serving.py +++ b/vllm/entrypoints/anthropic/serving.py @@ -237,6 +237,10 @@ def _convert_block( cls._convert_tool_use_block(block, tool_calls) elif block.type == "tool_result": cls._convert_tool_result_block(block, role, openai_messages, content_parts) + elif block.type == "tool_reference": + # Tool references are expanded during tool_result processing + # when they appear inside tool_result content. + pass @classmethod def _convert_tool_use_block(cls, block, tool_calls: list[dict[str, Any]]) -> None: @@ -275,6 +279,7 @@ def _convert_user_tool_result( """Convert user tool_result with text and image support""" tool_text = "" tool_image_urls: list[str] = [] + tool_reference: list[dict[str, Any]] = [] if isinstance(block.content, str): tool_text = block.content @@ -291,6 +296,12 @@ def _convert_user_tool_result( url = cls._convert_image_source_to_url(source) if url: tool_image_urls.append(url) + elif item_type == "tool_reference": + ref_name = item.get("tool_name") or item.get("name") + if ref_name: + tool_reference.append( + {"type": "tool_reference", "name": ref_name} + ) tool_text = "\n".join(text_parts) openai_messages.append( @@ -312,6 +323,15 @@ def _convert_user_tool_result( } ) + if tool_reference: + openai_messages.append( + { + "role": "tool", + "tool_call_id": block.tool_use_id or "", + "content": tool_reference, # type: ignore[dict-item] + } + ) + @classmethod def _build_base_request( cls, @@ -400,6 +420,7 @@ def _convert_tools( "name": tool.name, "description": tool.description, "parameters": tool.input_schema, + "defer_loading": tool.defer_loading, }, } ) diff --git a/vllm/entrypoints/chat_utils.py b/vllm/entrypoints/chat_utils.py index 73a32033aa48..c6bf6f0e6e78 100644 --- a/vllm/entrypoints/chat_utils.py +++ b/vllm/entrypoints/chat_utils.py @@ -255,6 +255,23 @@ class CustomThinkCompletionContentParam(TypedDict, total=False): """The thinking type.""" +class CustomChatCompletionContentToolReferenceParam(TypedDict, total=False): + """A tool reference content param that only accepts a plain tool name. + + Example: + { + "name": "get_weather", + "type": "tool_reference" + } + """ + + name: str + """The name of the tool being referenced.""" + + type: Literal["tool_reference"] + """The content type.""" + + ChatCompletionContentPartParam: TypeAlias = ( OpenAIChatCompletionContentPartParam | ChatCompletionContentPartAudioParam @@ -267,6 +284,7 @@ class CustomThinkCompletionContentParam(TypedDict, total=False): | ChatCompletionContentPartAudioEmbedsParam | CustomChatCompletionContentSimpleAudioParam | CustomChatCompletionContentSimpleVideoParam + | CustomChatCompletionContentToolReferenceParam | str | CustomThinkCompletionContentParam ) @@ -1281,6 +1299,9 @@ def _get_full_multimodal_text_prompt( "input_audio": lambda part: _InputAudioParser(part).get("input_audio", None), "refusal": lambda part: _RefusalParser(part).get("refusal", None), "video_url": lambda part: _VideoParser(part).get("video_url", {}).get("url", None), + "tool_reference": lambda part: cast( + CustomChatCompletionContentToolReferenceParam, part + ).get("name", None), } @@ -1372,6 +1393,12 @@ def _parse_chat_message_content_mm_part( # with url as a dict of {"url": url} video_url = video_url.get("url", None) return "video_url", video_url + if "tool_reference" in part: + tool_reference_params = cast( + CustomChatCompletionContentToolReferenceParam, part + ) + tool_reference = tool_reference_params.get("name", None) + return "tool_reference", tool_reference # Raise an error if no 'type' or direct URL is found. raise ValueError("Missing 'type' field in multimodal part.") @@ -1501,6 +1528,12 @@ def _parse_chat_message_content_part( str_content = cast(str, content) mm_parser.parse_video(str_content, uuid) modality = "video" + elif part_type == "tool_reference": + # Tool references are not multimodal data — they reference deferred + # tools and are passed through as-is for the chat template to expand. + if wrap_dicts: + return {"type": "tool_reference", "name": cast(str, content)} + return cast(str, content) else: supported = sorted(MM_PARSER_MAP.keys() | set(PART_TYPES_TO_SKIP_NONE_CONTENT)) raise VLLMValidationError( @@ -1567,14 +1600,24 @@ def _parse_chat_message_content( # string. Clients like Claude Code / Cursor send tool results as # [{"type": "text", "text": "..."}], but most chat templates only # handle string content for tool messages. + # However, tool_reference items must be preserved as structured + # dicts for the chat template to expand them. msg_content = result_msg.get("content") if isinstance(msg_content, list): - texts = [ - item.get("text", "") + has_non_text = any( + isinstance(item, dict) and item.get("type") != "text" for item in msg_content - if isinstance(item, dict) and item.get("type") == "text" - ] - result_msg["content"] = "\n".join(texts) if texts else "" + ) + if has_non_text: + # Keep structured content (e.g., tool_reference) + result_msg["content"] = msg_content + else: + texts = [ + item.get("text", "") + for item in msg_content + if isinstance(item, dict) and item.get("type") == "text" + ] + result_msg["content"] = "\n".join(texts) if texts else "" if "name" in message and isinstance(message["name"], str): result_msg["name"] = message["name"] diff --git a/vllm/entrypoints/openai/chat_completion/protocol.py b/vllm/entrypoints/openai/chat_completion/protocol.py index 9f3619923540..c92cc13da01f 100644 --- a/vllm/entrypoints/openai/chat_completion/protocol.py +++ b/vllm/entrypoints/openai/chat_completion/protocol.py @@ -11,7 +11,7 @@ ChatCompletionAudio as OpenAIChatCompletionAudio, ) from openai.types.chat.chat_completion_message import Annotation as OpenAIAnnotation -from pydantic import Field, PrivateAttr, model_validator +from pydantic import Field, PrivateAttr, model_serializer, model_validator from vllm.config import ModelConfig from vllm.config.utils import replace @@ -139,6 +139,20 @@ class ChatCompletionStreamResponse(OpenAIBaseModel): class ChatCompletionToolsParam(OpenAIBaseModel): type: Literal["function"] = "function" function: FunctionDefinition + defer_loading: bool | None = None + + @model_validator(mode="after") + def _propagate_defer_loading(self) -> "ChatCompletionToolsParam": + if self.defer_loading is not None and self.function.defer_loading is None: + self.function.defer_loading = self.defer_loading + return self + + @model_serializer(mode="wrap") + def _serialize(self, handler): + data = handler(self) + if self.defer_loading is None: + data.pop("defer_loading", None) + return data class ChatCompletionNamedFunction(OpenAIBaseModel): diff --git a/vllm/entrypoints/openai/engine/protocol.py b/vllm/entrypoints/openai/engine/protocol.py index 8f6cdb3e6241..890af0300efc 100644 --- a/vllm/entrypoints/openai/engine/protocol.py +++ b/vllm/entrypoints/openai/engine/protocol.py @@ -12,6 +12,7 @@ BaseModel, ConfigDict, Field, + model_serializer, model_validator, ) @@ -166,6 +167,14 @@ class FunctionDefinition(OpenAIBaseModel): name: str description: str | None = None parameters: dict[str, Any] | None = None + defer_loading: bool | None = None + + @model_serializer(mode="wrap") + def _serialize(self, handler): + data = handler(self) + if self.defer_loading is None: + data.pop("defer_loading", None) + return data # extra="forbid" is a workaround to have kwargs as a field, From 9d8ad5b408bf447e41a3629fc21a453720aaf52b Mon Sep 17 00:00:00 2001 From: Jee Jee Li Date: Wed, 29 Apr 2026 20:29:55 +0800 Subject: [PATCH 045/237] [Bugfix] Fix repeated DSv4 RoPE cache initialization (#41148) Signed-off-by: Jee Jee Li Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../rotary_embedding/deepseek_scaling_rope.py | 13 +++++++++++-- vllm/model_executor/models/deepseek_v4.py | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/vllm/model_executor/layers/rotary_embedding/deepseek_scaling_rope.py b/vllm/model_executor/layers/rotary_embedding/deepseek_scaling_rope.py index 6cb9101a78b1..9a06eedd0f7d 100644 --- a/vllm/model_executor/layers/rotary_embedding/deepseek_scaling_rope.py +++ b/vllm/model_executor/layers/rotary_embedding/deepseek_scaling_rope.py @@ -45,6 +45,7 @@ def __init__( beta_slow: int = 1, mscale: float = 1, mscale_all_dim: float = 0, + init_cache: bool = True, ) -> None: self.scaling_factor = scaling_factor self.extrapolation_factor = extrapolation_factor @@ -65,7 +66,13 @@ def __init__( and head_size in [64, 128, 256, 512] ) super().__init__( - head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype + head_size, + rotary_dim, + max_position_embeddings, + base, + is_neox_style, + dtype, + init_cache=init_cache, ) def _compute_inv_freq(self, scaling_factor: float) -> torch.Tensor: @@ -211,7 +218,9 @@ class DeepseekV4ScalingRotaryEmbedding(DeepseekScalingRotaryEmbedding): """ def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + # Avoid compute cache repeatedly + kwargs.pop("init_cache", None) + super().__init__(*args, **kwargs, init_cache=False) cache_fp32 = self._compute_cos_sin_cache() self.register_buffer("cos_sin_cache", cache_fp32, persistent=False) diff --git a/vllm/model_executor/models/deepseek_v4.py b/vllm/model_executor/models/deepseek_v4.py index d41a8b666d33..baf28d04581a 100644 --- a/vllm/model_executor/models/deepseek_v4.py +++ b/vllm/model_executor/models/deepseek_v4.py @@ -1027,7 +1027,6 @@ def __init__( max_position=self.max_position_embeddings, rope_parameters=rope_parameters, is_neox_style=False, - dtype=config.torch_dtype, ) self.indexer = None From 22524f7a92b71c8e65eade20ef274fa3b4006d3e Mon Sep 17 00:00:00 2001 From: Tianmu Li Date: Wed, 29 Apr 2026 05:43:21 -0700 Subject: [PATCH 046/237] [Feat] CPU fp8 attn for AMX/AVX-512 (#39445) Signed-off-by: Li, Tianmu Co-authored-by: Claude Co-authored-by: Li, Jiang --- csrc/cpu/cpu_attn.cpp | 103 ++++++--- csrc/cpu/cpu_attn_amx.hpp | 217 ++++++++++++++---- csrc/cpu/cpu_attn_fp8.hpp | 214 ++++++++++++++++++ csrc/cpu/cpu_attn_impl.hpp | 38 +++- csrc/cpu/cpu_attn_neon.hpp | 9 +- csrc/cpu/cpu_attn_neon_bfmmla.hpp | 3 +- csrc/cpu/cpu_attn_vec.hpp | 133 ++++++++--- csrc/cpu/cpu_attn_vec16.hpp | 6 +- csrc/cpu/cpu_attn_vxe.hpp | 7 +- csrc/cpu/cpu_types_arm.hpp | 6 + csrc/cpu/cpu_types_vxe.hpp | 6 + csrc/cpu/cpu_types_x86.hpp | 139 ++++++++++++ csrc/cpu/generate_cpu_attn_dispatch.py | 268 ++++++++++++----------- csrc/cpu/torch_bindings.cpp | 16 +- docs/design/attention_backends.md | 2 +- tests/kernels/attention/test_cpu_attn.py | 111 ++++++++-- vllm/_custom_ops.py | 12 + vllm/platforms/cpu.py | 19 +- vllm/v1/attention/backends/cpu_attn.py | 43 +++- 19 files changed, 1068 insertions(+), 284 deletions(-) create mode 100644 csrc/cpu/cpu_attn_fp8.hpp diff --git a/csrc/cpu/cpu_attn.cpp b/csrc/cpu/cpu_attn.cpp index a582b4b4d7cc..18afe4b7925c 100644 --- a/csrc/cpu/cpu_attn.cpp +++ b/csrc/cpu/cpu_attn.cpp @@ -1,5 +1,16 @@ #include "cpu_attn_dispatch_generated.h" +// Maps kv_cache_dtype string to Fp8KVCacheDataType enum. +// "auto" -> kAuto(0); "fp8"/"fp8_e4m3" -> kFp8E4M3; "fp8_e5m2" -> kFp8E5M2. +static inline cpu_attention::Fp8KVCacheDataType parse_fp8_kv_dtype( + const std::string& kv_cache_dtype) { + if (kv_cache_dtype == "fp8_e5m2") + return cpu_attention::Fp8KVCacheDataType::kFp8E5M2; + if (kv_cache_dtype == "fp8_e4m3" || kv_cache_dtype == "fp8") + return cpu_attention::Fp8KVCacheDataType::kFp8E4M3; + return cpu_attention::Fp8KVCacheDataType::kAuto; +} + torch::Tensor get_scheduler_metadata( const int64_t num_req, const int64_t num_heads_q, const int64_t num_heads_kv, const int64_t head_dim, @@ -49,7 +60,7 @@ torch::Tensor get_scheduler_metadata( input.enable_kv_split = enable_kv_split; VLLM_DISPATCH_FLOATING_TYPES(dtype, "get_scheduler_metadata", [&]() { - CPU_ATTN_DISPATCH(head_dim, isa, [&]() { + CPU_ATTN_DISPATCH(head_dim, isa, 0, [&]() { input.elem_size = sizeof(scalar_t); input.q_buffer_elem_size = sizeof(attn_impl::q_buffer_t); input.logits_buffer_elem_size = sizeof(attn_impl::logits_buffer_t); @@ -72,7 +83,9 @@ void cpu_attn_reshape_and_cache( key_cache, // [num_blocks, num_kv_heads, block_size, head_size] torch::Tensor& value_cache, // [num_blocks, num_kv_heads, block_size, head_size] - const torch::Tensor& slot_mapping, const std::string& isa) { + const torch::Tensor& slot_mapping, const std::string& isa, + const double k_scale = 1.0, const double v_scale = 1.0, + const std::string& kv_cache_dtype = "auto") { TORCH_CHECK_EQ(key.dim(), 3); TORCH_CHECK_EQ(value.dim(), 3); TORCH_CHECK_EQ(key_cache.dim(), 4); @@ -80,18 +93,30 @@ void cpu_attn_reshape_and_cache( TORCH_CHECK_EQ(key.stride(2), 1); TORCH_CHECK_EQ(value.stride(2), 1); + const int64_t kv_cache_idx = + static_cast(parse_fp8_kv_dtype(kv_cache_dtype)); + const bool is_fp8 = (kv_cache_idx != 0); + + if (is_fp8) { + TORCH_CHECK(key_cache.scalar_type() == at::ScalarType::Byte, + "key_cache must be uint8 for FP8 path"); + TORCH_CHECK(value_cache.scalar_type() == at::ScalarType::Byte, + "value_cache must be uint8 for FP8 path"); + TORCH_CHECK(k_scale > 0, "k_scale must be positive for FP8 path"); + TORCH_CHECK(v_scale > 0, "v_scale must be positive for FP8 path"); + } + + const float k_inv = is_fp8 ? 1.0f / static_cast(k_scale) : 0.0f; + const float v_inv = is_fp8 ? 1.0f / static_cast(v_scale) : 0.0f; + const int64_t token_num = key.size(0); - const int64_t key_token_num_stride = key.stride(0); - const int64_t value_token_num_stride = value.stride(0); - const int64_t head_num = value.size(1); - const int64_t key_head_num_stride = key.stride(1); - const int64_t value_head_num_stride = value.stride(1); + const int64_t head_num = key.size(1); + const int64_t head_dim = key.size(2); const int64_t num_blocks = key_cache.size(0); const int64_t num_blocks_stride = key_cache.stride(0); const int64_t cache_head_num_stride = key_cache.stride(1); const int64_t block_size = key_cache.size(2); const int64_t block_size_stride = key_cache.stride(2); - const int64_t head_dim = key.size(-1); cpu_attention::ISA isa_tag = [&]() { if (isa == "amx") { @@ -109,16 +134,24 @@ void cpu_attn_reshape_and_cache( } }(); + if (is_fp8) { + TORCH_CHECK(isa_tag == cpu_attention::ISA::AMX || + isa_tag == cpu_attention::ISA::VEC, + "FP8 KV cache is only supported on x86 (AMX/VEC) ISA"); + } + VLLM_DISPATCH_FLOATING_TYPES( key.scalar_type(), "cpu_attn_reshape_and_cache", [&]() { - CPU_ATTN_DISPATCH(head_dim, isa_tag, [&]() { + CPU_ATTN_DISPATCH(head_dim, isa_tag, kv_cache_idx, [&]() { + using kv_t = typename attn_impl::kv_cache_t; attn_impl::reshape_and_cache( key.data_ptr(), value.data_ptr(), - key_cache.data_ptr(), value_cache.data_ptr(), - slot_mapping.data_ptr(), token_num, key_token_num_stride, - value_token_num_stride, head_num, key_head_num_stride, - value_head_num_stride, num_blocks, num_blocks_stride, - cache_head_num_stride, block_size, block_size_stride); + reinterpret_cast(key_cache.data_ptr()), + reinterpret_cast(value_cache.data_ptr()), + slot_mapping.data_ptr(), token_num, key.stride(0), + value.stride(0), head_num, key.stride(1), value.stride(1), + num_blocks, num_blocks_stride, cache_head_num_stride, block_size, + block_size_stride, k_inv, v_inv); }); }); } @@ -137,13 +170,26 @@ void cpu_attention_with_kv_cache( const int64_t sliding_window_left, const int64_t sliding_window_right, const torch::Tensor& block_table, // [num_tokens, max_block_num] const double softcap, const torch::Tensor& scheduler_metadata, - const std::optional& s_aux // [num_heads] -) { + const std::optional& s_aux, // [num_heads] + const double k_scale = 1.0, const double v_scale = 1.0, + const std::string& kv_cache_dtype = "auto") { TORCH_CHECK_EQ(query.dim(), 3); TORCH_CHECK_EQ(query.stride(2), 1); TORCH_CHECK_EQ(key_cache.dim(), 4); TORCH_CHECK_EQ(value_cache.dim(), 4); + const int64_t kv_cache_idx = + static_cast(parse_fp8_kv_dtype(kv_cache_dtype)); + const bool is_fp8 = (kv_cache_idx != 0); + if (is_fp8) { + TORCH_CHECK(key_cache.scalar_type() == at::ScalarType::Byte, + "key_cache must be uint8 for FP8 path"); + TORCH_CHECK(value_cache.scalar_type() == at::ScalarType::Byte, + "value_cache must be uint8 for FP8 path"); + TORCH_CHECK(k_scale > 0, "k_scale must be positive for FP8 path"); + TORCH_CHECK(v_scale > 0, "v_scale must be positive for FP8 path"); + } + cpu_attention::AttentionInput input; input.metadata = reinterpret_cast( scheduler_metadata.data_ptr()); @@ -165,25 +211,32 @@ void cpu_attention_with_kv_cache( input.block_table = block_table.data_ptr(); input.alibi_slopes = alibi_slopes.has_value() ? alibi_slopes->data_ptr() : nullptr; - // For now sink must be bf16 input.s_aux = s_aux.has_value() ? s_aux->data_ptr() : nullptr; input.scale = scale; input.causal = causal; input.sliding_window_left = sliding_window_left; input.sliding_window_right = sliding_window_right; if (input.causal) { - // to make boundary calculation easier input.sliding_window_right = 0; } - float softcap_fp32 = softcap; - input.softcap = softcap_fp32; + input.softcap = static_cast(softcap); + + if (is_fp8) { + input.k_scale_fp8 = static_cast(k_scale); + input.v_scale_fp8 = static_cast(v_scale); + TORCH_CHECK(input.metadata->isa == cpu_attention::ISA::AMX || + input.metadata->isa == cpu_attention::ISA::VEC, + "FP8 KV cache is only supported on x86 (AMX/VEC) ISA"); + } VLLM_DISPATCH_FLOATING_TYPES( query.scalar_type(), "cpu_attention_with_kv_cache", [&]() { - CPU_ATTN_DISPATCH(query.size(2), input.metadata->isa, [&]() { - TORCH_CHECK_EQ(input.block_size % attn_impl::BlockSizeAlignment, 0); - cpu_attention::AttentionMainLoop mainloop; - mainloop(&input); - }); + CPU_ATTN_DISPATCH( + query.size(2), input.metadata->isa, kv_cache_idx, [&]() { + TORCH_CHECK_EQ(input.block_size % attn_impl::BlockSizeAlignment, + 0); + cpu_attention::AttentionMainLoop mainloop; + mainloop(&input); + }); }); } diff --git a/csrc/cpu/cpu_attn_amx.hpp b/csrc/cpu/cpu_attn_amx.hpp index 1c8644d52329..6a0341085dce 100644 --- a/csrc/cpu/cpu_attn_amx.hpp +++ b/csrc/cpu/cpu_attn_amx.hpp @@ -1,6 +1,7 @@ #ifndef CPU_ATTN_AMX_HPP #define CPU_ATTN_AMX_HPP +#include "cpu_attn_fp8.hpp" #include "cpu_attn_impl.hpp" namespace cpu_attention { @@ -21,9 +22,10 @@ typedef struct __tile_config { // 2-2-4 pattern, for 16 < m <= 32 // TILE 0, 1: load A matrix, row num should be 16, m - 16 // TILE 2, 3: load B matrix, row num should be 16 -// TILE 4, 5, 6, 7: store results C matrix, row num should be 16, 16, m - 16, m -// - 16 -template +// TILE 4, 5, 6, 7: store results C matrix, row num should be 16, 16, +// m - 16, m - 16 +// q_buffer_t: A (Q/P) tile type; kv_cache_t: B (K/V cache) tile type. +template class TileGemm224 { public: template @@ -42,13 +44,56 @@ class TileGemm224 { } }; -template <> -class TileGemm224 { +// Dequantize one FP8 tile (AMX_TILE_ROW_NUM rows x 32 cols) to BF16. +template +FORCE_INLINE void deq_tile_amx(const uint8_t* src, c10::BFloat16* dst) { + for (int r = 0; r < AMX_TILE_ROW_NUM; ++r) { + if constexpr (std::is_same_v) { + vec_op::BF16Vec32(src + r * 32, vec_op::fp8_bf16_e4m3_tag{}) + .save(dst + r * 32); + } else { + vec_op::BF16Vec32(src + r * 32, vec_op::fp8_bf16_e5m2_tag{}) + .save(dst + r * 32); + } + } +} + +// For FP8: dequant src into scratch and return scratch. +// For BF16: return src directly (scratch is unused; the compiler elides it). +template +FORCE_INLINE const c10::BFloat16* prepare_b_tile(const kv_cache_t* src, + c10::BFloat16* scratch) { + if constexpr (std::is_same_v || + std::is_same_v) { + deq_tile_amx(reinterpret_cast(src), scratch); + return scratch; + } else { + return reinterpret_cast(src); + } +} + +// Handles both BF16 and FP8 KV cache (2-2-4 pattern). +template +class TileGemm224 { + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v, + "kv_cache_t must be BFloat16, Float8_e4m3fn, or Float8_e5m2"); + + static constexpr bool fp8_kv = + std::is_same_v || + std::is_same_v; + + static constexpr int64_t tile_elems = AMX_TILE_BYTES / sizeof(c10::BFloat16); + // BF16 path: scratch_elems=1 so the scratch array is eliminated by the + // compiler. + static constexpr int64_t scratch_elems = fp8_kv ? tile_elems : 1; + public: template FORCE_INLINE static void gemm(const int32_t m_size, c10::BFloat16* __restrict__ a_tile, - c10::BFloat16* __restrict__ b_tile, + kv_cache_t* __restrict__ b_tile, float* __restrict__ c_tile, const int64_t lda, const int64_t ldb, const int64_t ldc, const int32_t block_size, @@ -56,6 +101,7 @@ class TileGemm224 { const bool accum_c) { const int32_t k_times = dynamic_k_size / (AMX_TILE_ROW_NUM * 4 / sizeof(c10::BFloat16)); + c10::BFloat16* __restrict__ a_tile_0 = a_tile; c10::BFloat16* __restrict__ a_tile_1 = a_tile + lda * AMX_TILE_ROW_NUM; const int64_t a_tile_stride = [&]() { @@ -70,8 +116,8 @@ class TileGemm224 { } }(); - c10::BFloat16* __restrict__ b_tile_2 = b_tile; - c10::BFloat16* __restrict__ b_tile_3 = [&]() { + kv_cache_t* __restrict__ b_tile_2 = b_tile; + kv_cache_t* __restrict__ b_tile_3 = [&]() { if constexpr (phase == AttentionGemmPhase::QK) { // k_cache is prepacked return b_tile + (k_size * AMX_TILE_ROW_BYTES / 4); @@ -106,11 +152,16 @@ class TileGemm224 { _tile_zero(7); } + alignas(64) c10::BFloat16 scratch_2[scratch_elems]; + alignas(64) c10::BFloat16 scratch_3[scratch_elems]; for (int32_t k = 0; k < k_times; ++k) { + const c10::BFloat16* load_2 = prepare_b_tile(b_tile_2, scratch_2); + const c10::BFloat16* load_3 = prepare_b_tile(b_tile_3, scratch_3); + _tile_loadd(0, a_tile_0, a_tile_stride); - _tile_stream_loadd(2, b_tile_2, b_tile_stride); + _tile_stream_loadd(2, const_cast(load_2), b_tile_stride); _tile_dpbf16ps(4, 0, 2); - _tile_stream_loadd(3, b_tile_3, b_tile_stride); + _tile_stream_loadd(3, const_cast(load_3), b_tile_stride); _tile_dpbf16ps(5, 0, 3); _tile_loadd(1, a_tile_1, a_tile_stride); _tile_dpbf16ps(6, 1, 2); @@ -154,13 +205,13 @@ class TileGemm224 { }; // 1-2-2 pattern, for 0 < m <= 16 -// TILE 0, (1): load A matrix, use extra 1 tile for prefetch, row num should be -// m, m -// TILE 2, 3, (4, 5): load B matrix, use extra 2 tiles for prefetch, row -// num should be 16 -// TILE 6, 7, (6, 7): store results C matrix, row num should be -// m -template +// TILE 0, (1): load A matrix, use extra 1 tile for prefetch, row num should +// be m, m +// TILE 2, 3, (4, 5): load B matrix, use extra 2 tiles for prefetch, row num +// should be 16 +// TILE 6, 7: store results C matrix, row num should be m +// q_buffer_t: A (Q/P) tile type; kv_cache_t: B (K/V cache) tile type. +template class TileGemm122 { public: template @@ -179,13 +230,26 @@ class TileGemm122 { } }; -template <> -class TileGemm122 { +// Handles both BF16 and FP8 KV cache (1-2-2 pattern). +template +class TileGemm122 { + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v, + "kv_cache_t must be BFloat16, Float8_e4m3fn, or Float8_e5m2"); + + static constexpr bool fp8_kv = + std::is_same_v || + std::is_same_v; + + static constexpr int64_t tile_elems = AMX_TILE_BYTES / sizeof(c10::BFloat16); + static constexpr int64_t scratch_elems = fp8_kv ? tile_elems : 1; + public: template FORCE_INLINE static void gemm(const int32_t m_size, c10::BFloat16* __restrict__ a_tile, - c10::BFloat16* __restrict__ b_tile, + kv_cache_t* __restrict__ b_tile, float* __restrict__ c_tile, const int64_t lda, const int64_t ldb, const int64_t ldc, const int32_t block_size, @@ -215,21 +279,19 @@ class TileGemm122 { } }(); - c10::BFloat16* __restrict__ b_tile_2 = b_tile; - c10::BFloat16* __restrict__ b_tile_3 = [&]() { + kv_cache_t* __restrict__ b_tile_2 = b_tile; + kv_cache_t* __restrict__ b_tile_3 = [&]() { if constexpr (phase == AttentionGemmPhase::QK) { - // k_cache is prepacked return b_tile + (k_size * AMX_TILE_ROW_BYTES / 4); } else if constexpr (phase == AttentionGemmPhase::PV) { - // v_cache is prepacked return b_tile + (block_size * AMX_TILE_ROW_BYTES / 4); } else { TORCH_CHECK(false, "Unreachable"); } }(); - c10::BFloat16* __restrict__ b_tile_4 = + kv_cache_t* __restrict__ b_tile_4 = b_tile_2 + AMX_TILE_BYTES / sizeof(c10::BFloat16); - c10::BFloat16* __restrict__ b_tile_5 = + kv_cache_t* __restrict__ b_tile_5 = b_tile_3 + AMX_TILE_BYTES / sizeof(c10::BFloat16); int64_t b_stride = AMX_TILE_ROW_BYTES; @@ -250,16 +312,25 @@ class TileGemm122 { _tile_zero(7); } + alignas(64) c10::BFloat16 scratch_2[scratch_elems]; + alignas(64) c10::BFloat16 scratch_3[scratch_elems]; + alignas(64) c10::BFloat16 scratch_4[scratch_elems]; + alignas(64) c10::BFloat16 scratch_5[scratch_elems]; for (int32_t k = 0; k < k_group_times; ++k) { + const c10::BFloat16* load_2 = prepare_b_tile(b_tile_2, scratch_2); + const c10::BFloat16* load_3 = prepare_b_tile(b_tile_3, scratch_3); + const c10::BFloat16* load_4 = prepare_b_tile(b_tile_4, scratch_4); + const c10::BFloat16* load_5 = prepare_b_tile(b_tile_5, scratch_5); + _tile_loadd(0, a_tile_0, a_tile_stride); - _tile_stream_loadd(2, b_tile_2, b_stride); + _tile_stream_loadd(2, const_cast(load_2), b_stride); _tile_dpbf16ps(6, 0, 2); - _tile_stream_loadd(3, b_tile_3, b_stride); + _tile_stream_loadd(3, const_cast(load_3), b_stride); _tile_dpbf16ps(7, 0, 3); _tile_loadd(1, a_tile_1, a_tile_stride); - _tile_stream_loadd(4, b_tile_4, b_stride); + _tile_stream_loadd(4, const_cast(load_4), b_stride); _tile_dpbf16ps(6, 1, 4); - _tile_stream_loadd(5, b_tile_5, b_stride); + _tile_stream_loadd(5, const_cast(load_5), b_stride); _tile_dpbf16ps(7, 1, 5); // update ptrs @@ -279,10 +350,13 @@ class TileGemm122 { } if (has_tail) { + const c10::BFloat16* load_2 = prepare_b_tile(b_tile_2, scratch_2); + const c10::BFloat16* load_3 = prepare_b_tile(b_tile_3, scratch_3); + _tile_loadd(0, a_tile_0, a_tile_stride); - _tile_stream_loadd(2, b_tile_2, b_stride); + _tile_stream_loadd(2, const_cast(load_2), b_stride); _tile_dpbf16ps(6, 0, 2); - _tile_stream_loadd(3, b_tile_3, b_stride); + _tile_stream_loadd(3, const_cast(load_3), b_stride); _tile_dpbf16ps(7, 0, 3); } @@ -302,21 +376,25 @@ class TileGemm122 { _tile_loadconfig(&config); } }; + } // namespace -template -class AttentionImpl { +template +class AttentionImpl { + static constexpr bool fp8_kv = + std::is_same_v || + std::is_same_v; + public: using query_t = scalar_t; using q_buffer_t = scalar_t; - using kv_cache_t = scalar_t; + using kv_cache_t = kv_cache_scalar_t; using logits_buffer_t = float; using partial_output_buffer_t = float; using prob_buffer_t = scalar_t; constexpr static int64_t BlockSizeAlignment = - AMX_TILE_ROW_BYTES / - sizeof(kv_cache_t); // KV token num unit of QK and PV phases + 32; // AMX_TILE_ROW_NUM = 16 tokens/tile; 32 = 2 tiles constexpr static int64_t HeadDimAlignment = 2 * (AMX_TILE_ROW_BYTES / 4); // headdim num unit of PV phase constexpr static int64_t MaxQHeadNumPerIteration = 32; @@ -324,6 +402,9 @@ class AttentionImpl { constexpr static ISA ISAType = ISA::AMX; constexpr static bool scale_on_logits = true; + float k_scale = 1.0f; + float v_scale = 1.0f; + public: AttentionImpl() : current_q_head_num_(0) { // Use all columns in AMX tiles @@ -332,21 +413,50 @@ class AttentionImpl { ~AttentionImpl() { _tile_release(); } + void init_from_input(const AttentionInput* input) { + if constexpr (fp8_kv) { + k_scale = input->k_scale_fp8; + v_scale = input->v_scale_fp8; + } + } + + float get_output_v_scale() const noexcept { + if constexpr (fp8_kv) { + // AMX dequant places FP8 payload into a BF16 field (exponent bias 127). + // Correction = 2^(127 - FP8_bias): E4M3 bias=7 → 2^120, E5M2 bias=15 → + // 2^112. + constexpr float bias = + std::is_same_v ? 0x1p112f : 0x1p120f; + return v_scale * bias; + } + return 1.0f; + } + template