test_spec_decode.py 21.4 KB
Newer Older
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import random
4
from typing import Any
5

6
import os
7
import pytest
zhiweiz's avatar
zhiweiz committed
8
import torch
9

10
from tests.utils import get_attn_backend_list_based_on_platform, large_gpu_mark
11
from vllm import LLM, SamplingParams
12

13
14
from vllm.assets.base import VLLM_S3_BUCKET_URL
from vllm.assets.image import VLM_IMAGES_DIR
15

zhiweiz's avatar
zhiweiz committed
16
from vllm.distributed import cleanup_dist_env_and_memory
17
from vllm.platforms import current_platform
18
from ...utils import models_path_prefix
19

20
21
MTP_SIMILARITY_RATE = 0.8

22
23
MTP_SIMILARITY_RATE = 0.8

24

25
26
27
28
29
30
31
32
33
34
def _skip_if_insufficient_gpus_for_tp(tp_size: int):
    """Skip test if available GPUs < tp_size on ROCm."""
    if current_platform.is_rocm():
        available_gpus = torch.cuda.device_count()
        if available_gpus < tp_size:
            pytest.skip(
                f"Test requires {tp_size} GPUs, but only {available_gpus} available"
            )


35
def get_test_prompts(mm_enabled: bool):
36
    prompt_types = ["repeat", "sentence"]
37
38
    if mm_enabled:
        prompt_types.append("mm")
39
40
41
42
43
    num_prompts = 100
    prompts = []

    random.seed(0)
    random_prompt_type_choices = random.choices(prompt_types, k=num_prompts)
44
    print(f"Prompt types: {random_prompt_type_choices}")
45
46
47
48
49
50

    # Generate a mixed batch of prompts, some of which can be easily
    # predicted by n-gram matching and some which likely cannot.
    for kind in random_prompt_type_choices:
        word_choices = ["test", "temp", "hello", "where"]
        word = random.choice(word_choices)
51
        prompt: str | list[dict[str, Any]] = ""
52
53
54
55
56
57
58
59
60
61
62
63
        if kind == "repeat":
            prompt = f"""
            please repeat the word '{word}' 10 times.
            give no other output than the word at least ten times in a row,
            in lowercase with spaces between each word and without quotes.
            """
        elif kind == "sentence":
            prompt = f"""
            please give a ten-word sentence that
            uses the word {word} at least once.
            give no other output than that simple sentence without quotes.
            """
64
        elif kind == "mm":
65
66
67
68
69
70
71
72
            placeholders = [
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"{VLLM_S3_BUCKET_URL}/{VLM_IMAGES_DIR}/stop_sign.jpg"
                    },
                }
            ]
73
74
            prompt = [
                *placeholders,
75
                {"type": "text", "text": "The meaning of the image is"},
76
            ]
77
78
79
80
81
        else:
            raise ValueError(f"Unknown prompt type: {kind}")
        prompts.append([{"role": "user", "content": prompt}])

    return prompts
82
83
84
85


@pytest.fixture
def sampling_config():
86
    return SamplingParams(temperature=0, max_tokens=10, ignore_eos=False)
87
88
89
90


@pytest.fixture
def model_name():
91
    # return os.path.join(models_path_prefix, "meta-llama/Llama-3.1-8B-Instruct")
zhuwenwen's avatar
zhuwenwen committed
92
    return os.path.join(models_path_prefix, "meta-llama/Llama-3.1-8B-Instruct")
93
94


95
96
97
98
99
100
101
102
@pytest.fixture(autouse=True)
def reset_torch_dynamo():
    """Reset torch dynamo cache before each test"""
    yield
    # Cleanup after test
    torch._dynamo.reset()


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@pytest.mark.parametrize(
    "speculative_config",
    [
        {
            "method": "ngram",
            "prompt_lookup_max": 5,
            "prompt_lookup_min": 3,
            "num_speculative_tokens": 3,
        },
        {
            "method": "suffix",
            "suffix_decoding_max_spec_factor": 2.0,
        },
    ],
)
def test_ngram_and_suffix_correctness(
    speculative_config: dict,
120
121
122
123
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
    model_name: str,
):
124
    """
125
    Compare the outputs of an original LLM and a speculative LLM
126
    should be the same when using ngram speculative decoding.
127
    """
