| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import math |
| 5 | from pathlib import Path |
| 6 | |
| 7 | import pytest |
| 8 | |
| 9 | from tests.conftest import VllmRunner |
| 10 | from tests.utils import create_new_process_for_each_test |
| 11 | from vllm import SamplingParams, TokensPrompt |
| 12 | from vllm.config import CUDAGraphMode |
| 13 | from vllm.transformers_utils.configs.kimi_k3 import KimiK3Config |
| 14 | |
| 15 | MODEL = "moonshotai/Kimi-Linear-48B-A3B-Instruct" |
| 16 | |
| 17 | |
| 18 | def _make_tiny_overrides() -> dict: |
| 19 | linear_attn_config = { |
| 20 | "full_attn_layers": [4], |
| 21 | "head_dim": 128, |
| 22 | "kda_layers": [1, 2, 3], |
| 23 | "num_heads": 8, |
| 24 | "short_conv_kernel_size": 4, |
| 25 | "use_full_rank_gate": True, |
| 26 | } |
| 27 | |
| 28 | return { |
| 29 | "bos_token_id": 1, |
| 30 | "eos_token_id": 2, |
| 31 | "pad_token_id": 0, |
| 32 | "vocab_size": 256, |
| 33 | "hidden_size": 256, |
| 34 | "head_dim": 32, |
| 35 | "intermediate_size": 512, |
| 36 | "num_hidden_layers": 4, |
| 37 | "num_attention_heads": 8, |
| 38 | "num_key_value_heads": 8, |
| 39 | "num_experts": None, |
| 40 | "num_experts_per_token": None, |
| 41 | "num_shared_experts": 0, |
| 42 | "q_lora_rank": 128, |
| 43 | "kv_lora_rank": 512, |
| 44 | "qk_nope_head_dim": 128, |
| 45 | "qk_rope_head_dim": 64, |
| 46 | "v_head_dim": 128, |
| 47 | "mla_use_nope": True, |
| 48 | "mla_use_output_gate": True, |
| 49 | "linear_attn_config": linear_attn_config, |
| 50 | "max_position_embeddings": 512, |
| 51 | "model_max_length": 512, |
| 52 | } |
| 53 | |
| 54 | |
| 55 | def _run_tiny_model( |
| 56 | vllm_runner: type[VllmRunner], dcp_size: int |
| 57 | ) -> list[tuple[list[int], list[float]]]: |
| 58 | with vllm_runner( |
| 59 | model_name=MODEL, |
| 60 | skip_tokenizer_init=True, |
| 61 | load_format="dummy", |
| 62 | hf_overrides=_make_tiny_overrides(), |
| 63 | tensor_parallel_size=2, |
| 64 | decode_context_parallel_size=dcp_size, |
| 65 | cp_kv_cache_interleave_size=1, |
| 66 | distributed_executor_backend="mp", |
| 67 | dtype="bfloat16", |
| 68 | seed=0, |
| 69 | enforce_eager=True, |
| 70 | max_model_len=512, |
| 71 | max_num_seqs=8, |
| 72 | max_num_batched_tokens=1024, |
| 73 | gpu_memory_utilization=0.25, |
| 74 | enable_chunked_prefill=True, |
| 75 | enable_prefix_caching=False, |
| 76 | ) as runner: |
| 77 | lengths = [1, 32, 33, 129] |
| 78 | prompts = [ |
| 79 | TokensPrompt( |
| 80 | prompt_token_ids=[ |
| 81 | 3 + ((request_idx + token_idx) % 251) for token_idx in range(length) |
| 82 | ] |
| 83 | ) |
| 84 | for request_idx, length in enumerate(lengths) |
| 85 | ] |
| 86 | outputs = runner.llm.generate( |
| 87 | prompts, |
| 88 | SamplingParams( |
| 89 | temperature=0, |
| 90 | max_tokens=8, |
| 91 | seed=0, |
| 92 | ignore_eos=True, |
| 93 | logprobs=20, |
| 94 | ), |
| 95 | use_tqdm=False, |
| 96 | ) |
| 97 | |
| 98 | results = [] |
| 99 | for request_output in outputs: |
| 100 | completion = request_output.outputs[0] |
| 101 | token_ids = list(completion.token_ids) |
| 102 | assert completion.logprobs is not None |
| 103 | selected_logprobs = [ |
| 104 | step_logprobs[token_id].logprob |
| 105 | for token_id, step_logprobs in zip(token_ids, completion.logprobs) |
| 106 | ] |
| 107 | results.append((token_ids, selected_logprobs)) |
| 108 | return results |
| 109 | |
| 110 | |
| 111 | @create_new_process_for_each_test() |
| 112 | @pytest.mark.distributed(num_gpus=2) |
| 113 | def test_kimi_linear_dcp_tiny( |
| 114 | vllm_runner: type[VllmRunner], |
| 115 | num_gpus_available: int, |
| 116 | ) -> None: |
| 117 | if num_gpus_available < 2: |
| 118 | pytest.skip("Need at least 2 GPUs") |
| 119 | |
| 120 | baseline = _run_tiny_model(vllm_runner, dcp_size=1) |
| 121 | dcp = _run_tiny_model(vllm_runner, dcp_size=2) |
| 122 | |
| 123 | assert [tokens for tokens, _ in dcp] == [tokens for tokens, _ in baseline] |
| 124 | logprob_drifts = [] |
| 125 | for (_, baseline_logprobs), (_, dcp_logprobs) in zip(baseline, dcp): |
| 126 | for baseline_logprob, dcp_logprob in zip(baseline_logprobs, dcp_logprobs): |
| 127 | assert math.isfinite(baseline_logprob) |
| 128 | assert math.isfinite(dcp_logprob) |
| 129 | logprob_drifts.append(abs(baseline_logprob - dcp_logprob)) |
| 130 | |
| 131 | assert max(logprob_drifts) <= 1e-2 |
| 132 | |
| 133 | |
| 134 | def _make_tiny_k3_config(model_dir: Path) -> str: |
| 135 | config = KimiK3Config( |
| 136 | text_config=_make_tiny_overrides(), |
| 137 | vision_config={ |
| 138 | "vt_num_attention_heads": 2, |
| 139 | "vt_num_hidden_layers": 1, |
| 140 | "vt_hidden_size": 32, |
| 141 | "vt_intermediate_size": 64, |
| 142 | "qkv_hidden_size": 48, |
| 143 | }, |
| 144 | architectures=["KimiK3ForConditionalGeneration"], |
| 145 | ) |
| 146 | config.save_pretrained(model_dir) |
| 147 | return str(model_dir) |
| 148 | |
| 149 | |
| 150 | def _run_k3_partial_prefix_reuse( |
| 151 | vllm_runner: type[VllmRunner], model_name: str, dcp_size: int |
| 152 | ) -> tuple[list[int], list[float], int]: |
| 153 | block_size = 256 |
| 154 | common_prefix = [3 + (token_idx % 251) for token_idx in range(block_size)] |
| 155 | prime_prompt = TokensPrompt(prompt_token_ids=common_prefix + [17]) |
| 156 | replay_prompt = TokensPrompt(prompt_token_ids=common_prefix + [18, 19]) |
| 157 | sampling_params = SamplingParams( |
| 158 | temperature=0, |
| 159 | max_tokens=8, |
| 160 | seed=0, |
| 161 | ignore_eos=True, |
| 162 | logprobs=20, |
| 163 | ) |
| 164 | |
| 165 | with vllm_runner( |
| 166 | model_name=model_name, |
| 167 | skip_tokenizer_init=True, |
| 168 | load_format="dummy", |
| 169 | language_model_only=True, |
| 170 | tensor_parallel_size=2, |
| 171 | decode_context_parallel_size=dcp_size, |
| 172 | cp_kv_cache_interleave_size=1, |
| 173 | distributed_executor_backend="mp", |
| 174 | dtype="bfloat16", |
| 175 | seed=0, |
| 176 | max_model_len=512, |
| 177 | max_num_seqs=4, |
| 178 | max_num_batched_tokens=256, |
| 179 | gpu_memory_utilization=0.85, |
| 180 | block_size=block_size, |
| 181 | enable_chunked_prefill=True, |
| 182 | enable_prefix_caching=True, |
| 183 | mamba_cache_mode="align", |
| 184 | ) as runner: |
| 185 | assert ( |
| 186 | runner.llm.llm_engine.vllm_config.compilation_config.cudagraph_mode |
| 187 | == CUDAGraphMode.FULL_AND_PIECEWISE |
| 188 | ) |
| 189 | runner.llm.generate( |
| 190 | [prime_prompt], |
| 191 | sampling_params, |
| 192 | use_tqdm=False, |
| 193 | ) |
| 194 | replay_output = runner.llm.generate( |
| 195 | [replay_prompt], |
| 196 | sampling_params, |
| 197 | use_tqdm=False, |
| 198 | )[0] |
| 199 | |
| 200 | completion = replay_output.outputs[0] |
| 201 | token_ids = list(completion.token_ids) |
| 202 | assert len(token_ids) == sampling_params.max_tokens |
| 203 | assert completion.logprobs is not None |
| 204 | assert len(completion.logprobs) == sampling_params.max_tokens |
| 205 | selected_logprobs = [ |
| 206 | step_logprobs[token_id].logprob |
| 207 | for token_id, step_logprobs in zip(token_ids, completion.logprobs) |
| 208 | ] |
| 209 | return token_ids, selected_logprobs, replay_output.num_cached_tokens |
| 210 | |
| 211 | |
| 212 | @create_new_process_for_each_test() |
| 213 | @pytest.mark.distributed(num_gpus=2) |
| 214 | def test_kimi_k3_dcp_partial_prefix_reuse( |
| 215 | vllm_runner: type[VllmRunner], |
| 216 | num_gpus_available: int, |
| 217 | tmp_path: Path, |
| 218 | ) -> None: |
| 219 | if num_gpus_available < 2: |
| 220 | pytest.skip("Need at least 2 GPUs") |
| 221 | |
| 222 | model_name = _make_tiny_k3_config(tmp_path / "tiny-kimi-k3") |
| 223 | baseline_tokens, baseline_logprobs, baseline_cached = _run_k3_partial_prefix_reuse( |
| 224 | vllm_runner, model_name, dcp_size=1 |
| 225 | ) |
| 226 | dcp_tokens, dcp_logprobs, dcp_cached = _run_k3_partial_prefix_reuse( |
| 227 | vllm_runner, model_name, dcp_size=2 |
| 228 | ) |
| 229 | |
| 230 | assert baseline_cached == 256 |
| 231 | assert dcp_cached == 256 |
| 232 | assert dcp_tokens == baseline_tokens |
| 233 | assert len(dcp_logprobs) == len(baseline_logprobs) == 8 |
| 234 | logprob_drifts = [] |
| 235 | for baseline_logprob, dcp_logprob in zip(baseline_logprobs, dcp_logprobs): |
| 236 | assert math.isfinite(baseline_logprob) |
| 237 | assert math.isfinite(dcp_logprob) |
| 238 | logprob_drifts.append(abs(baseline_logprob - dcp_logprob)) |
| 239 | assert max(logprob_drifts) <= 1e-2 |