enginex-mlu370-compat: test_wrapper_e2e.py
This commit is contained in:
167
test_wrapper_e2e.py
Normal file
167
test_wrapper_e2e.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_wrapper_e2e.py —— 用平台真实命令形态端到端验证 vllm_wrapper.sh
|
||||
|
||||
背景:平台 bi-100 × vllm × text-generation 的 build-config 实际下发的是
|
||||
vllm serve /model --port 80 --served-model-name llm --max-model-len 4096 \
|
||||
--gpu-memory-utilization 0.9 --enforce-eager --trust-remote-code -tp 1
|
||||
因为 Dockerfile 把镜像里的 vllm 换成了本仓 wrapper,这条命令会流经 wrapper。
|
||||
本测试用一个"假 vllm_real"(把收到的参数原样打印)验证:
|
||||
1. wrapper 正确识别 serve 形态,不吞参数、不串位
|
||||
2. preflight 产出的修补参数被**追加**在原命令之后(不覆盖、不前置)
|
||||
3. 非 serve 子命令(如 --version)原样透传
|
||||
4. preflight 自己挂掉时不阻断启动(仍能 exec vllm_real)
|
||||
5. 模型目录里有真实缺陷时,参数确实被补上
|
||||
|
||||
不需要 GPU / vLLM / Docker。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
WRAPPER = os.path.join(HERE, 'vllm_wrapper.sh')
|
||||
PREFLIGHT = os.path.join(HERE, 'preflight.py')
|
||||
|
||||
PASS, FAIL = [], []
|
||||
|
||||
|
||||
def check(name, cond, detail=''):
|
||||
(PASS if cond else FAIL).append(name)
|
||||
print((' PASS ' if cond else ' FAIL ') + name + ((' -- ' + str(detail)) if (detail and not cond) else ''))
|
||||
|
||||
|
||||
def make_fake_preflight(dirpath, extra_args, fail=False):
|
||||
"""造一个假 preflight:按给定 extra_args 输出 JSON;fail=True 时非 0 退出。"""
|
||||
p = os.path.join(dirpath, 'preflight.py')
|
||||
body = [
|
||||
'import json, sys',
|
||||
'args = sys.argv[1:]',
|
||||
'out = ""',
|
||||
'if "--out" in args: out = args[args.index("--out") + 1]',
|
||||
'if ' + ('True' if fail else 'False') + ':',
|
||||
' sys.stderr.write("boom' + chr(92) + 'n"); sys.exit(3)',
|
||||
'payload = {"extra_args": ' + repr(extra_args) + ', "patches": ["stub"], "overlay": ""}',
|
||||
'if out:',
|
||||
' fh = open(out, "w")',
|
||||
' fh.write(json.dumps(payload))',
|
||||
' fh.close()',
|
||||
'print(json.dumps(payload))',
|
||||
]
|
||||
with io.open(p, 'w', encoding='utf-8', newline=chr(10)) as f:
|
||||
f.write(chr(10).join(body) + chr(10))
|
||||
return p
|
||||
|
||||
|
||||
def run_wrapper(model_dir, args, box, extra_args=None, fail_preflight=False):
|
||||
"""在隔离目录跑 wrapper。
|
||||
|
||||
box = 隔离目录;里面放:vllm_real(假)、preflight.py(假)、wrapper_local.sh。
|
||||
extra_args 由假 preflight 产出,用来验证 wrapper 的参数拼装。
|
||||
全部使用相对路径(cwd=box),规避 Windows 路径被 MSYS 吃掉。
|
||||
"""
|
||||
real = os.path.join(box, 'vllm_real')
|
||||
with io.open(real, 'w', encoding='utf-8', newline=chr(10)) as f:
|
||||
f.write('#!/bin/bash' + chr(10) + 'echo "VLLM_REAL_ARGS: $@"' + chr(10))
|
||||
os.chmod(real, 0o755)
|
||||
|
||||
make_fake_preflight(box, extra_args or [], fail=fail_preflight)
|
||||
|
||||
src = io.open(WRAPPER, encoding='utf-8', newline=chr(10)).read()
|
||||
local = os.path.join(box, 'wrapper_local.sh')
|
||||
with io.open(local, 'w', encoding='utf-8', newline=chr(10)) as f:
|
||||
f.write(src
|
||||
.replace('/usr/local/corex/lib64/python3/dist-packages/bin/vllm_real', './vllm_real')
|
||||
.replace('/opt/preflight.py', './preflight.py')
|
||||
.replace('/tmp/mhxc_preflight.json', './preflight.json')
|
||||
.replace('/tmp/mhxc_preflight.out', './preflight.out')
|
||||
.replace('/tmp/mhxc_preflight.err', './preflight.err'))
|
||||
os.chmod(local, 0o755)
|
||||
|
||||
env = dict(os.environ)
|
||||
env['PATH'] = box + os.pathsep + env.get('PATH', '')
|
||||
cmd = ['bash', 'wrapper_local.sh'] + args
|
||||
p = subprocess.run(cmd, capture_output=True, text=True, timeout=90, env=env, cwd=box)
|
||||
return p.returncode, p.stdout, p.stderr
|
||||
|
||||
|
||||
def parse_real_args(stdout):
|
||||
for l in stdout.splitlines():
|
||||
if l.startswith('VLLM_REAL_ARGS: '):
|
||||
return l[len('VLLM_REAL_ARGS: '):]
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
tmp = tempfile.mkdtemp(prefix='mhxc_e2e_')
|
||||
try:
|
||||
PLATFORM_CMD = ['serve', '/model', '--port', '80', '--served-model-name', 'llm',
|
||||
'--max-model-len', '4096', '--gpu-memory-utilization', '0.9',
|
||||
'--enforce-eager', '--trust-remote-code', '-tp', '1']
|
||||
EXTRA = ['--tokenizer', '/tmp/ov/tokenizer',
|
||||
'--chat-template', '/tmp/ov/tpl.jinja',
|
||||
'--hf-overrides', '{"architectures":["Qwen3_5MoeForCausalLM"]}']
|
||||
|
||||
print('[1] 平台真实命令 + preflight 产出三类补丁')
|
||||
rc, out, err = run_wrapper('/model', PLATFORM_CMD, tmp, extra_args=EXTRA)
|
||||
final = parse_real_args(out) or ''
|
||||
check('wrapper 正常退出', rc == 0, err[-300:])
|
||||
check('真 vllm 被调起', final != '', out[:200])
|
||||
orig = PLATFORM_CMD[2:]
|
||||
check('原命令参数全部保留', all(a in final.split() for a in orig), final)
|
||||
check('serve 与 /model 在最前', final.split()[:2] == ['serve', '/model'], final[:100])
|
||||
check('三类补丁都带上', all(a in final.split() for a in ['--tokenizer', '--chat-template', '--hf-overrides']), final)
|
||||
# 正确性要求不是"补丁必须在末尾",而是"平台原命令的参数序列不被拆乱":
|
||||
# 平台参数应以原有顺序连续出现,且补丁只作为追加项存在。
|
||||
toks = final.split()
|
||||
plat = PLATFORM_CMD[2:]
|
||||
letidx = toks.index(plat[0]) if plat[0] in toks else -1
|
||||
seq_ok = letidx >= 0
|
||||
if seq_ok:
|
||||
for k in range(len(plat)):
|
||||
if toks[letidx + k] != plat[k]:
|
||||
seq_ok = False
|
||||
break
|
||||
check('平台原命令参数序列完整且顺序不变', seq_ok, (letidx, final))
|
||||
head_ok = toks[0] == 'serve' and toks[1] == '/model'
|
||||
check('serve 与模型目录仍在最前(位置参数不被挤掉)', head_ok, final[:80])
|
||||
print(' 最终命令: ' + final[:260])
|
||||
|
||||
print('[2] 干净模型:preflight 无产出 → 命令与平台原始命令完全一致')
|
||||
rc2, out2, err2 = run_wrapper('/model', PLATFORM_CMD, tmp, extra_args=[])
|
||||
final2 = parse_real_args(out2) or ''
|
||||
check('无补丁时命令逐字一致', final2.strip() == ' '.join(PLATFORM_CMD).strip(), final2)
|
||||
|
||||
print('[3] 非 serve 子命令原样透传')
|
||||
rc3, out3, err3 = run_wrapper('/model', ['--version'], tmp, extra_args=EXTRA)
|
||||
check('--version 原样透传', (parse_real_args(out3) or '').strip() == '--version', out3[:200])
|
||||
|
||||
print('[4] preflight 挂掉时不阻断启动')
|
||||
rc4, out4, err4 = run_wrapper('/model', PLATFORM_CMD, tmp, fail_preflight=True)
|
||||
final4 = parse_real_args(out4) or ''
|
||||
check('仍能起服务且用原命令', rc4 == 0 and final4.strip() == ' '.join(PLATFORM_CMD).strip(), (rc4, final4[:150]))
|
||||
|
||||
print('[5] preflight 自己写 JSON 也能被正确读回(不依赖 --out 文件时的兜底)')
|
||||
rc5, out5, err5 = run_wrapper('/model', PLATFORM_CMD, tmp, extra_args=['--dtype', 'half'])
|
||||
check('单参数补丁也能拼上', '--dtype' in (parse_real_args(out5) or '').split(), out5[:200])
|
||||
|
||||
print('[6] bash 语法')
|
||||
p = subprocess.run(['bash', '-n', 'vllm_wrapper.sh'], capture_output=True, text=True, cwd=HERE)
|
||||
check('wrapper bash 语法正确', p.returncode == 0, p.stderr[:200])
|
||||
|
||||
print(chr(10) + '===== 结果: %d passed, %d failed =====' % (len(PASS), len(FAIL)))
|
||||
if FAIL:
|
||||
print('失败项: ' + ', '.join(FAIL))
|
||||
return 1
|
||||
return 0
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user