ops: add isolated ASR deployment and safe GPU test handoff
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download a pinned official ModelScope snapshot; never replace mismatched files."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
MODEL = 'Qwen/Qwen3-ASR-1.7B'
|
||||
REVISION = 'a04930dbe5419bfee073f7cade734f572689a3a8'
|
||||
ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else '/home/www/qwen-vllm/asr/models/Qwen3-ASR-1.7B')
|
||||
API = f'https://modelscope.cn/api/v1/models/{MODEL}/repo'
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
ROOT.mkdir(parents=True, exist_ok=True)
|
||||
url = API + '/files?' + urllib.parse.urlencode({'Revision': REVISION, 'Recursive': 'true'})
|
||||
with urllib.request.urlopen(url, timeout=60) as response:
|
||||
listing = json.load(response)
|
||||
assert listing['Code'] == 200 and listing['Success'], listing
|
||||
files = [x for x in listing['Data']['Files'] if x['Type'] == 'blob' and x['Path'] != '.gitattributes']
|
||||
manifest = {'provider': 'ModelScope', 'model': MODEL, 'revision': REVISION, 'api': url, 'files': files}
|
||||
manifest_path = ROOT / 'DOWNLOAD_MANIFEST.json'
|
||||
if manifest_path.exists():
|
||||
old = json.loads(manifest_path.read_text())
|
||||
assert old == manifest, 'Existing manifest differs; preserve and abort'
|
||||
else:
|
||||
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n')
|
||||
for row in files:
|
||||
relative = Path(row['Path'])
|
||||
assert not relative.is_absolute() and '..' not in relative.parts
|
||||
path = ROOT / relative
|
||||
if path.exists():
|
||||
assert path.stat().st_size == row['Size'] and digest(path) == row['Sha256'], f'Existing file mismatch: {path}'
|
||||
print(json.dumps({'event': 'download_reused', 'path': str(path), 'sha256': row['Sha256'], 'bytes': row['Size']}), flush=True)
|
||||
continue
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial = path.with_name(path.name + '.partial')
|
||||
# A partial file is only a disposable incomplete transfer created by this script.
|
||||
target = API + '?' + urllib.parse.urlencode({'Revision': REVISION, 'FilePath': row['Path']})
|
||||
print(json.dumps({'event': 'download_started', 'file': row['Path'], 'bytes': row['Size'], 'revision': REVISION}), flush=True)
|
||||
h = hashlib.sha256()
|
||||
size = 0
|
||||
with urllib.request.urlopen(target, timeout=120) as source, partial.open('wb') as output:
|
||||
while True:
|
||||
chunk = source.read(8 * 1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
output.write(chunk)
|
||||
h.update(chunk)
|
||||
size += len(chunk)
|
||||
assert size == row['Size'] and h.hexdigest() == row['Sha256'], f'Download integrity mismatch: {path}'
|
||||
os.replace(partial, path)
|
||||
print(json.dumps({'event': 'download_verified', 'path': str(path), 'sha256': h.hexdigest(), 'bytes': size}), flush=True)
|
||||
config = json.loads((ROOT / 'config.json').read_text())
|
||||
assert config['architectures'] == ['Qwen3ASRForConditionalGeneration'], config['architectures']
|
||||
weights = json.loads((ROOT / 'model.safetensors.index.json').read_text())
|
||||
assert set(weights['weight_map'].values()) <= {x['Path'] for x in files}
|
||||
# Public model weights must remain readable through a read-only bind mount with cap_drop=ALL.
|
||||
# This does not change the private service .env or acceptance directory permissions.
|
||||
ROOT.chmod(0o755)
|
||||
manifest_path.chmod(0o644)
|
||||
for row in files:
|
||||
(ROOT / row['Path']).chmod(0o644)
|
||||
print(json.dumps({'event': 'model_ready', 'model': MODEL, 'revision': REVISION, 'manifest_sha256': digest(manifest_path), 'total_bytes': sum(x['Size'] for x in files), 'architectures': config['architectures'], 'model_type': config.get('model_type'), 'dtype': config.get('torch_dtype', config.get('dtype')), 'weight_metadata': weights['metadata']}), flush=True)
|
||||
Reference in New Issue
Block a user