128
129
130
131
132
133
134
135
136
137
    test_prompts = get_test_prompts(mm_enabled=False)

    ref_llm = LLM(model=model_name, max_model_len=1024)
    ref_outputs = ref_llm.chat(test_prompts, sampling_config)
    del ref_llm
    torch.cuda.empty_cache()
    cleanup_dist_env_and_memory()

    spec_llm = LLM(
        model=model_name,
138
        speculative_config=speculative_config,
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        max_model_len=1024,
    )
    spec_outputs = spec_llm.chat(test_prompts, sampling_config)
    matches = 0
    misses = 0
    for ref_output, spec_output in zip(ref_outputs, spec_outputs):
        if ref_output.outputs[0].text == spec_output.outputs[0].text:
            matches += 1
        else:
            misses += 1
            print(f"ref_output: {ref_output.outputs[0].text}")
            print(f"spec_output: {spec_output.outputs[0].text}")

    # Heuristic: expect at least 66% of the prompts to match exactly
    # Upon failure, inspect the outputs to check for inaccuracy.
    assert matches >= int(0.66 * len(ref_outputs))
    del spec_llm
    torch.cuda.empty_cache()
    cleanup_dist_env_and_memory()


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def test_suffix_decoding_acceptance(
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
    model_name: str,
):
    """
    Check that suffix decoding caching takes effect and improves acceptance
    lengths and acceptance rates over multiple runs of the same prompts.
    """
    test_prompts = get_test_prompts(mm_enabled=False)

    spec_llm = LLM(
        model=model_name,
        speculative_config={
            "method": "suffix",
            "suffix_decoding_max_spec_factor": 2.0,
            "suffix_decoding_max_cached_requests": 1000,
        },
        max_model_len=1024,
        disable_log_stats=False,
    )

    # Run several times and check that the accepted tokens increase.
    num_draft = []
    num_accept = []
    for i in range(10):  # Run multiple times to warm up the cache.
        spec_llm.chat(test_prompts, sampling_config)
        # Collect draft and acceptance stats.
        metrics = spec_llm.get_metrics()
        for metric in metrics:
            if metric.name == "vllm:spec_decode_num_draft_tokens":
                num_draft.append(metric.value)
            if metric.name == "vllm:spec_decode_num_accepted_tokens":
                num_accept.append(metric.value)

    # Calculate the acceptance rates for the first and last runs.
    first_accept_tokens = num_accept[0]
    first_draft_tokens = num_draft[0]
    first_accept_rate = first_accept_tokens / first_draft_tokens

    # Take the diff since the stats are cumulative.
    last_accept_tokens = num_accept[-1] - num_accept[-2]
    last_draft_tokens = num_draft[-1] - num_draft[-2]
    last_accept_rate = last_accept_tokens / last_draft_tokens

    # Expect the acceptance length to improve.
    assert first_accept_tokens < last_accept_tokens

    # Expect the acceptance rate to improve.
    assert first_accept_rate < last_accept_rate

211
212
    # Heuristic: expect at least 80.0% acceptance rate at the end.
    assert last_accept_rate > 0.80
213
214
215
216
217
218

    del spec_llm
    torch.cuda.empty_cache()
    cleanup_dist_env_and_memory()


