Files
vllm-omni/tests/edge/local/test_e2e_local_text.py
T
tzhouamandClaude Fable 5.1 4e3e8a1ebc [edge-infer] WIP snapshot: uncommitted M0/W1/W4/W5 edge work as of 2026-09-18
Verbatim copy of the edge/omni-edge working tree (tracked diffs + untracked
edge/local, tests/edge/local, docs/edge, pipeline files). Taken so that
vLLM-Omni v0.29.0rc1 can be merged on a branch without touching the
user's working tree. logs_edge/ and benchmark .npy outputs excluded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 09:47:29 +08:00

280 lines
11 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""End to end: the M0 acceptance criteria, executed against a real checkpoint.
This is the test that says the local text mode works, so it tests the thing the
roadmap actually asks for rather than a proxy for it:
* both startups -- device-visible and capability-masked -- reach a decision,
and a refusal is a pass, because "选择兼容方案或明确拒绝" is an either/or;
* the same fixed set of twelve prompts, each generating at least 128 tokens;
* TTFT and decode rate recorded per request;
* the budgeted peak is an upper bound on the measured one, and nothing OOMs;
* cancellation releases, and no event from the retired epoch is delivered.
It needs real weights and a real backend, so it skips rather than fails when
there is no checkpoint. Which one it runs is whatever this interpreter can
drive: the CUDA venv gets the bf16 build, the CPU venv gets the int8 one --
which is itself the point, since the int8 build is the one the CUDA plan
refuses. Override with ``VLLM_OMNI_LOCAL_TEST_MODEL``.
Roughly a minute on the reference laptop's GPU and three on its CPU. Marked
``local_model`` because it is not a unit test and should not run in a PR gate.
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
import pytest_asyncio
from vllm_omni.edge.local import (
LocalTextEngine,
PlanNotAdmitted,
acceptance_prompts,
plan_text_session,
)
from vllm_omni.edge.local.capabilities import FORMAT_INT8
# One event loop for the whole module. The engine fixture is module-scoped --
# loading 7.7 GB per test would be absurd -- and the asyncio primitives inside
# AsyncOmni belong to whichever loop created them. Leaving the tests on
# pytest-asyncio's default per-function loop means a cancel awaits a task whose
# backend cleanup lives on a different, no-longer-running loop, and it hangs
# there rather than failing.
pytestmark = [pytest.mark.local_model, pytest.mark.asyncio(loop_scope="module")]
MIN_TOKENS = 128
MAX_MODEL_LEN = 4096
# Checkpoints under the repo's models/ directory, by the platform that can run
# them. The int8 build is deliberately the CPU entry: Blackwell refuses it.
_CANDIDATES = {
"cuda": ("Spark-X2.5-4B", "Spark-X2.5-1.7B-int8"),
"cpu": ("Spark-X2.5-1.7B-int8", "Spark-X2.5-4B"),
}
def _models_root() -> Path:
# tests/edge/local/ -> vllm-omni/ -> the workspace that holds models/
return Path(__file__).resolve().parents[3].parent / "models"
def _platform() -> str:
from vllm.platforms import current_platform
return str(getattr(current_platform, "device_type", "") or "")
def _find_model() -> str:
override = os.environ.get("VLLM_OMNI_LOCAL_TEST_MODEL")
if override:
return override
root = _models_root()
for name in _CANDIDATES.get(_platform(), ()):
if (root / name / "config.json").is_file():
return str(root / name)
pytest.skip(
f"no Spark checkpoint for platform {_platform()!r} under {root}; "
"set VLLM_OMNI_LOCAL_TEST_MODEL to run this"
)
@pytest.fixture(scope="module")
def model_dir() -> str:
return _find_model()
@pytest.fixture(scope="module")
def plan(model_dir: str):
p = plan_text_session(model_dir, max_model_len=MAX_MODEL_LEN, max_num_seqs=1)
if not p.admitted:
pytest.skip(f"this host cannot run {model_dir}:\n{p.summary()}")
return p
@pytest_asyncio.fixture(scope="module", loop_scope="module")
async def engine(plan):
eng = LocalTextEngine(plan)
await eng.start()
try:
yield eng
finally:
await eng.close()
# ------------------------------------------------------------------ planning
def test_the_masked_startup_reaches_a_decision(model_dir):
"""The no-NVIDIA class must produce a plan or an explicit refusal -- never
a crash and never a silent substitution."""
masked = plan_text_session(
model_dir, max_model_len=MAX_MODEL_LEN, mask=frozenset({"gpu_discrete"})
)
if masked.admitted:
assert masked.selected.memory_pool == "host_ram"
assert masked.selected.kind != "gpu_discrete"
else:
assert masked.refusals
assert all(r.message and r.remedy for r in masked.refusals)
assert any("mask" in r.message for r in masked.refusals) or _platform() != "cuda"
def test_a_refused_plan_cannot_be_started(model_dir):
refused = plan_text_session(
model_dir, max_model_len=MAX_MODEL_LEN, mask=frozenset({"gpu_discrete", "cpu"})
)
assert not refused.admitted
with pytest.raises(PlanNotAdmitted):
LocalTextEngine(refused)
@pytest.mark.skipif(
not (_models_root() / "Spark-X2.5-1.7B-int8" / "config.json").is_file(),
reason="the int8 checkpoint is not present",
)
def test_int8_is_refused_on_cuda_and_admitted_on_cpu():
"""The refusal and its remedy, checked against the real checkpoint on
whichever backend this interpreter has."""
int8 = str(_models_root() / "Spark-X2.5-1.7B-int8")
p = plan_text_session(int8, max_model_len=MAX_MODEL_LEN)
assert p.manifest.weight_format == FORMAT_INT8
if _platform() == "cuda":
assert not p.admitted
formats = [r for r in p.refusals if r.code == "weight_format_unsupported"]
assert formats and "omni-cpu" in formats[0].remedy
else:
assert p.admitted
assert p.selected.device_id == "cpu"
# ------------------------------------------------------------------- loading
async def test_the_engine_loads_and_reports_where_it_landed(engine):
assert engine.load_seconds and engine.load_seconds > 0
placement = engine.report_placement()
configured = placement["configured"]
assert "error" not in configured
# Read back from the stage's own config, not echoed from the plan.
assert configured["device_type"] == _platform()
assert configured["max_model_len"] == MAX_MODEL_LEN
assert configured["enforce_eager"] is True
async def test_the_kv_pool_is_the_size_the_plan_asked_for(engine, plan):
"""An absolute budget, honoured -- not whatever was left inside a
utilisation fraction."""
configured = engine.report_placement()["configured"]
assert configured["kv_cache_memory_bytes"] == plan.kv_budget.bytes_total
# ---------------------------------------------------------- the twelve prompts
async def test_twelve_prompts_each_reach_128_tokens(engine):
prompts = acceptance_prompts()
assert len(prompts) == 12
records = []
for name, text in prompts:
session = engine.open_session()
request_id, stream = await engine.submit(
session, text, max_tokens=MIN_TOKENS, temperature=0.0, ignore_eos=True
)
seqs = []
async for event in stream:
if event.kind == "token":
seqs.append(event.seq)
assert event.epoch == session.epoch
assert event.error is None, f"{name}: {event.error}"
record = engine.records[request_id]
engine.close_session(session.session_id)
assert record.finished, f"{name} did not finish"
assert record.output_tokens >= MIN_TOKENS, f"{name} produced {record.output_tokens}"
assert record.ttft_s is not None and record.ttft_s > 0
assert record.decode_tok_per_s is not None and record.decode_tok_per_s > 0
# Incremental, in order, no repeats: seq is what detects a lost chunk.
assert seqs == sorted(seqs)
assert len(seqs) == len(set(seqs))
records.append(record)
# The long prompt crosses the 512 sliding window many times; if window
# rollover were broken it would be broken here and not on the short ones.
long_record = next(r for r in records if r.prompt_tokens and r.prompt_tokens > 2000)
assert long_record.output_tokens >= MIN_TOKENS
async def test_the_budget_bounded_the_measured_peak(engine, plan):
"""Admission's whole claim: the plan's number is an upper bound, proven
before anything allocated."""
measured = engine.measured_peak()
assert measured["budgeted_peak_bytes"] == plan.peak_bytes
attributable = measured["attributable_peak_bytes"]
assert attributable is not None and attributable > 0
assert attributable <= measured["budgeted_peak_bytes"], (
f"measured {attributable / 2**30:.2f} GiB exceeded the budgeted "
f"{measured['budgeted_peak_bytes'] / 2**30:.2f} GiB"
)
async def test_usage_separates_what_was_measured_from_what_was_estimated(engine):
usage = engine.report_usage()
by_purpose = {r["purpose"]: r for r in usage["reservations"]}
assert by_purpose["weights"]["evidence"] == "D"
assert by_purpose["kv_cache"]["evidence"] == "E"
# Nothing gets an actual it cannot support: only the weights line is
# bounded by a counter on this host.
bounded = [p for p, r in by_purpose.items() if r["actual_upper_bound_bytes"]]
assert bounded == ["weights"]
# ------------------------------------------------------------- cancellation
async def test_cancel_releases_and_delivers_nothing_from_the_retired_epoch(engine):
session = engine.open_session()
request_id, stream = await engine.submit(
session, acceptance_prompts()[0][1], max_tokens=4096, ignore_eos=True
)
seen = 0
async for event in stream:
if event.kind == "token":
seen += 1
if seen >= 8:
break
assert seen >= 8
report = await engine.cancel(request_id)
assert report["aborted_in_backend"]
assert report["producer_stopped"], "the producer task did not unwind"
assert report["inflight_after"] == 0
# The session's state is retired, not reused.
assert report["session_epoch_now"] == session.epoch + 1
leaked = []
while True:
event = await stream.get()
if event is None:
break
leaked.append(event)
assert leaked == [], f"{len(leaked)} events escaped the retired epoch"
async def test_a_retired_handle_cannot_be_resubmitted(engine):
session = engine.open_session()
request_id, stream = await engine.submit(session, "Hello", max_tokens=32)
async for _ in stream:
break
await engine.cancel(request_id)
with pytest.raises(RuntimeError, match="stale"):
await engine.submit(session, "Hello again", max_tokens=8)
async def test_the_engine_still_generates_after_a_cancel(engine):
"""A cancelled request must not poison the engine for the next one."""
session = engine.open_session()
request_id, stream = await engine.submit(
session, "The capital of Australia is", max_tokens=16, ignore_eos=True
)
async for _ in stream:
pass
record = engine.records[request_id]
assert record.finished and record.output_tokens >= 16