| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """LM eval harness on model to compare vs HF baseline computed offline. |
| 4 | Configs are found in configs/$MODEL.yaml |
| 5 | |
| 6 | pytest -s -v test_lm_eval_correctness.py \ |
| 7 | --config-list-file=configs/models-small.txt \ |
| 8 | --tp-size=1 |
| 9 | """ |
| 10 | |
| 11 | import os |
| 12 | from contextlib import contextmanager |
| 13 | |
| 14 | import lm_eval |
| 15 | import pytest |
| 16 | import yaml |
| 17 | |
| 18 | from vllm.platforms import current_platform |
| 19 | |
| 20 | DEFAULT_RTOL = 0.08 |
| 21 | |
| 22 | |
| 23 | @contextmanager |
| 24 | def scoped_env_vars(new_env: dict[str, str]): |
| 25 | if not new_env: |
| 26 | # Fast path: nothing to do |
| 27 | yield |
| 28 | return |
| 29 | |
| 30 | old_values = {} |
| 31 | new_keys = [] |
| 32 | |
| 33 | try: |
| 34 | for key, value in new_env.items(): |
| 35 | if key in os.environ: |
| 36 | old_values[key] = os.environ[key] |
| 37 | else: |
| 38 | new_keys.append(key) |
| 39 | os.environ[key] = str(value) |
| 40 | yield |
| 41 | finally: |
| 42 | # Restore / clean up |
| 43 | for key, value in old_values.items(): |
| 44 | os.environ[key] = value |
| 45 | for key in new_keys: |
| 46 | os.environ.pop(key, None) |
| 47 | |
| 48 | |
| 49 | def launch_lm_eval(eval_config, tp_size): |
| 50 | trust_remote_code = eval_config.get("trust_remote_code", False) |
| 51 | max_model_len = eval_config.get("max_model_len", 4096) |
| 52 | batch_size = eval_config.get("batch_size", "auto") |
| 53 | backend = eval_config.get("backend", "vllm") |
| 54 | enforce_eager = eval_config.get("enforce_eager", "true") |
| 55 | kv_cache_dtype = eval_config.get("kv_cache_dtype", "auto") |
| 56 | model_args = ( |
| 57 | f"pretrained={eval_config['model_name']}," |
| 58 | f"tensor_parallel_size={tp_size}," |
| 59 | f"enforce_eager={enforce_eager}," |
| 60 | f"kv_cache_dtype={kv_cache_dtype}," |
| 61 | f"add_bos_token=true," |
| 62 | f"trust_remote_code={trust_remote_code}," |
| 63 | f"max_model_len={max_model_len}," |
| 64 | "allow_deprecated_quantization=True," |
| 65 | ) |
| 66 | |
| 67 | if current_platform.is_rocm() and "Nemotron-3" in eval_config["model_name"]: |
| 68 | model_args += "attention_backend=TRITON_ATTN" |
| 69 | |
| 70 | moe_backend = eval_config.get("moe_backend", None) |
| 71 | if moe_backend is not None: |
| 72 | model_args += f"moe_backend={moe_backend}," |
| 73 | |
| 74 | if current_platform.is_rocm(): |
| 75 | rocm_load_strategy = eval_config.get("rocm_safetensors_load_strategy") |
| 76 | if rocm_load_strategy is not None: |
| 77 | model_args += f"safetensors_load_strategy={rocm_load_strategy}," |
| 78 | |
| 79 | tokenizer_mode = eval_config.get("tokenizer_mode", None) |
| 80 | if tokenizer_mode is not None: |
| 81 | model_args += f"tokenizer_mode={tokenizer_mode}," |
| 82 | |
| 83 | env_vars = eval_config.get("env_vars", None) |
| 84 | with scoped_env_vars(env_vars): |
| 85 | results = lm_eval.simple_evaluate( |
| 86 | model=backend, |
| 87 | model_args=model_args, |
| 88 | tasks=[task["name"] for task in eval_config["tasks"]], |
| 89 | num_fewshot=eval_config["num_fewshot"], |
| 90 | limit=eval_config["limit"], |
| 91 | # TODO(yeq): using chat template w/ fewshot_as_multiturn is supposed help |
| 92 | # text models. however, this is regressing measured strict-match for |
| 93 | # existing text models in CI, so only apply it for mm, or explicitly set |
| 94 | apply_chat_template=eval_config.get( |
| 95 | "apply_chat_template", backend == "vllm-vlm" |
| 96 | ), |
| 97 | fewshot_as_multiturn=eval_config.get("fewshot_as_multiturn", False), |
| 98 | # Forward decoding and early-stop controls (e.g., max_gen_toks, until=...) |
| 99 | gen_kwargs=eval_config.get("gen_kwargs"), |
| 100 | batch_size=batch_size, |
| 101 | ) |
| 102 | return results |
| 103 | |
| 104 | |
| 105 | def _check_rocm_gpu_arch_requirement(eval_config): |
| 106 | """Skip the test if the model requires a ROCm GPU arch not present. |
| 107 | |
| 108 | Model YAML configs can specify:: |
| 109 | |
| 110 | required_gpu_arch: |
| 111 | - gfx942 |
| 112 | - gfx950 |
| 113 | |
| 114 | The check only applies on ROCm. On other platforms (e.g. CUDA) the |
| 115 | field is ignored so that shared config files work for both NVIDIA and |
| 116 | AMD CI pipelines. |
| 117 | """ |
| 118 | required_archs = eval_config.get("required_gpu_arch") |
| 119 | if not required_archs: |
| 120 | return |
| 121 | |
| 122 | if not current_platform.is_rocm(): |
| 123 | return |
| 124 | |
| 125 | from vllm.platforms.rocm import _GCN_ARCH # noqa: E402 |
| 126 | |
| 127 | if not any(arch in _GCN_ARCH for arch in required_archs): |
| 128 | pytest.skip( |
| 129 | f"Model requires GPU arch {required_archs}, " |
| 130 | f"but detected arch is '{_GCN_ARCH}'" |
| 131 | ) |
| 132 | |
| 133 | |
| 134 | def test_lm_eval_correctness_param(config_filename, tp_size): |
| 135 | eval_config = yaml.safe_load(config_filename.read_text(encoding="utf-8")) |
| 136 | |
| 137 | _check_rocm_gpu_arch_requirement(eval_config) |
| 138 | |
| 139 | results = launch_lm_eval(eval_config, tp_size) |
| 140 | |
| 141 | rtol = eval_config.get("rtol", DEFAULT_RTOL) |
| 142 | |
| 143 | success = True |
| 144 | for task in eval_config["tasks"]: |
| 145 | for metric in task["metrics"]: |
| 146 | ground_truth = metric["value"] |
| 147 | measured_value = results["results"][task["name"]][metric["name"]] |
| 148 | print( |
| 149 | f"{task['name']} | {metric['name']}: " |
| 150 | f"ground_truth={ground_truth:.3f} | " |
| 151 | f"measured={measured_value:.3f} | rtol={rtol}" |
| 152 | ) |
| 153 | |
| 154 | min_acceptable = ground_truth * (1 - rtol) |
| 155 | success = success and measured_value >= min_acceptable |
| 156 | |
| 157 | assert success |