219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
@pytest.mark.parametrize(
    "model_path",
    [
        "RedHatAI/Llama-3.1-8B-Instruct-speculator.eagle3",
        "RedHatAI/Qwen3-8B-speculator.eagle3",
    ],
    ids=["llama3_eagle3_speculator", "qwen3_eagle3_speculator"],
)
def test_speculators_model_integration(
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
    model_path: str,
):
    """
    Test that speculators models work with the simplified integration.

    This verifies the `vllm serve <speculator-model>` use case where
    speculative config is automatically detected from the model config
    without requiring explicit --speculative-config argument.

    Tests:
    1. Speculator model is correctly detected
    2. Verifier model is extracted from speculator config
    3. Speculative decoding is automatically enabled
    4. Text generation works correctly
    5. Output matches reference (non-speculative) generation
    """
    monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")

    # Generate test prompts
    test_prompts = get_test_prompts(mm_enabled=False)

    # First run: Direct speculator model (simplified integration)
    spec_llm = LLM(model=model_path, max_model_len=1024)
    spec_outputs = spec_llm.chat(test_prompts, sampling_config)

    # Verify speculative config was auto-detected
    assert spec_llm.llm_engine.vllm_config.speculative_config is not None, (
        f"Speculative config should be auto-detected for {model_path}"
    )

    spec_config = spec_llm.llm_engine.vllm_config.speculative_config
    assert spec_config.num_speculative_tokens > 0, (
        f"Expected positive speculative tokens, "
        f"got {spec_config.num_speculative_tokens}"
    )

    # Verify draft model is set to the speculator model
    assert spec_config.model == model_path, (
        f"Draft model should be {model_path}, got {spec_config.model}"
    )

    # Extract verifier model for reference run
    verifier_model = spec_llm.llm_engine.vllm_config.model_config.model

    del spec_llm
    torch.cuda.empty_cache()
    cleanup_dist_env_and_memory()

    # Second run: Reference without speculative decoding
    ref_llm = LLM(model=verifier_model, max_model_len=1024)
    ref_outputs = ref_llm.chat(test_prompts, sampling_config)
    del ref_llm
    torch.cuda.empty_cache()
    cleanup_dist_env_and_memory()

    # Compare outputs
    matches = sum(
        1
        for ref, spec in zip(ref_outputs, spec_outputs)
        if ref.outputs[0].text == spec.outputs[0].text
    )

    # Heuristic: expect at least 66% of prompts to match exactly
    assert matches >= int(0.66 * len(ref_outputs)), (
        f"Only {matches}/{len(ref_outputs)} outputs matched. "
        f"Expected at least {int(0.66 * len(ref_outputs))} matches."
    )


