Compare commits

...
35 Commits
Author SHA1 Message Date
chrisvelaandGitHub 9e1c483136 Merge pull request #311 from runpod-workers/t3code/cd56c69d
Release / release (push) Waiting to run
fix: serve original model name when HF cache dir is lowercased (#310)
2026-06-26 13:48:13 -05:00
chrisvelaandGitHub d71ea9939d Merge pull request #308 from runpod-workers/docs/sync-vllm-version-0.20.2
docs: sync vLLM version to 0.20.2 in READMEs
2026-06-26 13:47:59 -05:00
chrisvelaandGitHub 7e2b4e2288 Merge pull request #314 from runpod-workers/runpod-package-update
chore: update runpod to 1.10.0
2026-06-26 13:45:16 -05:00
deanqandgithub-actions[bot] 015f8f3c4d chore: update runpod to 1.10.0 2026-06-26 17:20:41 +00:00
Hailong YangandGitHub 75ffcf73f2 Merge pull request #309 from adithyaJRunpod/feature/tuned-configs-round2
added configs for  Gemma 4 31B and GPT-OSS 120B
2026-06-22 17:41:50 -04:00
Tim Pietrusky d7ba3b6ab7 test: install pyyaml and isolate vllm config file in tests
after rebasing onto main, get_engine_args() loads a vllm-style config via
PyYAML (a transitive vllm dep). vllm is stubbed in tests, so add pyyaml
explicitly and point VLLM_CONFIG_FILE at a nonexistent path so no stray
config is picked up.
2026-06-19 19:05:24 +02:00
Tim Pietrusky b11c91722c test: complete vllm stub so src.utils imports under py<3.14
src.utils uses ErrorResponse (a vllm import) as a module-level return
annotation, evaluated eagerly on python <3.14. the vllm stub lacked it,
so collection failed with NameError on ci (py3.11) while passing locally
(py3.14, lazy annotations). add the missing vllm.utils / protocol /
SamplingParams symbols to the stub.
2026-06-19 19:04:45 +02:00
Tim Pietrusky fcdc799e0d fix: serve original model name when hf cache dir is lowercased (#310)
the fde-174 cache resolver rewrites engine_args.model to an on-disk
snapshot path when the model is found only under a lowercased hf cache
dir. the openai served model name is derived from engine_args.model, so
it silently became the filesystem path and requests using the real repo
id returned 404.

set served_model_name to the original repo id whenever the model is
rewritten to a path, unless an explicit served name (or
OPENAI_SERVED_MODEL_NAME_OVERRIDE) is provided.

add the first python tests in the repo (tests/) covering the cache-path
resolution and served-name decoupling, plus a Tests github workflow that
runs pytest on prs and pushes to main. vllm/torch are stubbed when absent
so the suite runs on a plain cpu runner.
2026-06-19 19:04:45 +02:00
AdithyaJob 84ec446493 added configs for Gemma 4 31B and GPT-OSS 120B 2026-06-17 22:19:58 -07:00
velaraptor-runpodandgithub-actions[bot] db246653a2 docs: sync vLLM version to 0.20.2 in READMEs 2026-06-12 20:51:06 +00:00
chrisvelaandGitHub 1b3228a2dc Merge pull request #307 from runpod-workers/fix/revert-0.20.0
Release / release (push) Waiting to run
revert to 0.20.2
2026-06-12 15:50:52 -05:00
velaraptor-runpod 4817d4a8e7 revert to 0.20.2 2026-06-12 15:49:23 -05:00
chrisvelaandGitHub 0378382a92 Merge pull request #306 from runpod-workers/revert/v2.20.1
Release / release (push) Waiting to run
chore: carry non-vllm changes from main (tests GPU + configs)
2026-06-12 15:21:22 -05:00
velaraptor-runpodandClaude Sonnet 4.6 08580e7ccf chore: carry non-vllm changes from main (tests GPU + configs)
Brings forward the L40 GPU type in tests.json and the new llama/qwen
tuned config files, while keeping Dockerfile pinned at vllm 0.20.2
(v2.20.1 state).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:18:36 -05:00
chrisvelaandGitHub 8868aae6b1 Merge pull request #305 from runpod-workers/fix/typo-tests-l40
Release / release (push) Waiting to run
fix: fix typo in tests
2026-06-12 14:00:54 -05:00
velaraptor-runpod fb8adc5c06 fix: fix typo in tests 2026-06-12 13:59:29 -05:00
chrisvelaandGitHub 7351da512b Merge pull request #304 from runpod-workers/fix/fix-tests
Release / release (push) Waiting to run
fix: change test gpu to L40
2026-06-12 13:49:15 -05:00
velaraptor-runpod 1e78043b2c fix: change test gpu to L40 2026-06-12 13:46:33 -05:00
chrisvelaandGitHub 352c64f4c1 Merge pull request #303 from runpod-workers/chore/check-vllm-versions-readmes
chore: check readmes vllm version in sync with version in Dockerfile
2026-06-12 09:49:07 -05:00
velaraptor-runpod 0922f5b435 chore: check readmes vllm version in sync with version in Dockerfile 2026-06-11 17:33:16 -05:00
chrisvelaandGitHub 5d9a48fc70 Merge pull request #302 from runpod-workers/feat/0.22.1
Release / release (push) Waiting to run
feat: upgrade vllm to 0.22.1
2026-06-11 17:27:38 -05:00
velaraptor-runpod 5d1579e361 fix: update readme links 2026-06-11 17:00:31 -05:00
velaraptor-runpod 0488b77d89 feat: upgrade vllm to 0.22.1 2026-06-11 16:29:02 -05:00
chrisvelaandGitHub d3a962c33b Merge pull request #301 from adithyaJRunpod/feature/tuned-configs
Release / release (push) Waiting to run
Add tuned configs,  CON-239
2026-06-11 14:26:00 -05:00
chrisvelaandGitHub 105c125698 Merge pull request #300 from runpod-workers/feat/0.21.0
feat: upgrade vllm to 0.21.0
2026-06-11 12:47:08 -05:00
AdithyaJob 0a0ccfcb60 Add tuned configs for Llama 3.1 8B and Qwen3 8B 2026-06-10 21:07:41 -07:00
velaraptor-runpod c8ce53c72c fix: add kenels, and fix for cuda 2026-06-10 16:01:53 -05:00
velaraptor-runpod 9618e799ba chore: fix cuda libraries 2026-06-04 15:35:58 -05:00
velaraptor-runpod cb3f077dba feat: upgrade vllm to 0.21.0 2026-06-03 14:43:56 -05:00
chrisvelaandGitHub 69646b9e99 Merge pull request #294 from runpod-workers/feat/allow-config
Release / release (push) Waiting to run
feat: allow config.yaml like vllm serve
2026-06-02 20:28:02 -05:00
Jacob CiparandGitHub 8b991a7ad7 Merge pull request #297 from runpod-workers/jhcipar/bump-runpod-python-version
feat: bump runpod-python version
2026-06-02 11:17:48 -04:00
Tim PietruskyandGitHub dac05b62b3 fix: make .runpod/tests.json hub tests pass on CUDA 13.0 (#299)
Release / release (push) Waiting to run
Three coupled fixes verified end-to-end on a private fork
(TimPietruskyRunPod/worker-vllm v0.1.3 → both hub tests passing):

1. tests.json allowedCudaVersions: 12.x → 13.0
   The Dockerfile and hub.json moved to CUDA 13.0 in v2.20.0
   (#288, #289), but tests.json was still pinned to 12.5–12.9, so
   the test pod was scheduled on a GPU with driver < 13.0 and
   container init failed at the nvidia-container-cli hook with
   "unsatisfied condition: cuda>=13.0".

2. requirements.txt kernels<0.15
   huggingface/kernels v0.15.1 tightened LayerRepository to require
   a revision or version argument
   (https://github.com/huggingface/kernels/pull/544). transformers
   >=5 still constructs LayerRepository(repo_id=..., layer_name=...)
   without either, so worker import raised ValueError during
   `from transformers import ...`. 0.14.1 is the last safe release.

3. tests.json timeout 30000 → 300000
   vLLM cold start (torch.compile + FlashInfer warmup) on RTX 4090
   for SmolLM2-135M takes ~60–70s before the first request can be
   served. The previous 30s per-test timeout fired before the
   worker came up, producing "context cancelled or timed out:
   context deadline exceeded" for every test even when the worker
   was healthy. 300s gives enough headroom for cold start + the
   actual inference call.

Refs: DR-1161
2026-06-02 17:08:54 +02:00
jhcipar d356c31675 feat: bump runpod-python version 2026-06-01 20:28:40 -04:00
Tim PietruskyandGitHub 14b74a4989 chore: re-enable .runpod/tests.json hub tests (#295)
Release / release (push) Waiting to run
Rename tests_json back to tests.json to re-enable the automated hub
tests that were temporarily disabled in #253.

Refs: DR-1161
2026-06-01 17:52:23 +02:00
velaraptor-runpod 80072047ab feat: allow config.yaml like vllm serve 2026-05-29 15:50:22 -05:00
14 changed files with 439 additions and 7 deletions
@@ -0,0 +1,71 @@
name: CI | Sync vLLM version in READMEs
on:
push:
branches: ["main"]
paths:
- "Dockerfile"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
sync_version:
runs-on: ubuntu-latest
name: Check README version matches Dockerfile and update if needed
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract vLLM version from Dockerfile and sync READMEs
run: |
echo "Extracting vLLM version from Dockerfile..."
dockerfile_version=$(grep -oP 'vllm(?:\[[\w,]+\])?==\K[\d.]+' Dockerfile | head -1)
if [ -z "$dockerfile_version" ]; then
echo "ERROR: Could not extract vLLM version from Dockerfile."
exit 1
fi
echo "Dockerfile vLLM version: $dockerfile_version"
echo "VLLM_VERSION=$dockerfile_version" >> $GITHUB_ENV
updated=0
for readme in README.md .runpod/README.md; do
if [ ! -f "$readme" ]; then
echo "Skipping $readme (not found)"
continue
fi
readme_version=$(grep -oP 'Current vLLM version: \[\K[\d.]+' "$readme" || echo "")
echo "$readme current version: ${readme_version:-not found}"
if [ "$readme_version" = "$dockerfile_version" ]; then
echo "$readme is already up to date."
continue
fi
echo "Updating $readme from $readme_version to $dockerfile_version..."
sed -i "s|Current vLLM version: \[${readme_version}\](https://github.com/vllm-project/vllm/releases/tag/v${readme_version})|Current vLLM version: [${dockerfile_version}](https://github.com/vllm-project/vllm/releases/tag/v${dockerfile_version})|g" "$readme"
updated=1
done
echo "UPDATED=$updated" >> $GITHUB_ENV
- name: Create Pull Request
if: env.UPDATED == '1'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "docs: sync vLLM version to ${{ env.VLLM_VERSION }} in READMEs"
title: "docs: sync vLLM version to ${{ env.VLLM_VERSION }} in READMEs"
body: |
The vLLM version in the Dockerfile has been updated to `${{ env.VLLM_VERSION }}`.
This PR syncs the version badge/link in:
- `README.md`
- `.runpod/README.md`
branch: docs/sync-vllm-version-${{ env.VLLM_VERSION }}
labels: documentation
+32
View File
@@ -0,0 +1,32 @@
name: Tests
on:
pull_request:
branches:
- "**"
push:
branches:
- "main"
permissions:
contents: read
jobs:
pytest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install -r tests/requirements.txt
- name: Run unit tests
run: python -m pytest tests -v
+11
View File
@@ -33,6 +33,17 @@ All behaviour is controlled through environment variables:
**Pass any vLLM engine arg** not listed above by setting an env var with the **UPPERCASED** field name (e.g. `MAX_MODEL_LEN=4096`, `ENABLE_CHUNKED_PREFILL=true`). The worker auto-discovers all `AsyncEngineArgs` fields from env. See the [vLLM engine args docs](https://docs.vllm.ai/en/latest/configuration/engine_args) for all available options. **Pass any vLLM engine arg** not listed above by setting an env var with the **UPPERCASED** field name (e.g. `MAX_MODEL_LEN=4096`, `ENABLE_CHUNKED_PREFILL=true`). The worker auto-discovers all `AsyncEngineArgs` fields from env. See the [vLLM engine args docs](https://docs.vllm.ai/en/latest/configuration/engine_args) for all available options.
**Configuration file:** You can also supply a `config.yaml` instead of (or alongside) env vars. Mount it at `/vllm_config.yaml` in the container, or set `VLLM_CONFIG_FILE` to a custom path. Use the same key names as `vllm serve` — hyphens and underscores both work:
```yaml
model: meta-llama/Llama-3.1-8B-Instruct
max-model-len: 8192
gpu-memory-utilization: 0.90
quantization: awq
```
Environment variables always override config file values.
For complete configuration options, see the [full configuration documentation](https://github.com/runpod-workers/worker-vllm/blob/main/docs/configuration.md). For complete configuration options, see the [full configuration documentation](https://github.com/runpod-workers/worker-vllm/blob/main/docs/configuration.md).
### Specify Transformers Version ### Specify Transformers Version
+4 -4
View File
@@ -5,7 +5,7 @@
"input": { "input": {
"prompt": "Write a short poem about artificial intelligence." "prompt": "Write a short poem about artificial intelligence."
}, },
"timeout": 30000 "timeout": 300000
}, },
{ {
"name": "openai_messages_test", "name": "openai_messages_test",
@@ -26,11 +26,11 @@
"temperature": 0.1 "temperature": 0.1
} }
}, },
"timeout": 30000 "timeout": 300000
} }
], ],
"config": { "config": {
"gpuTypeId": "NVIDIA GeForce RTX 4090", "gpuTypeId": "NVIDIA L40",
"gpuCount": 1, "gpuCount": 1,
"env": [ "env": [
{ {
@@ -38,6 +38,6 @@
"value": "HuggingFaceTB/SmolLM2-135M-Instruct" "value": "HuggingFaceTB/SmolLM2-135M-Instruct"
} }
], ],
"allowedCudaVersions": ["12.9", "12.8", "12.7", "12.6", "12.5"] "allowedCudaVersions": ["13.0"]
} }
} }
+14
View File
@@ -78,6 +78,20 @@ Configure worker-vllm using environment variables:
Any env var whose name matches a valid `AsyncEngineArgs` field (uppercased) is applied automatically. Backward-compat aliases: `MODEL_NAME`, `TOKENIZER_NAME`, `MAX_CONTEXT_LEN_TO_CAPTURE`. This lets you configure any vLLM option without waiting for explicit worker support. Any env var whose name matches a valid `AsyncEngineArgs` field (uppercased) is applied automatically. Backward-compat aliases: `MODEL_NAME`, `TOKENIZER_NAME`, `MAX_CONTEXT_LEN_TO_CAPTURE`. This lets you configure any vLLM option without waiting for explicit worker support.
### Configuration File (config.yaml)
As an alternative to environment variables, you can supply a `config.yaml` file using the same key names as `vllm serve` (hyphens or underscores both work):
```yaml
model: meta-llama/Llama-3.1-8B-Instruct
max-model-len: 8192
gpu-memory-utilization: 0.90
quantization: awq
tensor-parallel-size: 2
```
Mount the file into the container at `/vllm_config.yaml`, or point to a custom path with the `VLLM_CONFIG_FILE` env var. Environment variables always take precedence over config file values.
For the complete list of all available environment variables, examples, and detailed descriptions: **[Configuration](docs/configuration.md)** For the complete list of all available environment variables, examples, and detailed descriptions: **[Configuration](docs/configuration.md)**
### Specify Transformers Version ### Specify Transformers Version
+2 -2
View File
@@ -1,7 +1,7 @@
ray ray
pandas pandas
pyarrow pyarrow
runpod==1.9.0 runpod~=1.10.0
huggingface-hub huggingface-hub
lmcache==0.4.5 lmcache==0.4.5
packaging>=24.2 packaging>=24.2
@@ -11,5 +11,5 @@ pydantic-settings
hf-transfer hf-transfer
transformers>=5 transformers>=5
bitsandbytes>=0.45.0 bitsandbytes>=0.45.0
kernels kernels<0.15
torch-c-dlpack-ext torch-c-dlpack-ext
+11
View File
@@ -0,0 +1,11 @@
model: google/gemma-4-31b-it
gpu-memory-utilization: 0.95
max-model-len: 8192
dtype: auto
trust-remote-code: true
quantization: fp8
kv-cache-dtype: fp8
enforce-eager: false
enable-prefix-caching: true
enable-chunked-prefill: true
speculative-config: '{"model":"RedHatAI/gemma-4-31B-it-speculator.eagle3","method":"eagle3","num_speculative_tokens":3}'
+9
View File
@@ -0,0 +1,9 @@
model: openai/gpt-oss-120b
gpu-memory-utilization: 0.95
max-model-len: 8192
dtype: auto
trust-remote-code: true
enforce-eager: false
enable-prefix-caching: true
enable-chunked-prefill: true
speculative-config: '{"model":"RedHatAI/gpt-oss-120b-speculator.eagle3","method":"eagle3","num_speculative_tokens":3}'
+10
View File
@@ -0,0 +1,10 @@
model: meta-llama/Llama-3.1-8B-Instruct
gpu-memory-utilization: 0.95
max-model-len: 8192
dtype: auto
trust-remote-code: true
quantization: fp8
kv-cache-dtype: fp8
enforce-eager: false
enable-prefix-caching: true
speculative-config: '{"model":"RedHatAI/Llama-3.1-8B-Instruct-speculator.eagle3","method":"eagle3","num_speculative_tokens":3}'
+10
View File
@@ -0,0 +1,10 @@
model: Qwen/Qwen3-8B
gpu-memory-utilization: 0.95
max-model-len: 8192
dtype: auto
trust-remote-code: true
quantization: fp8
kv-cache-dtype: fp8
enforce-eager: false
enable-prefix-caching: true
speculative-config: '{"model":"RedHatAI/Qwen3-8B-speculator.eagle3","method":"eagle3","num_speculative_tokens":3}'
+28 -1
View File
@@ -404,6 +404,23 @@ def _resolve_cached_model_path(model_name: str) -> str:
return resolved return resolved
def _get_args_from_config_file() -> dict:
"""Load engine args from a vLLM-style config.yaml.
Checks VLLM_CONFIG_FILE env var, then falls back to /vllm_config.yaml.
Keys use the same long-form names as vllm serve (hyphens converted to underscores).
"""
import yaml
path = os.getenv("VLLM_CONFIG_FILE", "/vllm_config.yaml")
if not os.path.exists(path):
return {}
with open(path) as f:
raw = yaml.safe_load(f) or {}
normalized = {k.replace("-", "_"): v for k, v in raw.items()}
logging.info("Loaded engine args from config file %s: %s", path, list(normalized.keys()))
return normalized
def get_local_args(): def get_local_args():
""" """
Retrieve local arguments from a JSON file. Retrieve local arguments from a JSON file.
@@ -429,6 +446,9 @@ def get_engine_args():
# Start with worker custom defaults (only where we differ from vLLM) # Start with worker custom defaults (only where we differ from vLLM)
args = dict(DEFAULT_ARGS) args = dict(DEFAULT_ARGS)
# Config file values sit above defaults but below env vars
args.update(_get_args_from_config_file())
# Auto-discover: every AsyncEngineArgs field from env UPPERCASED (e.g. MAX_MODEL_LEN) # Auto-discover: every AsyncEngineArgs field from env UPPERCASED (e.g. MAX_MODEL_LEN)
args.update(_get_args_from_env_auto_discover()) args.update(_get_args_from_env_auto_discover())
@@ -581,6 +601,13 @@ def get_engine_args():
# Resolve lowercase HF cache paths (FDE-174) # Resolve lowercase HF cache paths (FDE-174)
if args.get("model"): if args.get("model"):
args["model"] = _resolve_cached_model_path(args["model"]) original_model = args["model"]
args["model"] = _resolve_cached_model_path(original_model)
# When the model was rewritten to an on-disk snapshot path, keep serving
# under the original repo id so the OpenAI API model name does not become
# a filesystem path (issue #310). An explicit served_model_name (or the
# OPENAI_SERVED_MODEL_NAME_OVERRIDE handled downstream) still wins.
if args["model"] != original_model and not args.get("served_model_name"):
args["served_model_name"] = original_model
return AsyncEngineArgs(**args) return AsyncEngineArgs(**args)
+105
View File
@@ -0,0 +1,105 @@
"""Shared test fixtures.
``src/engine_args.py`` hard-imports ``vllm`` (and a tensorizer submodule) and
``torch.cuda``. Both are only installed inside the GPU Docker image, so when the
tests run on a machine without them we install lightweight stubs. When the real
packages *are* available (e.g. CI inside the worker image) the stubs are skipped
and the real ones are used instead.
"""
import sys
import types
from dataclasses import dataclass
from typing import Optional, Union, List
def _install_torch_stub():
try:
import torch # noqa: F401
return # real torch present, nothing to stub
except Exception:
pass
torch = types.ModuleType("torch")
cuda = types.ModuleType("torch.cuda")
# No GPU in the test environment -> 0 devices (skips tensor-parallel setup).
cuda.device_count = lambda: 0
torch.cuda = cuda
sys.modules["torch"] = torch
sys.modules["torch.cuda"] = cuda
def _install_vllm_stub():
try:
import vllm # noqa: F401
return # real vLLM present, nothing to stub
except Exception:
pass
vllm = types.ModuleType("vllm")
@dataclass
class AsyncEngineArgs:
# Only the fields the worker actually sets/reads need to exist here;
# get_engine_args() filters args down to AsyncEngineArgs.__dataclass_fields__
# before construction, so unknown keys are dropped rather than passed.
model: Optional[str] = None
served_model_name: Optional[Union[str, List[str]]] = None
revision: Optional[str] = None
tokenizer: Optional[str] = None
trust_remote_code: bool = False
max_model_len: Optional[int] = None
max_num_batched_tokens: Optional[int] = None
disable_log_stats: bool = False
gpu_memory_utilization: float = 0.9
tensor_parallel_size: int = 1
max_parallel_loading_workers: Optional[int] = None
kv_cache_dtype: Optional[str] = None
class _Stub: # pragma: no cover - placeholder for vllm symbols
def __init__(self, *args, **kwargs):
pass
vllm.AsyncEngineArgs = AsyncEngineArgs
vllm.SamplingParams = _Stub
sys.modules["vllm"] = vllm
# src.utils imports these at module load and uses ErrorResponse as a return
# annotation, which Python evaluates eagerly on <3.14 -> must be defined.
vllm_utils = types.ModuleType("vllm.utils")
vllm_utils.random_uuid = lambda: "stub-uuid"
vllm.utils = vllm_utils
sys.modules["vllm.utils"] = vllm_utils
protocol = types.ModuleType("vllm.entrypoints.openai.engine.protocol")
protocol.ErrorResponse = _Stub
protocol.ErrorInfo = _Stub
protocol.RequestResponseMetadata = _Stub
for name in (
"vllm.entrypoints",
"vllm.entrypoints.openai",
"vllm.entrypoints.openai.engine",
):
sys.modules.setdefault(name, types.ModuleType(name))
sys.modules["vllm.entrypoints.openai.engine.protocol"] = protocol
# vllm.model_executor.model_loader.tensorizer.TensorizerConfig
model_executor = types.ModuleType("vllm.model_executor")
model_loader = types.ModuleType("vllm.model_executor.model_loader")
tensorizer = types.ModuleType("vllm.model_executor.model_loader.tensorizer")
class TensorizerConfig: # pragma: no cover - placeholder
def __init__(self, *args, **kwargs):
pass
tensorizer.TensorizerConfig = TensorizerConfig
model_loader.tensorizer = tensorizer
model_executor.model_loader = model_loader
vllm.model_executor = model_executor
sys.modules["vllm.model_executor"] = model_executor
sys.modules["vllm.model_executor.model_loader"] = model_loader
sys.modules["vllm.model_executor.model_loader.tensorizer"] = tensorizer
_install_torch_stub()
_install_vllm_stub()
+6
View File
@@ -0,0 +1,6 @@
# Test-only dependencies. vllm/torch are stubbed in conftest.py when absent,
# so the unit tests run on a plain CPU runner without the GPU image.
pytest>=8,<10
# get_engine_args() reads a vLLM-style config via PyYAML (a transitive vllm dep
# at runtime); install it explicitly here since vllm itself is stubbed.
pyyaml
+126
View File
@@ -0,0 +1,126 @@
"""Tests for HF cache path resolution and served-model-name decoupling.
Regression coverage for issue #310: when MODEL_NAME is served from a lowercased
HF cache dir, the cache resolver rewrites engine_args.model to a snapshot path.
The served model name must stay the original repo id, not the path.
"""
import os
import pytest
from src import engine_args
from src.engine_args import _resolve_cached_model_path, get_engine_args
MODEL = "Qwen/Qwen3.6-27B-FP8"
SNAPSHOT_HASH = "e89b16ebf1988b3d6befa7de50abc2d76f26eb09"
def _make_cache(root, folder_name, snapshot=SNAPSHOT_HASH):
"""Create a HF-style ``models--…/snapshots/<hash>/`` dir and return its path."""
snap_dir = os.path.join(root, folder_name, "snapshots", snapshot)
os.makedirs(snap_dir)
return snap_dir
def _is_case_sensitive_fs(path):
"""The lowercase-cache resolution only matters on case-sensitive filesystems.
On macOS (APFS, case-insensitive by default) ``models--Qwen--…`` and
``models--qwen--…`` collide, so the resolver always sees the exact-case dir
as present. Production runs on Linux (case-sensitive), which is what these
tests exercise.
"""
probe = os.path.join(path, "CaseProbe")
open(probe, "w").close()
try:
return not os.path.exists(os.path.join(path, "caseprobe"))
finally:
os.remove(probe)
requires_case_sensitive_fs = pytest.mark.skipif(
not _is_case_sensitive_fs(os.environ.get("TMPDIR", "/tmp")),
reason="lowercase HF cache resolution only applies on case-sensitive filesystems",
)
@pytest.fixture
def hf_cache(tmp_path, monkeypatch):
cache = tmp_path / "hub"
cache.mkdir()
monkeypatch.setenv("HUGGINGFACE_HUB_CACHE", str(cache))
# Make sure HF_HOME does not shadow the explicit cache dir during the test.
monkeypatch.delenv("HF_HOME", raising=False)
return cache
class TestResolveCachedModelPath:
def test_exact_case_dir_returns_repo_id(self, hf_cache):
_make_cache(str(hf_cache), "models--Qwen--Qwen3.6-27B-FP8")
assert _resolve_cached_model_path(MODEL) == MODEL
def test_no_cache_returns_repo_id(self, hf_cache):
assert _resolve_cached_model_path(MODEL) == MODEL
def test_absolute_path_passthrough(self, hf_cache):
path = "/runpod-volume/some/local/model"
assert _resolve_cached_model_path(path) == path
@requires_case_sensitive_fs
def test_lowercase_dir_returns_snapshot_path(self, hf_cache):
snap = _make_cache(str(hf_cache), "models--qwen--qwen3.6-27b-fp8")
assert _resolve_cached_model_path(MODEL) == snap
def test_lowercase_dir_without_snapshots_returns_repo_id(self, hf_cache):
# Dir exists but has no snapshots subdir -> nothing to resolve to.
os.makedirs(os.path.join(str(hf_cache), "models--qwen--qwen3.6-27b-fp8"))
assert _resolve_cached_model_path(MODEL) == MODEL
@requires_case_sensitive_fs
def test_lowercase_dir_picks_latest_snapshot(self, hf_cache):
folder = "models--qwen--qwen3.6-27b-fp8"
_make_cache(str(hf_cache), folder, snapshot="aaaa")
latest = _make_cache(str(hf_cache), folder, snapshot="zzzz")
assert _resolve_cached_model_path(MODEL) == latest
class TestGetEngineArgsServedName:
"""Issue #310: served name must be decoupled from the resolved on-disk path."""
@pytest.fixture(autouse=True)
def base_env(self, monkeypatch):
# Avoid the network branch in _resolve_max_model_len.
monkeypatch.setenv("MAX_NUM_BATCHED_TOKENS", "2048")
monkeypatch.delenv("SERVED_MODEL_NAME", raising=False)
# Don't pick up a stray vLLM config file from the environment.
monkeypatch.setenv("VLLM_CONFIG_FILE", "/nonexistent-vllm-config.yaml")
@requires_case_sensitive_fs
def test_served_name_is_repo_id_when_path_rewritten(self, hf_cache, monkeypatch):
snap = _make_cache(str(hf_cache), "models--qwen--qwen3.6-27b-fp8")
monkeypatch.setenv("MODEL_NAME", MODEL)
result = get_engine_args()
assert result.model == snap # weights load from the lowercase cache
assert result.served_model_name == MODEL # API still serves the repo id
def test_served_name_untouched_when_no_rewrite(self, hf_cache, monkeypatch):
_make_cache(str(hf_cache), "models--Qwen--Qwen3.6-27B-FP8")
monkeypatch.setenv("MODEL_NAME", MODEL)
result = get_engine_args()
assert result.model == MODEL
assert result.served_model_name is None
def test_explicit_served_name_not_overridden(self, hf_cache, monkeypatch):
_make_cache(str(hf_cache), "models--qwen--qwen3.6-27b-fp8")
monkeypatch.setenv("MODEL_NAME", MODEL)
monkeypatch.setenv("SERVED_MODEL_NAME", "custom-name")
result = get_engine_args()
assert result.served_model_name == "custom-name"