Files
vllm-omni/vllm_omni/edge/auto_profile.py
T
tzhouamandClaude Fable 5.1 37c0ea9a6b [Edge][WP8] Per-step overhead attribution, edge scheduling helpers, CPU thread sweep
- vllm_omni/edge/step_stats.py: process-local counters enabled by
  VLLM_OMNI_STEP_STATS_DIR (scheduler schedule/update_from_output, engine-core
  step, chunk hop enqueue->put / put->get / get, orchestrator dispatch), dumped
  per process and merged into an attribution table (share of the 80 ms frame).
  Hooks: stage_engine_core_proc (instrument after construction, dump at exit),
  chunk_transfer_adapter (put/get timing, put->get via the shm lock-file mtime),
  orchestrator dispatch loop. Bench: stream_latency_bench.py --step-stats.
- vllm_omni/edge/scheduling.py: playback slack, prefill-throttle decision and
  the no-preemption KV admission rule (exact bytes/token from the talker KV
  geometry); adapt.derive_overrides now sizes max_num_seqs with it and
  auto_profile passes the geometry read from the model config.
- Engine re-reads parallel_stage_init after apply_deploy_profile so the edge
  profile's orchestrator default takes effect (was read before the profile).
- benchmarks/tts/cpu_thread_sweep.py: CPU-only 8/16/32-thread sweep (+inductor)
  with per-step attribution.
- Tests: tests/core/test_edge_scheduling.py, tests/edge/test_step_stats.py,
  tests/benchmarks/test_cpu_thread_sweep.py, profile default test.

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

181 lines
6.6 KiB
Python