299
@pytest.mark.parametrize(
300
    ["model_setup", "mm_enabled", "enable_chunked_prefill", "model_impl"],
301
    [
302
303
304
305
306
307
308
309
310
311
312
313
        (
            ("eagle3", "Qwen/Qwen3-8B", "AngelSlim/Qwen3-8B_eagle3", 1),
            False,
            False,
            "auto",
        ),
        (
            ("eagle3", "Qwen/Qwen3-8B", "AngelSlim/Qwen3-8B_eagle3", 1),
            False,
            False,
            "transformers",
        ),
314
315
316
317
318
319
320
321
322
        pytest.param(
            (
                "eagle3",
                "Qwen/Qwen3-VL-8B-Instruct",
                "taobao-mnn/Qwen3-VL-8B-Instruct-Eagle3",
                1,
            ),
            False,
            False,
323
            "auto",
324
325
326
327
            marks=pytest.mark.skip(
                reason="architecture of its eagle3 is LlamaForCausalLMEagle3"
            ),
        ),
328
329
330
331
332
333
334
335
        pytest.param(
            (
                "eagle3",
                "Qwen/Qwen2.5-VL-7B-Instruct",
                "Rayzl/qwen2.5-vl-7b-eagle3-sgl",
                1,
            ),
            False,
336
            False,
337
            "auto",
338
339
340
341
            marks=pytest.mark.skip(
                reason="Skipping due to its head_dim not being a a multiple of 32"
            ),
        ),
342
        pytest.param(
343
344
345
346
347
348
349
            (
                "eagle",
                "meta-llama/Llama-3.1-8B-Instruct",
                "yuhuili/EAGLE-LLaMA3.1-Instruct-8B",
                1,
            ),
            False,
350
            True,
351
            "auto",
352
353
            marks=large_gpu_mark(min_gb=40),
        ),  # works on 4x H100
354
355
356
357
358
359
360
361
        (
            (
                "eagle3",
                "meta-llama/Llama-3.1-8B-Instruct",
                "yuhuili/EAGLE3-LLaMA3.1-Instruct-8B",
                1,
            ),
            False,
362
            False,
363
            "auto",
364
365
366
367
368
369
370
371
372
        ),
        pytest.param(
            (
                "eagle",
                "meta-llama/Llama-4-Scout-17B-16E-Instruct",
                "morgendave/EAGLE-Llama-4-Scout-17B-16E-Instruct",
                4,
            ),
            False,
373
            False,
374
            "auto",
375
376
377
378
379
380
381
382
383
384
            marks=large_gpu_mark(min_gb=80),
        ),  # works on 4x H100
        pytest.param(
            (
                "eagle",
                "meta-llama/Llama-4-Scout-17B-16E-Instruct",
                "morgendave/EAGLE-Llama-4-Scout-17B-16E-Instruct",
                4,
            ),
            True,
385
            True,
386
            "auto",
387
388
389
390
391
392
393
394
395
396
            marks=large_gpu_mark(min_gb=80),
        ),  # works on 4x H100
        (
            (
                "eagle",
                "eagle618/deepseek-v3-random",
                "eagle618/eagle-deepseek-v3-random",
                1,
            ),
            False,
397
            False,
398
            "auto",
399
        ),
400
401
    ],
    ids=[
402
        "qwen3_eagle3",
403
        "qwen3_eagle3-transformers",
404
        "qwen3_vl_eagle3",
405
406
407
408
409
410
411
412
413
        "qwen2_5_vl_eagle3",
        "llama3_eagle",
        "llama3_eagle3",
        "llama4_eagle",
        "llama4_eagle_mm",
        "deepseek_eagle",
    ],
)
@pytest.mark.parametrize("attn_backend", get_attn_backend_list_based_on_platform())
414
415
416
def test_eagle_correctness(
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
zhiweiz's avatar
zhiweiz committed
417
    model_setup: tuple[str, str, str, int],
418
    mm_enabled: bool,
419
    enable_chunked_prefill: bool,
420
    model_impl: str,
421
    attn_backend: str,
422
):
423
424
425
426
    if attn_backend == "TREE_ATTN":
        # TODO: Fix this flaky test
        pytest.skip(
            "TREE_ATTN is flaky in the test disable for now until it can be "
427
428
            "resolved (see https://github.com/vllm-project/vllm/issues/22922)"
        )
429
430
431
432
433
434
435
436
437
438
439
    if model_impl == "transformers":
        import transformers
        from packaging.version import Version

        installed = Version(transformers.__version__)
        required = Version("5.0.0.dev")
        if installed < required:
            pytest.skip(
                "Eagle3 with the Transformers modeling backend requires "
                f"transformers>={required}, but got {installed}"
            )
440

441
442
    # Generate test prompts inside the function instead of using fixture
    test_prompts = get_test_prompts(mm_enabled)
443
    """
444
445
    Compare the outputs of a original LLM and a speculative LLM
    should be the same when using eagle speculative decoding.
zhiweiz's avatar
zhiweiz committed
446
    model_setup: (method, model_name, eagle_model_name, tp_size)
447
    """
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
    # Determine attention config
    # Scout requires default backend selection because vision encoder has
    # head_dim 88 being incompatible with FLASH_ATTN and needs to fall back
    # to Flex Attn
    if "Llama-4-Scout" in model_setup[1] and attn_backend == "FLASH_ATTN":
        if current_platform.is_rocm():
            # TODO: Enable Flex Attn for spec_decode on ROCm
            pytest.skip("Flex Attn for spec_decode not supported on ROCm currently")
        attention_config = None  # Let it fall back to default
    else:
        attention_config = {"backend": attn_backend}

    if attn_backend == "TRITON_ATTN" and not current_platform.is_rocm():
        pytest.skip(
            "TRITON_ATTN does not support "
            "multi-token eagle spec decode on current platform"
        )
