fix loop
This commit is contained in:
@@ -51,3 +51,4 @@ RUN mkdir -p /opt/vllm-shim/vllm/entrypoints/openai \
|
||||
/opt/vllm-shim/vllm/entrypoints/cli/__init__.py
|
||||
|
||||
ENV PYTHONPATH="/opt/vllm-shim:${PYTHONPATH}"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
vLLM shim with custom weights download.
|
||||
|
||||
Intercepts `python -m vllm.entrypoints.openai.api_server` so that
|
||||
if --model points to a URL (http/https), we download + extract it
|
||||
to a local cache dir, then replace --model with that local path
|
||||
before handing off to the real vLLM server.
|
||||
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
|
||||
@@ -15,7 +15,7 @@ import sys
|
||||
import subprocess
|
||||
import datetime
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
@@ -23,13 +23,17 @@ import urllib.request
|
||||
# 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)
|
||||
print(line, flush=True)
|
||||
try:
|
||||
with open(log_path, "a") as f:
|
||||
f.write(line + "\n")
|
||||
@@ -37,18 +41,19 @@ def log(msg: str):
|
||||
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).
|
||||
"""
|
||||
# Strip query string and fragment
|
||||
path = urllib.parse.urlparse(url).path
|
||||
# Check multi-part extensions first
|
||||
for ext in (".tar.gz", ".tar.bz2", ".tar.xz"):
|
||||
if path.endswith(ext):
|
||||
return ext.lstrip(".")
|
||||
# Single-part
|
||||
_, ext = os.path.splitext(path)
|
||||
mapping = {
|
||||
".tar": "tar",
|
||||
@@ -89,7 +94,6 @@ def _download_progress(block_num, block_size, total_size):
|
||||
return
|
||||
downloaded = block_num * block_size
|
||||
pct = min(downloaded * 100 // total_size, 100)
|
||||
# Only print every 10% to avoid spam
|
||||
if pct % 10 == 0 and pct > 0:
|
||||
mb_down = downloaded / (1024 * 1024)
|
||||
mb_total = total_size / (1024 * 1024)
|
||||
@@ -105,8 +109,6 @@ def extract_archive(archive_path: str, dest_dir: str, archive_type: str):
|
||||
elif archive_type == "tar.bz2":
|
||||
shutil.unpack_archive(archive_path, dest_dir, "bztar")
|
||||
elif archive_type == "tar.xz":
|
||||
# shutil.unpack_archive doesn't support xztar in all Pythons,
|
||||
# use subprocess for reliability
|
||||
subprocess.run(
|
||||
["tar", "-xJf", archive_path, "-C", dest_dir],
|
||||
check=True,
|
||||
@@ -125,13 +127,11 @@ 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 — doesn't matter how people
|
||||
compressed it, we find the safetensors.
|
||||
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
|
||||
# Fallback: if no safetensors found, use the old heuristic
|
||||
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"]
|
||||
@@ -146,10 +146,9 @@ def download_and_extract_model(url: str) -> str:
|
||||
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] # e.g. "model-v1.0"
|
||||
cache_key = os.path.splitext(url_filename)[0]
|
||||
local_dir = os.path.join(CACHE_DIR, cache_key)
|
||||
|
||||
# Already extracted?
|
||||
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}")
|
||||
@@ -163,13 +162,11 @@ def download_and_extract_model(url: str) -> str:
|
||||
f"Supported extensions: .tar, .tar.gz, .tgz, .tar.bz2, .tar.xz, .zip"
|
||||
)
|
||||
|
||||
# Download to a temp file in the cache dir
|
||||
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:
|
||||
# Clean up the archive file to save space
|
||||
if os.path.exists(tmp_archive):
|
||||
os.remove(tmp_archive)
|
||||
|
||||
@@ -178,13 +175,19 @@ def download_and_extract_model(url: str) -> str:
|
||||
|
||||
def parse_args(args):
|
||||
"""
|
||||
Parse argv, intercepting --model.
|
||||
If --model is a URL, download+extract and replace with local path.
|
||||
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]
|
||||
@@ -192,7 +195,7 @@ def parse_args(args):
|
||||
# --model=<value>
|
||||
if arg.startswith("--model="):
|
||||
value = arg.split("=", 1)[1]
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
if is_url(value):
|
||||
local_path = download_and_extract_model(value)
|
||||
result.append(f"--model={local_path}")
|
||||
model_replaced = True
|
||||
@@ -207,7 +210,7 @@ def parse_args(args):
|
||||
i += 1
|
||||
if i < len(args):
|
||||
value = args[i]
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
if is_url(value):
|
||||
local_path = download_and_extract_model(value)
|
||||
result.append(local_path)
|
||||
model_replaced = True
|
||||
@@ -216,15 +219,51 @@ def parse_args(args):
|
||||
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")
|
||||
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:]
|
||||
|
||||
@@ -233,9 +272,12 @@ def main():
|
||||
log(f" Invoked as: python -m {__name__} {' '.join(args)}")
|
||||
log("=" * 50)
|
||||
|
||||
# Intercept --model if it's a URL
|
||||
# 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
|
||||
vllm_cmd = [sys.executable, "-m", "vllm.entrypoints.openai.api_server"] + modified_args
|
||||
|
||||
|
||||
Reference in New Issue
Block a user