Compare commits
18 Commits
custom-wei
...
deepseekv4
| Author | SHA1 | Date | |
|---|---|---|---|
| e6b204f23b | |||
| 3ce154170d | |||
| bda7dc7d28 | |||
| fa939de3b9 | |||
| b594775d07 | |||
| 14df07de28 | |||
| 6c89a6d6fd | |||
| 5f8682d00a | |||
| 4556945525 | |||
| 8520a27c06 | |||
| d8ec84210f | |||
| e5ab680045 | |||
| 05cbcf71f4 | |||
| 6de9103325 | |||
| 01c1ab46db | |||
| 7a27871402 | |||
| 4381ca1e3b | |||
| 4bd3b65fda |
47
Dockerfile
47
Dockerfile
@@ -1,8 +1,9 @@
|
||||
#FROM vllm/vllm-openai:v0.19.0-cu130
|
||||
FROM vllm/vllm-openai:cu130-nightly-x86_64
|
||||
#FROM vllm/vllm-openai:deepseekv4-cu130
|
||||
FROM vllm/vllm-openai:v0.20.0-cu130
|
||||
|
||||
# Fix the broken ass nightly build that forgot to include pandas
|
||||
RUN pip install --no-cache-dir pandas
|
||||
#RUN pip install --no-cache-dir pandas
|
||||
|
||||
# Install LMCache for KV cache offloading / sharing across nodes
|
||||
# Build with system CUDA 13.0 for Blackwell (B200)
|
||||
@@ -12,6 +13,7 @@ RUN apt-get update && apt-get install -y git \
|
||||
libcublas-dev-13-0 \
|
||||
libcurand-dev-13-0 \
|
||||
libcufft-dev-13-0 \
|
||||
cuda-nvrtc-dev-13-0 \
|
||||
libnvjitlink-dev-13-0 && \
|
||||
git clone https://github.com/biondizzle/LMCache.git /tmp/lmcache && \
|
||||
cd /tmp/lmcache && \
|
||||
@@ -19,36 +21,29 @@ RUN apt-get update && apt-get install -y git \
|
||||
CUDA_HOME=/usr/local/cuda \
|
||||
TORCH_CUDA_ARCH_LIST="10.0" \
|
||||
pip install --no-cache-dir --no-build-isolation . && \
|
||||
rm -rf /tmp/lmcache && export CACHE_BUSTER=1
|
||||
rm -rf /tmp/lmcache && export CACHE_BUSTER=2
|
||||
|
||||
ENV VLLM_DOCKER_BUILD_CONTEXT=1
|
||||
# Needs git to be installed before this can run
|
||||
#RUN curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm/v0.20.0/tools/install_deepgemm.sh | bash
|
||||
# Had to modify this
|
||||
COPY ./install_deepgemm.sh /opt/install_deepgemm.sh
|
||||
RUN chmod +x /opt/install_deepgemm.sh
|
||||
RUN /opt/install_deepgemm.sh
|
||||
|
||||
# Copy over nemotron reasonong parser
|
||||
COPY ./super_v3_reasoning_parser.py /opt/super_v3_reasoning_parser.py
|
||||
#COPY ./super_v3_reasoning_parser.py /opt/super_v3_reasoning_parser.py
|
||||
|
||||
# Copy over deepseek tool call parser with MTP fixes
|
||||
COPY deepseekv32_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/deepseekv32_tool_parser.py
|
||||
#COPY deepseekv32_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/deepseekv32_tool_parser.py
|
||||
#COPY deepseekv4_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/deepseekv4_tool_parser.py
|
||||
|
||||
# Copy over minimax tool call parser with kwargs fixes
|
||||
COPY minimax_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/minimax_tool_parser.py
|
||||
#COPY minimax_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/minimax_tool_parser.py
|
||||
|
||||
# Copy over minimax parsers with kwargs fixes
|
||||
COPY minimax_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/minimax_tool_parser.py
|
||||
COPY minimax_m2_parser.py /usr/local/lib/python3.12/dist-packages/vllm/parser/minimax_m2_parser.py
|
||||
#COPY minimax_tool_parser.py /usr/local/lib/python3.12/dist-packages/vllm/tool_parsers/minimax_tool_parser.py
|
||||
#COPY minimax_m2_parser.py /usr/local/lib/python3.12/dist-packages/vllm/parser/minimax_m2_parser.py
|
||||
|
||||
# Copy vLLM shim that intercepts --model to download custom weights from URLs
|
||||
COPY vllm_shim_module.py /opt/vllm-shim/vllm_shim_module.py
|
||||
|
||||
# Shadow `python -m vllm.*` invocations via PYTHONPATH
|
||||
# The shim masquerades as the vllm package so python -m vllm/entrypoints/openai/api_server
|
||||
# hits our interceptor first, which downloads weights then execs the real vLLM
|
||||
RUN mkdir -p /opt/vllm-shim/vllm/entrypoints/openai \
|
||||
/opt/vllm-shim/vllm/entrypoints/cli && \
|
||||
cp /opt/vllm-shim/vllm_shim_module.py /opt/vllm-shim/vllm/__main__.py && \
|
||||
cp /opt/vllm-shim/vllm_shim_module.py /opt/vllm-shim/vllm/entrypoints/openai/api_server.py && \
|
||||
cp /opt/vllm-shim/vllm_shim_module.py /opt/vllm-shim/vllm/entrypoints/cli/main.py && \
|
||||
touch /opt/vllm-shim/vllm/__init__.py \
|
||||
/opt/vllm-shim/vllm/entrypoints/__init__.py \
|
||||
/opt/vllm-shim/vllm/entrypoints/openai/__init__.py \
|
||||
/opt/vllm-shim/vllm/entrypoints/cli/__init__.py
|
||||
|
||||
ENV PYTHONPATH=/opt/vllm-shim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
# Fix DeepseekV4Config missing max_position_embeddings attr for RoPE standardization
|
||||
#COPY deepseek_v4.py /usr/local/lib/python3.12/dist-packages/vllm/transformers_utils/configs/deepseek_v4.py
|
||||
|
||||
73
deepseek_v4.py
Executable file
73
deepseek_v4.py
Executable file
@@ -0,0 +1,73 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
|
||||
class DeepseekV4Config(PretrainedConfig):
|
||||
model_type = "deepseek_v4"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vocab_size: int = 129024,
|
||||
hidden_size: int = 7168,
|
||||
intermediate_size: int = 18432,
|
||||
moe_intermediate_size: int = 1408,
|
||||
num_hidden_layers: int = 61,
|
||||
num_attention_heads: int = 128,
|
||||
num_key_value_heads: int = 128,
|
||||
n_routed_experts: int = 256,
|
||||
n_shared_experts: int = 1,
|
||||
num_experts_per_tok: int = 8,
|
||||
head_dim: int = 256,
|
||||
q_lora_rank: int = 1536,
|
||||
o_lora_rank: int = 1536,
|
||||
qk_rope_head_dim: int = 64,
|
||||
o_groups: int = 128,
|
||||
rms_norm_eps: float = 1e-6,
|
||||
rope_theta: float = 10000.0,
|
||||
rope_scaling: dict | None = None,
|
||||
max_position_embeddings: int = 163840,
|
||||
hidden_act: str = "silu",
|
||||
norm_topk_prob: bool = True,
|
||||
compress_ratios: list | None = None,
|
||||
compress_rope_theta: float = 10000.0,
|
||||
num_hash_layers: int = 0,
|
||||
hc_mult: float = 1.0,
|
||||
hc_eps: float = 1e-6,
|
||||
hc_sinkhorn_iters: int = 8,
|
||||
swiglu_limit: int = 0,
|
||||
index_topk: int = 0,
|
||||
num_nextn_predict_layers: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
self.vocab_size = vocab_size
|
||||
self.hidden_size = hidden_size
|
||||
self.intermediate_size = intermediate_size
|
||||
self.moe_intermediate_size = moe_intermediate_size
|
||||
self.num_hidden_layers = num_hidden_layers
|
||||
self.num_attention_heads = num_attention_heads
|
||||
self.num_key_value_heads = num_key_value_heads
|
||||
self.n_routed_experts = n_routed_experts
|
||||
self.n_shared_experts = n_shared_experts
|
||||
self.num_experts_per_tok = num_experts_per_tok
|
||||
self.head_dim = head_dim
|
||||
self.q_lora_rank = q_lora_rank
|
||||
self.o_lora_rank = o_lora_rank
|
||||
self.qk_rope_head_dim = qk_rope_head_dim
|
||||
self.o_groups = o_groups
|
||||
self.rms_norm_eps = rms_norm_eps
|
||||
self.rope_theta = rope_theta
|
||||
self.rope_scaling = rope_scaling
|
||||
self.max_position_embeddings = max_position_embeddings
|
||||
self.hidden_act = hidden_act
|
||||
self.norm_topk_prob = norm_topk_prob
|
||||
self.compress_ratios = compress_ratios or [1] * num_hidden_layers
|
||||
self.compress_rope_theta = compress_rope_theta
|
||||
self.num_hash_layers = num_hash_layers
|
||||
self.hc_mult = hc_mult
|
||||
self.hc_eps = hc_eps
|
||||
self.hc_sinkhorn_iters = hc_sinkhorn_iters
|
||||
self.swiglu_limit = swiglu_limit
|
||||
self.index_topk = index_topk
|
||||
self.num_nextn_predict_layers = num_nextn_predict_layers
|
||||
super().__init__(**kwargs)
|
||||
33
deepseekv4_tool_parser.py
Normal file
33
deepseekv4_tool_parser.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import regex as re
|
||||
|
||||
from vllm.tool_parsers.deepseekv32_tool_parser import DeepSeekV32ToolParser
|
||||
|
||||
|
||||
class DeepSeekV4ToolParser(DeepSeekV32ToolParser):
|
||||
"""
|
||||
DeepSeek V4 DSML tool parser.
|
||||
|
||||
V4 keeps the V3.2 DSML invoke/parameter grammar, but wraps tool calls in
|
||||
``<|DSML|tool_calls>`` instead of ``<|DSML|function_calls>``.
|
||||
"""
|
||||
|
||||
tool_call_start_token: str = "<|DSML|tool_calls>"
|
||||
tool_call_end_token: str = "</|DSML|tool_calls>"
|
||||
|
||||
def __init__(self, tokenizer, tools=None):
|
||||
super().__init__(tokenizer, tools)
|
||||
# The V3.2 __init__ overwrites the class-level start/end tokens
|
||||
# with instance attributes set to "function_calls". Re-override
|
||||
# them to the V4 "tool_calls" tags so _extract_content(),
|
||||
# _extract_invoke_regions(), etc. match the correct tags.
|
||||
self.tool_call_start_token = "<|DSML|tool_calls>"
|
||||
self.tool_call_end_token = "</|DSML|tool_calls>"
|
||||
self.tool_calls_start_token = self.tool_call_start_token
|
||||
# Recompile regexes with the V4 tool_calls tags instead of
|
||||
# the V3.2 function_calls tags that the base class hardcodes.
|
||||
self.tool_call_complete_regex = re.compile(
|
||||
r"<|DSML|tool_calls>(.*?)</|DSML|tool_calls>", re.DOTALL
|
||||
)
|
||||
121
install_deepgemm.sh
Normal file
121
install_deepgemm.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# Script to build and/or install DeepGEMM from source
|
||||
# Default: build and install immediately
|
||||
# Optional: build wheels to a directory for later installation (useful in multi-stage builds)
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
# Keep DEEPGEMM_GIT_REF in sync with cmake/external_projects/deepgemm.cmake
|
||||
DEEPGEMM_GIT_REPO="https://github.com/deepseek-ai/DeepGEMM.git"
|
||||
DEEPGEMM_GIT_REF="891d57b4db1071624b5c8fa0d1e51cb317fa709f"
|
||||
WHEEL_DIR=""
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--ref)
|
||||
if [[ -z "$2" || "$2" =~ ^- ]]; then
|
||||
echo "Error: --ref requires an argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
DEEPGEMM_GIT_REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--cuda-version)
|
||||
if [[ -z "$2" || "$2" =~ ^- ]]; then
|
||||
echo "Error: --cuda-version requires an argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
CUDA_VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--wheel-dir)
|
||||
if [[ -z "$2" || "$2" =~ ^- ]]; then
|
||||
echo "Error: --wheel-dir requires a directory path." >&2
|
||||
exit 1
|
||||
fi
|
||||
WHEEL_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --ref REF Git reference to checkout (default: $DEEPGEMM_GIT_REF)"
|
||||
echo " --cuda-version VER CUDA version (auto-detected if not provided)"
|
||||
echo " --wheel-dir PATH If set, build wheel into PATH but do not install"
|
||||
echo " -h, --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect CUDA version if not provided
|
||||
if [ -z "$CUDA_VERSION" ]; then
|
||||
if command -v nvcc >/dev/null 2>&1; then
|
||||
CUDA_VERSION=$(nvcc --version | grep "release" | sed -n 's/.*release \([0-9]\+\.[0-9]\+\).*/\1/p')
|
||||
echo "Auto-detected CUDA version: $CUDA_VERSION"
|
||||
else
|
||||
echo "Warning: Could not auto-detect CUDA version. Please specify with --cuda-version"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract major and minor version numbers
|
||||
CUDA_MAJOR="${CUDA_VERSION%%.*}"
|
||||
CUDA_MINOR="${CUDA_VERSION#"${CUDA_MAJOR}".}"
|
||||
CUDA_MINOR="${CUDA_MINOR%%.*}"
|
||||
echo "CUDA version: $CUDA_VERSION (major: $CUDA_MAJOR, minor: $CUDA_MINOR)"
|
||||
|
||||
# Check CUDA version requirement
|
||||
if [ "$CUDA_MAJOR" -lt 12 ] || { [ "$CUDA_MAJOR" -eq 12 ] && [ "$CUDA_MINOR" -lt 8 ]; }; then
|
||||
echo "Skipping DeepGEMM build/installation (requires CUDA 12.8+ but got ${CUDA_VERSION})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Preparing DeepGEMM build..."
|
||||
echo "Repository: $DEEPGEMM_GIT_REPO"
|
||||
echo "Reference: $DEEPGEMM_GIT_REF"
|
||||
|
||||
# Create a temporary directory for the build
|
||||
INSTALL_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$INSTALL_DIR"' EXIT
|
||||
|
||||
# Clone the repository
|
||||
git clone --recursive --shallow-submodules "$DEEPGEMM_GIT_REPO" "$INSTALL_DIR/deepgemm"
|
||||
pushd "$INSTALL_DIR/deepgemm"
|
||||
|
||||
# Checkout the specific reference
|
||||
git checkout "$DEEPGEMM_GIT_REF"
|
||||
|
||||
# Clean previous build artifacts
|
||||
# (Based on https://github.com/deepseek-ai/DeepGEMM/blob/main/install.sh)
|
||||
rm -rf -- build dist *.egg-info 2>/dev/null || true
|
||||
|
||||
# Build wheel
|
||||
echo "🏗️ Building DeepGEMM wheel..."
|
||||
python3 setup.py bdist_wheel
|
||||
|
||||
# If --wheel-dir was specified, copy wheels there and exit
|
||||
if [ -n "$WHEEL_DIR" ]; then
|
||||
mkdir -p "$WHEEL_DIR"
|
||||
cp dist/*.whl "$WHEEL_DIR"/
|
||||
echo "✅ Wheel built and copied to $WHEEL_DIR"
|
||||
popd
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Default behaviour: install built wheel
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
echo "Installing DeepGEMM wheel using uv..."
|
||||
uv pip install --system dist/*.whl
|
||||
else
|
||||
echo "Installing DeepGEMM wheel using pip..."
|
||||
python3 -m pip install dist/*.whl
|
||||
fi
|
||||
|
||||
popd
|
||||
echo "✅ DeepGEMM installation completed successfully"
|
||||
@@ -1,297 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
vLLM shim with custom weights download.
|
||||
|
||||
Intercepts `python -m vllm.entrypoints.openai.api_server` so that
|
||||
if --model or the positional model arg (after "serve") points to a URL,
|
||||
we download + extract it to a local cache dir, then replace it with
|
||||
the local path before handing off to the real vLLM server.
|
||||
|
||||
Supported archive formats (detected from URL extension):
|
||||
.tar, .tar.gz, .tgz, .tar.bz2, .tar.xz, .zip
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import datetime
|
||||
import shutil
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# Where to cache downloaded+extracted weights
|
||||
# Production stack mounts the PVC at /data — use a subdir so it persists across pod restarts
|
||||
CACHE_DIR = os.environ.get("VLLM_WEIGHTS_CACHE", "/data/weights")
|
||||
|
||||
# The shim dir that shadows the vllm package — must be stripped from PYTHONPATH
|
||||
# before exec'ing the real vLLM, otherwise we loop forever.
|
||||
SHIM_DIR = "/opt/vllm-shim"
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
"""Write to both stdout and the shim log file."""
|
||||
log_path = os.environ.get("VLLM_SHIM_LOG", "/tmp/vllm-shim.log")
|
||||
ts = datetime.datetime.now().isoformat()
|
||||
line = f"[{ts}] {msg}"
|
||||
print(line, flush=True)
|
||||
try:
|
||||
with open(log_path, "a") as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def is_url(value: str) -> bool:
|
||||
return value.startswith("http://") or value.startswith("https://")
|
||||
|
||||
|
||||
def detect_archive_type(url: str) -> str:
|
||||
"""
|
||||
Detect archive type from URL path extension.
|
||||
Returns one of: 'tar', 'tar.gz', 'tar.bz2', 'tar.xz', 'zip', or '' (unknown).
|
||||
"""
|
||||
path = urllib.parse.urlparse(url).path
|
||||
for ext in (".tar.gz", ".tar.bz2", ".tar.xz"):
|
||||
if path.endswith(ext):
|
||||
return ext.lstrip(".")
|
||||
_, ext = os.path.splitext(path)
|
||||
mapping = {
|
||||
".tar": "tar",
|
||||
".tgz": "tar.gz",
|
||||
".zip": "zip",
|
||||
}
|
||||
return mapping.get(ext.lower(), "")
|
||||
|
||||
|
||||
MAX_DOWNLOAD_RETRIES = int(os.environ.get("VLLM_SHIM_MAX_RETRIES", "5"))
|
||||
RETRY_DELAY_SECONDS = 5
|
||||
|
||||
|
||||
def download_file(url: str, dest: str):
|
||||
"""Download url to dest with retries and a progress indicator."""
|
||||
for attempt in range(1, MAX_DOWNLOAD_RETRIES + 1):
|
||||
try:
|
||||
log(f"Downloading {url} -> {dest} (attempt {attempt}/{MAX_DOWNLOAD_RETRIES})")
|
||||
urllib.request.urlretrieve(url, dest, reporthook=_download_progress)
|
||||
log(f"Download complete: {dest}")
|
||||
return
|
||||
except Exception as e:
|
||||
log(f"Download attempt {attempt} failed: {e}")
|
||||
if os.path.exists(dest):
|
||||
os.remove(dest)
|
||||
if attempt < MAX_DOWNLOAD_RETRIES:
|
||||
wait = RETRY_DELAY_SECONDS * attempt
|
||||
log(f"Retrying in {wait}s...")
|
||||
time.sleep(wait)
|
||||
else:
|
||||
log(f"All {MAX_DOWNLOAD_RETRIES} download attempts failed")
|
||||
raise
|
||||
|
||||
|
||||
def _download_progress(block_num, block_size, total_size):
|
||||
"""Simple download progress callback."""
|
||||
if total_size <= 0:
|
||||
return
|
||||
downloaded = block_num * block_size
|
||||
pct = min(downloaded * 100 // total_size, 100)
|
||||
if pct % 10 == 0 and pct > 0:
|
||||
mb_down = downloaded / (1024 * 1024)
|
||||
mb_total = total_size / (1024 * 1024)
|
||||
sys.stdout.write(f"\r {pct}% ({mb_down:.0f}/{mb_total:.0f} MB)")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def extract_archive(archive_path: str, dest_dir: str, archive_type: str):
|
||||
"""Extract archive to dest_dir based on archive_type."""
|
||||
log(f"Extracting {archive_path} ({archive_type}) -> {dest_dir}")
|
||||
if archive_type == "tar.gz" or archive_type == "tgz":
|
||||
shutil.unpack_archive(archive_path, dest_dir, "gztar")
|
||||
elif archive_type == "tar.bz2":
|
||||
shutil.unpack_archive(archive_path, dest_dir, "bztar")
|
||||
elif archive_type == "tar.xz":
|
||||
subprocess.run(
|
||||
["tar", "-xJf", archive_path, "-C", dest_dir],
|
||||
check=True,
|
||||
)
|
||||
elif archive_type == "tar":
|
||||
shutil.unpack_archive(archive_path, dest_dir, "tar")
|
||||
elif archive_type == "zip":
|
||||
shutil.unpack_archive(archive_path, dest_dir, "zip")
|
||||
else:
|
||||
raise ValueError(f"Unsupported archive type: {archive_type}")
|
||||
log(f"Extraction complete: {dest_dir}")
|
||||
|
||||
|
||||
def find_model_dir(extract_dir: str) -> str:
|
||||
"""
|
||||
After extraction, find the directory containing the actual model weights.
|
||||
Walks the tree looking for .safetensors files and returns the directory
|
||||
that contains one. This handles archives with extra parent dirs,
|
||||
nested structures, or flat extractions.
|
||||
"""
|
||||
for root, dirs, files in os.walk(extract_dir):
|
||||
if any(f.endswith(".safetensors") for f in files):
|
||||
return root
|
||||
log("WARNING: No .safetensors files found in extracted archive, falling back to single-dir heuristic")
|
||||
entries = [e for e in os.listdir(extract_dir)
|
||||
if not e.startswith(".") and e != "__MACOSX"]
|
||||
if len(entries) == 1 and os.path.isdir(os.path.join(extract_dir, entries[0])):
|
||||
return os.path.join(extract_dir, entries[0])
|
||||
return extract_dir
|
||||
|
||||
|
||||
def download_and_extract_model(url: str) -> str:
|
||||
"""
|
||||
Download a model from URL, extract it, and return the local path.
|
||||
Uses a cache keyed by URL filename to avoid re-downloading.
|
||||
"""
|
||||
url_filename = os.path.basename(urllib.parse.urlparse(url).path)
|
||||
cache_key = os.path.splitext(url_filename)[0]
|
||||
local_dir = os.path.join(CACHE_DIR, cache_key)
|
||||
|
||||
if os.path.isdir(local_dir) and os.listdir(local_dir):
|
||||
model_path = find_model_dir(local_dir)
|
||||
log(f"Using cached weights: {model_path}")
|
||||
return model_path
|
||||
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
archive_type = detect_archive_type(url)
|
||||
if not archive_type:
|
||||
raise ValueError(
|
||||
f"Cannot determine archive type from URL: {url}\n"
|
||||
f"Supported extensions: .tar, .tar.gz, .tgz, .tar.bz2, .tar.xz, .zip"
|
||||
)
|
||||
|
||||
tmp_archive = os.path.join(CACHE_DIR, url_filename + ".tmp")
|
||||
try:
|
||||
download_file(url, tmp_archive)
|
||||
extract_archive(tmp_archive, local_dir, archive_type)
|
||||
finally:
|
||||
if os.path.exists(tmp_archive):
|
||||
os.remove(tmp_archive)
|
||||
|
||||
return find_model_dir(local_dir)
|
||||
|
||||
|
||||
def parse_args(args):
|
||||
"""
|
||||
Parse argv, intercepting --model and positional model args.
|
||||
Production stack invokes: python -m vllm.entrypoints.openai.api_server serve <model-url> ...
|
||||
The model can appear as:
|
||||
- --model <url>
|
||||
- --model=<url>
|
||||
- A positional arg after "serve" subcommand
|
||||
If the value is a URL, download+extract and replace with local path.
|
||||
Returns the modified argv list.
|
||||
"""
|
||||
result = []
|
||||
i = 0
|
||||
model_replaced = False
|
||||
saw_serve = False
|
||||
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
|
||||
# --model=<value>
|
||||
if arg.startswith("--model="):
|
||||
value = arg.split("=", 1)[1]
|
||||
if is_url(value):
|
||||
local_path = download_and_extract_model(value)
|
||||
result.append(f"--model={local_path}")
|
||||
model_replaced = True
|
||||
else:
|
||||
result.append(arg)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# --model <value>
|
||||
if arg == "--model":
|
||||
result.append(arg)
|
||||
i += 1
|
||||
if i < len(args):
|
||||
value = args[i]
|
||||
if is_url(value):
|
||||
local_path = download_and_extract_model(value)
|
||||
result.append(local_path)
|
||||
model_replaced = True
|
||||
else:
|
||||
result.append(value)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# "serve" subcommand — next positional is the model
|
||||
if arg == "serve":
|
||||
result.append(arg)
|
||||
saw_serve = True
|
||||
i += 1
|
||||
# The next non-flag argument is the model
|
||||
if i < len(args) and not args[i].startswith("-") and is_url(args[i]):
|
||||
local_path = download_and_extract_model(args[i])
|
||||
result.append(local_path)
|
||||
model_replaced = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Positional model arg when there's no "serve" subcommand
|
||||
# (first non-flag arg if no serve seen)
|
||||
if not arg.startswith("-") and not saw_serve and not model_replaced:
|
||||
if is_url(arg):
|
||||
local_path = download_and_extract_model(arg)
|
||||
result.append(local_path)
|
||||
model_replaced = True
|
||||
i += 1
|
||||
continue
|
||||
|
||||
result.append(arg)
|
||||
i += 1
|
||||
|
||||
if model_replaced:
|
||||
log("Model URL was replaced with local path")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def strip_shim_from_pythonpath():
|
||||
"""
|
||||
Remove the shim directory from PYTHONPATH so that when we exec the
|
||||
real vLLM, Python doesn't find our shadow package again (infinite loop).
|
||||
"""
|
||||
pp = os.environ.get("PYTHONPATH", "")
|
||||
parts = [p for p in pp.split(":") if p != SHIM_DIR]
|
||||
new_pp = ":".join(parts)
|
||||
if new_pp != pp:
|
||||
os.environ["PYTHONPATH"] = new_pp
|
||||
log(f"Stripped {SHIM_DIR} from PYTHONPATH (was: {pp!r}, now: {new_pp!r})")
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
# Determine which vllm module was actually invoked so we exec the real one
|
||||
# (could be vllm.entrypoints.cli.main, vllm.entrypoints.openai.api_server, etc.)
|
||||
invoked_module = __name__ # e.g. "vllm.entrypoints.cli.main" or "vllm.entrypoints.openai.api_server"
|
||||
log("=" * 50)
|
||||
log("vLLM Custom Weights Shim")
|
||||
log(f" Invoked as: python -m {invoked_module} {' '.join(args)}")
|
||||
log("=" * 50)
|
||||
|
||||
# Intercept --model / positional model if it's a URL
|
||||
modified_args = parse_args(args)
|
||||
|
||||
# Strip our shim from PYTHONPATH so the real vLLM resolves correctly
|
||||
strip_shim_from_pythonpath()
|
||||
|
||||
# Build the real vLLM command using the same module that was invoked
|
||||
vllm_cmd = [sys.executable, "-m", invoked_module] + modified_args
|
||||
|
||||
log(f"Launching vLLM: {' '.join(vllm_cmd)}")
|
||||
|
||||
# Exec into vLLM — replace this process so signals flow through cleanly
|
||||
os.execvp(vllm_cmd[0], vllm_cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# Also run if imported as a module (some invocation paths just import the file)
|
||||
main()
|
||||
Reference in New Issue
Block a user