465

466
467
    with monkeypatch.context() as m:
        m.setenv("VLLM_MLA_DISABLE", "1")
468

469
        if attn_backend == "ROCM_AITER_FA" and current_platform.is_rocm():
470
            if "deepseek" in model_setup[1].lower():
471
                pytest.skip("ROCM_AITER_FA for deepseek not supported on ROCm platform")
472
473
            else:
                m.setenv("VLLM_ROCM_USE_AITER", "1")
474

zhiweiz's avatar
zhiweiz committed
475
        method, model_name, spec_model_name, tp_size = model_setup
476
477
        _skip_if_insufficient_gpus_for_tp(tp_size)

478
        max_model_len = 2048
479
        max_num_batched_tokens = 128 if enable_chunked_prefill else max_model_len
480

481
        ref_llm = LLM(
482
483
484
485
            model=model_name,
            max_model_len=max_model_len,
            tensor_parallel_size=tp_size,
            attention_config=attention_config,
486
        )
487
        ref_outputs = ref_llm.chat(test_prompts, sampling_config)
488
        del ref_llm
zhiweiz's avatar
zhiweiz committed
489
490
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()
491

492
493
        spec_llm = LLM(
            model=model_name,
494
            trust_remote_code=True,
zhiweiz's avatar
zhiweiz committed
495
            tensor_parallel_size=tp_size,
496
            speculative_config={
zhiweiz's avatar
zhiweiz committed
497
                "method": method,
498
                "model": spec_model_name,
499
                "num_speculative_tokens": 3,
500
                "max_model_len": max_model_len,
501
            },
502
503
            max_model_len=max_model_len,
            max_num_batched_tokens=max_num_batched_tokens,
504
            enable_chunked_prefill=enable_chunked_prefill,
505
            model_impl=model_impl,
506
            attention_config=attention_config,
507
        )
508
509
510
        spec_outputs = spec_llm.chat(test_prompts, sampling_config)
        matches = 0
        misses = 0
511
        for ref_output, spec_output in zip(ref_outputs, spec_outputs):
512
513
514
515
516
517
518
            if ref_output.outputs[0].text == spec_output.outputs[0].text:
                matches += 1
            else:
                misses += 1
                print(f"ref_output: {ref_output.outputs[0].text}")
                print(f"spec_output: {spec_output.outputs[0].text}")

519
        # Heuristic: expect at least 60% of the prompts to match exactly
520
        # Upon failure, inspect the outputs to check for inaccuracy.
521
        assert matches > int(0.6 * len(ref_outputs))
522
        del spec_llm
zhiweiz's avatar
zhiweiz committed
523
524
525
526
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()


527
528
529
@pytest.mark.parametrize(
    ["model_setup", "mm_enabled"],
    [
530
531
        (("mtp", os.path.join(models_path_prefix, "XiaomiMiMo/MiMo-7B-Base"), 1), False),
        (("mtp", os.path.join(models_path_prefix, "ZixiQi/DeepSeek-V3-4layers-MTP-FP8"), 1), False),
532
533
534
    ],
    ids=["mimo", "deepseek"],
)
535
def test_mtp_correctness(
536
537
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
538
    model_setup: tuple[str, str, int],
539
    mm_enabled: bool,
540
):
541
542
    # Generate test prompts inside the function instead of using fixture
    test_prompts = get_test_prompts(mm_enabled)
543
    """
544
    Compare the outputs of a original LLM and a speculative LLM
545
546
    should be the same when using MTP speculative decoding.
    model_setup: (method, model_name, tp_size)
547
    """
548
    with monkeypatch.context() as m:
549
        m.setenv("VLLM_MLA_DISABLE", "1")
550

551
        method, model_name, tp_size = model_setup
