| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import asyncio |
| 4 | import os |
| 5 | from contextlib import AsyncExitStack |
| 6 | from dataclasses import replace |
| 7 | |
| 8 | import pytest |
| 9 | |
| 10 | from vllm import SamplingParams |
| 11 | from vllm.engine.arg_utils import AsyncEngineArgs |
| 12 | from vllm.platforms import current_platform |
| 13 | from vllm.sampling_params import RequestOutputKind |
| 14 | from vllm.v1.engine.async_llm import AsyncLLM |
| 15 | |
| 16 | DP_SIZE = int(os.getenv("DP_SIZE", 2)) |
| 17 | |
| 18 | if current_platform.is_rocm(): |
| 19 | ATTN_BACKENDS = ["ROCM_ATTN", "TRITON_ATTN", "FLEX_ATTENTION"] |
| 20 | else: |
| 21 | ATTN_BACKENDS = ["FLASH_ATTN"] |
| 22 | |
| 23 | # On SM<90 (e.g., L4), batch invariance does not support CUDA graphs. |
| 24 | # See https://github.com/vllm-project/vllm/pull/30018 and |
| 25 | # tests/v1/determinism/utils.py for the documented limitation. |
| 26 | IS_DEVICE_CAPABILITY_BELOW_90 = not current_platform.has_device_capability(90) |
| 27 | |
| 28 | |
| 29 | @pytest.mark.asyncio |
| 30 | @pytest.mark.parametrize("attn_backend", ATTN_BACKENDS) |
| 31 | @pytest.mark.xfail( |
| 32 | current_platform.is_rocm(), |
| 33 | reason="Test may fail on ROCm until batch invariance is enabled. " |
| 34 | "See: https://github.com/vllm-project/vllm/issues/27433", |
| 35 | strict=False, |
| 36 | ) |
| 37 | async def test_run_eagle_dp(monkeypatch: pytest.MonkeyPatch, attn_backend: str): |
| 38 | if not current_platform.is_rocm() and not current_platform.is_xpu(): |
| 39 | # This test checks that running a model with and without eagle |
| 40 | # leads to identical tokens. |
| 41 | # |
| 42 | # NOTE: This is only true in batch invariant mode |
| 43 | # (because the target model verifies all draft tokens in one big |
| 44 | # forward pass) |
| 45 | # |
| 46 | # TODO[ROCm]: Test is passing on ROCm CI but may break in future. |
| 47 | # Enable batch invariance for ROCm when possible. See: |
| 48 | # https://github.com/vllm-project/vllm/issues/27433 |
| 49 | |
| 50 | monkeypatch.setenv("VLLM_BATCH_INVARIANT", "1") |
| 51 | |
| 52 | target_model = "meta-llama/Llama-3.1-8B-Instruct" |
| 53 | draft_model = "yuhuili/EAGLE-LLaMA3.1-Instruct-8B" |
| 54 | |
| 55 | engine_args = AsyncEngineArgs( |
| 56 | model=target_model, |
| 57 | tokenizer_mode="auto", |
| 58 | enforce_eager=IS_DEVICE_CAPABILITY_BELOW_90, |
| 59 | tensor_parallel_size=int(os.getenv("TP_SIZE", 1)), |
| 60 | data_parallel_size=DP_SIZE, |
| 61 | data_parallel_backend="mp", # ray takes more time |
| 62 | trust_remote_code=True, |
| 63 | max_model_len=16384, |
| 64 | attention_config={"backend": attn_backend}, |
| 65 | ) |
| 66 | |
| 67 | eagle_engine_args = replace( |
| 68 | engine_args, |
| 69 | speculative_config={ |
| 70 | "model": draft_model, |
| 71 | "method": "eagle", |
| 72 | "num_speculative_tokens": 3, |
| 73 | }, |
| 74 | ) |
| 75 | |
| 76 | prompt = "This is a test of data parallel with eagle" |
| 77 | num_expected_tokens = 100 |
| 78 | sampling_params = SamplingParams( |
| 79 | max_tokens=num_expected_tokens, |
| 80 | ignore_eos=True, |
| 81 | output_kind=RequestOutputKind.FINAL_ONLY, |
| 82 | temperature=0, |
| 83 | ) |
| 84 | |
| 85 | async def generate_with_timeout(given_engine: AsyncLLM): |
| 86 | async for out in given_engine.generate( |
| 87 | request_id="test-eagle-dp", prompt=prompt, sampling_params=sampling_params |
| 88 | ): |
| 89 | token_ids = out.outputs[0].token_ids |
| 90 | assert len(token_ids) == num_expected_tokens |
| 91 | return token_ids |
| 92 | |
| 93 | async def engine_create_and_generate(engine_args: AsyncEngineArgs): |
| 94 | async with AsyncExitStack() as after: |
| 95 | engine = AsyncLLM.from_engine_args(engine_args) |
| 96 | after.callback(engine.shutdown) |
| 97 | |
| 98 | token_ids = await asyncio.wait_for( |
| 99 | generate_with_timeout(engine), timeout=30 |
| 100 | ) |
| 101 | |
| 102 | assert not engine.output_processor.has_unfinished_requests() |
| 103 | return token_ids |
| 104 | |
| 105 | token_ids_with_eagle = await engine_create_and_generate(eagle_engine_args) |
| 106 | token_ids_no_eagle = await engine_create_and_generate(engine_args) |
| 107 | |
| 108 | # Test for correctness |
| 109 | assert token_ids_with_eagle == token_ids_no_eagle |