"""``deploy_profile="auto"``: materialize a hardware-adapted deploy YAML.
Chain: base deploy YAML (``edge/<model_type>.yaml`` if present, else the model
default) -> hardware-class overlay (``edge/hardware/<class>.yaml``) -> derived
overrides (:func:`vllm_omni.edge.adapt.derive_overrides`) -> cached calibration
(:mod:`vllm_omni.edge.calibrate`). The result is written to a temp file and
returned as a normal ``deploy_config`` path, so nothing downstream changes.
"""
from __future__ import annotations
import atexit
import copy
import json
import os
import tempfile
from pathlib import Path
from typing import Any
import yaml
from vllm.logger import init_logger
from vllm_omni.config.stage_config import _DEPLOY_DIR, resolve_deploy_yaml
from vllm_omni.edge import calibrate
from vllm_omni.edge.adapt import derive_overrides, estimate_weights_bytes
from vllm_omni.edge.hardware_probe import HardwareProfile, describe, hardware_class, load_profile
from vllm_omni.edge.scheduling import KVGeometry
logger = init_logger(__name__)
HARDWARE_DIR = _DEPLOY_DIR / "edge" / "hardware"
def _merge_stage_list(base_stages: list[dict[str, Any]], overlay_stages: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_id = {int(s.get("stage_id", i)): dict(s) for i, s in enumerate(base_stages)}
for s in overlay_stages or []:
sid = int(s.get("stage_id", 0))
target = by_id.setdefault(sid, {"stage_id": sid})
for k, v in s.items():
if isinstance(v, dict) and isinstance(target.get(k), dict):
target[k] = {**target[k], **v}
else:
target[k] = v
return [by_id[k] for k in sorted(by_id)]
def _connector_extra(doc: dict[str, Any]) -> dict[str, Any] | None:
connectors = doc.get("connectors")
if not isinstance(connectors, dict) or not connectors:
return None
first = next(iter(connectors.values()))
if not isinstance(first, dict):
return None
return first.setdefault("extra", {})
def apply_hardware_overlay(doc: dict[str, Any], hw_class: str) -> dict[str, Any]:
path = HARDWARE_DIR / f"{hw_class}.yaml"
if not path.exists():
return doc
with open(path, encoding="utf-8") as f:
overlay = yaml.safe_load(f) or {}
out = copy.deepcopy(doc)
if overlay.get("stages"):
out["stages"] = _merge_stage_list(out.get("stages", []), overlay["stages"])
if isinstance(overlay.get("connectors"), dict):
extra = _connector_extra(out)
ov_extra = _connector_extra(overlay)
if extra is not None and ov_extra:
extra.update(ov_extra)
for k, v in overlay.items():
if k not in ("stages", "connectors", "platforms", "base_config"):
out[k] = v
return out
def apply_derived_overrides(doc: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
out = copy.deepcopy(doc)
stage_updates = []
for sid, upd in overrides.get("stages", {}).items():
entry: dict[str, Any] = {"stage_id": int(sid)}
for k, v in upd.items():
if k == "gpu_memory_utilization" and v is None:
continue # keep the YAML fraction unless an absolute budget replaces it
entry[k] = v
stage_updates.append(entry)
out["stages"] = _merge_stage_list(out.get("stages", []), stage_updates)
for k, v in overrides.get("top", {}).items():
out[k] = v
extra = _connector_extra(out)
if extra is not None:
extra.update(overrides.get("connector_extra", {}))
return out
def apply_calibration(doc: dict[str, Any], cal: dict[str, Any] | None) -> dict[str, Any]:
if not cal:
return doc
out = copy.deepcopy(doc)
extra = _connector_extra(out)
if extra is not None:
extra.update(calibrate.connector_extra_from_calibration(cal))
return out
def _model_dir(model: str) -> str | None:
if os.path.isdir(model):
return model
try:
from huggingface_hub import snapshot_download
return snapshot_download(model, local_files_only=True)
except Exception:
return None
def load_kv_geometry(model_dir: str | Path | None) -> KVGeometry | None:
"""Talker KV geometry from ``<model_dir>/config.json`` (None when unavailable)."""
if not model_dir:
return None
cfg_path = Path(model_dir) / "config.json"
try:
with open(cfg_path, encoding="utf-8") as f:
return KVGeometry.from_hf_config(json.load(f))
except (OSError, ValueError):
return None
def materialize_auto_deploy(
model: str,
base_yaml: str | Path,
*,
profile: HardwareProfile | None = None,
out_dir: str | Path | None = None,
use_calibration: bool = True,
) -> tuple[str, dict[str, Any]]:
"""Build the adapted deploy YAML; returns ``(path, report)``."""
prof = profile or load_profile()
hw_class = hardware_class(prof)
doc = resolve_deploy_yaml(Path(base_yaml))
doc.pop("base_config", None)
# ``platforms:`` overlays are applied later by the config factory and would
# override the derived values (thread binding, budgets, eager switches);
# the hardware-class overlay + derivation supersede them in auto mode.
doc.pop("platforms", None)
doc = apply_hardware_overlay(doc, hw_class)
model_dir = _model_dir(model)
weights = estimate_weights_bytes(model_dir)
overrides = derive_overrides(
prof,
weights_bytes=weights,
n_stages=len(doc.get("stages", [])) or 2,
kv_geometry=load_kv_geometry(model_dir),
)
doc = apply_derived_overrides(doc, overrides)
cal = None
if use_calibration:
cal = calibrate.load_calibration(calibrate.hardware_fingerprint(prof, model))
doc = apply_calibration(doc, cal)
target_dir = Path(out_dir) if out_dir else None
if target_dir is not None:
target_dir.mkdir(parents=True, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="auto_", suffix=".yaml", dir=str(target_dir))
else:
fd, path = tempfile.mkstemp(prefix=f"omni_auto_{hw_class}_", suffix=".yaml")
atexit.register(lambda p=path: Path(p).unlink(missing_ok=True))
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(f"# auto-generated by vllm_omni.edge.auto_profile for {model}\n# {describe(prof)}\n")
yaml.safe_dump(doc, f, sort_keys=False)
report = {
"hardware_class": hw_class,
"profile": prof.to_dict(),
"base_yaml": str(base_yaml),
"weights_bytes": weights,
"overrides": overrides,
"calibration": cal,
"deploy_config": path,
}
logger.info("[auto_profile] %s -> %s (%s)", hw_class, path, "; ".join(overrides.get("notes", [])))
return path, report