552
        _skip_if_insufficient_gpus_for_tp(tp_size)
553

554
555
556
557
558
559
        ref_llm = LLM(
            model=model_name,
            max_model_len=2048,
            tensor_parallel_size=tp_size,
            trust_remote_code=True,
        )
560
561
        ref_outputs = ref_llm.chat(test_prompts, sampling_config)
        del ref_llm
zhiweiz's avatar
zhiweiz committed
562
563
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()
564
565
566

        spec_llm = LLM(
            model=model_name,
567
            trust_remote_code=True,
zhiweiz's avatar
zhiweiz committed
568
            tensor_parallel_size=tp_size,
569
            speculative_config={
zhiweiz's avatar
zhiweiz committed
570
                "method": method,
571
                "num_speculative_tokens": 1,
572
                "max_model_len": 2048,
573
            },
574
            max_model_len=2048,
575
576
577
578
579
580
581
582
583
584
585
586
        )
        spec_outputs = spec_llm.chat(test_prompts, sampling_config)
        matches = 0
        misses = 0
        for ref_output, spec_output in zip(ref_outputs, spec_outputs):
            if ref_output.outputs[0].text == spec_output.outputs[0].text:
                matches += 1
            else:
                misses += 1
                print(f"ref_output: {ref_output.outputs[0].text}")
                print(f"spec_output: {spec_output.outputs[0].text}")

587
        # Heuristic: expect at least 80% of the prompts to match exactly
588
        # Upon failure, inspect the outputs to check for inaccuracy.
589
        assert matches > int(MTP_SIMILARITY_RATE * len(ref_outputs))
590
        del spec_llm
zhiweiz's avatar
zhiweiz committed
591
592
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655


@pytest.mark.parametrize(["model_setup", "mm_enabled"], [
    (("mtp", "XiaomiMiMo/MiMo-7B-Base", 1), False),
    (("mtp", "ZixiQi/DeepSeek-V3-4layers-MTP-FP8", 1), False),
],
                         ids=["mimo", "deepseek"])
def test_mtp_correctness(
    monkeypatch: pytest.MonkeyPatch,
    sampling_config: SamplingParams,
    model_setup: tuple[str, str, int],
    mm_enabled: bool,
):
    # Generate test prompts inside the function instead of using fixture
    test_prompts = get_test_prompts(mm_enabled)
    '''
    Compare the outputs of a original LLM and a speculative LLM
    should be the same when using MTP speculative decoding.
    model_setup: (method, model_name, tp_size)
    '''
    with monkeypatch.context() as m:
        m.setenv("VLLM_USE_V1", "1")
        m.setenv("VLLM_MLA_DISABLE", "1")

        method, model_name, tp_size = model_setup

        ref_llm = LLM(model=model_name,
                      max_model_len=2048,
                      tensor_parallel_size=tp_size,
                      trust_remote_code=True)
        ref_outputs = ref_llm.chat(test_prompts, sampling_config)
        del ref_llm
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()

        spec_llm = LLM(
            model=model_name,
            trust_remote_code=True,
            tensor_parallel_size=tp_size,
            speculative_config={
                "method": method,
                "num_speculative_tokens": 1,
                "max_model_len": 2048,
            },
            max_model_len=2048,
        )
        spec_outputs = spec_llm.chat(test_prompts, sampling_config)
        matches = 0
        misses = 0
        for ref_output, spec_output in zip(ref_outputs, spec_outputs):
            if ref_output.outputs[0].text == spec_output.outputs[0].text:
                matches += 1
            else:
                misses += 1
                print(f"ref_output: {ref_output.outputs[0].text}")
                print(f"spec_output: {spec_output.outputs[0].text}")

        # Heuristic: expect at least 80% of the prompts to match exactly
        # Upon failure, inspect the outputs to check for inaccuracy.
        assert matches > int(MTP_SIMILARITY_RATE * len(ref_outputs))
        del spec_llm
        torch.cuda.empty_cache()
        cleanup_dist_env_and_memory()