From 7019bb26f92453ef4a3e49513ff33e9014cb8fbd Mon Sep 17 00:00:00 2001
From: Wenyao Gao
Date: Wed, 9 Sep 2026 01:39:09 -0700
Subject: [PATCH 01/29] feat: add prepared H3 hardware sites and serving
matrices
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
接入已准备的 H100、H200 与 B200 站点,扩展每档请求数并单独记录全部分配 GPU。AMD 运行时和跨硬件实测尚待完成。
---
.github/workflows/e2e-tests.yml | 22 ++++++++
.github/workflows/h3-video.yml | 52 ++++++++++++++----
experimental/video-generation/README.md | 38 +++++++++----
experimental/video-generation/README_zh.md | 23 ++++++--
experimental/video-generation/ci.py | 53 ++++++++++++++-----
.../video-generation/evaluator/mvp_power.py | 2 +-
.../evaluator/mvp_serving_smoke.py | 23 ++++----
.../video-generation/runtime-entry.example.sh | 11 ++--
.../video-generation/tests/test_ci.py | 38 +++++++++++++
.../tests/test_mvp_serving_smoke.py | 14 ++---
perf-changelog.yaml | 6 +++
11 files changed, 225 insertions(+), 57 deletions(-)
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 8f0e2a0b32..afb656cee8 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -12,6 +12,16 @@ on:
required: false
type: boolean
default: false
+ h3-cluster:
+ description: "Prepared H3 site: h200-dgxc, h100-dgxc, or b200-nscale"
+ required: false
+ type: string
+ default: h200-dgxc
+ h3-site-config:
+ description: "Pinned runner-local H3 site JSON; empty uses existing H200 configuration"
+ required: false
+ type: string
+ default: ""
h3-reuse-run-ids:
description: "Reprocess one or two accepted H3 CI runs (comma-separated); no new H3 generation"
required: false
@@ -122,6 +132,16 @@ on:
required: false
type: boolean
default: false
+ h3-cluster:
+ description: "Prepared H3 site: h200-dgxc, h100-dgxc, or b200-nscale"
+ required: false
+ type: string
+ default: h200-dgxc
+ h3-site-config:
+ description: "Pinned runner-local H3 site JSON; empty uses existing H200 configuration"
+ required: false
+ type: string
+ default: ""
h3-reuse-run-ids:
description: "Reprocess one or two accepted H3 CI runs (comma-separated); no new H3 generation"
required: false
@@ -234,6 +254,8 @@ jobs:
actions: read
uses: ./.github/workflows/h3-video.yml
with:
+ cluster: ${{ inputs.h3-cluster }}
+ site-config: ${{ inputs.h3-site-config }}
source-run-ids: ${{ inputs.h3-reuse-run-ids }}
inventory-run-id: ${{ inputs.h3-inventory-run-id }}
diff --git a/.github/workflows/h3-video.yml b/.github/workflows/h3-video.yml
index f0d30bfef4..f0dfafb4f2 100644
--- a/.github/workflows/h3-video.yml
+++ b/.github/workflows/h3-video.yml
@@ -4,6 +4,17 @@ run-name: H3 video smoke - ${{ github.ref_name }}
on:
workflow_dispatch:
inputs:
+ cluster:
+ description: "Prepared H3 hardware site"
+ required: false
+ type: choice
+ options: [h200-dgxc, h100-dgxc, b200-nscale]
+ default: h200-dgxc
+ site-config:
+ description: "Pinned runner-local site JSON; empty uses the existing H200 configuration"
+ required: false
+ type: string
+ default: ""
inventory-run-id:
description: "Reuse a successful hardware inventory for CPU-only export"
required: false
@@ -16,6 +27,14 @@ on:
default: ""
workflow_call:
inputs:
+ cluster:
+ required: false
+ type: string
+ default: h200-dgxc
+ site-config:
+ required: false
+ type: string
+ default: ""
inventory-run-id:
required: false
type: string
@@ -42,6 +61,7 @@ jobs:
outputs:
priority: ${{ steps.queue.outputs.priority }}
queue-token: ${{ steps.queue.outputs.queue-token }}
+ gpu-model: ${{ steps.queue.outputs.gpu-model }}
steps:
- name: Authorize manual repository execution
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -68,13 +88,24 @@ jobs:
- name: Prepare native queue identity
id: queue
env:
- H3_SITE_CONFIG: ${{ vars.H3_SITE_CONFIG }}
+ H3_SITE_CONFIG: ${{ inputs.site-config || vars.H3_SITE_CONFIG }}
+ H3_CLUSTER: ${{ inputs.cluster }}
PRIORITY_ENABLED: ${{ vars.PRIORITY_SCHEDULER_ENABLED }}
NODE_SLOTS_ENABLED: ${{ vars.NODE_SLOT_SCHEDULER_ENABLED }}
H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
H3_INVENTORY_RUN_ID: ${{ inputs.inventory-run-id }}
run: |
set -euo pipefail
+ case "$H3_CLUSTER" in
+ h200-dgxc) echo 'gpu-model=H200' >> "$GITHUB_OUTPUT" ;;
+ h100-dgxc) echo 'gpu-model=H100' >> "$GITHUB_OUTPUT" ;;
+ b200-nscale) echo 'gpu-model=B200' >> "$GITHUB_OUTPUT" ;;
+ *) echo 'Unsupported H3 hardware site.' >&2; exit 1 ;;
+ esac
+ if [[ "$H3_CLUSTER" != h200-dgxc && ( -n "$H3_SOURCE_RUN_IDS" || -n "$H3_INVENTORY_RUN_ID" ) ]]; then
+ echo 'Historical inventory reuse remains specific to the H200 inventory contract.' >&2
+ exit 1
+ fi
if [[ -n "$H3_INVENTORY_RUN_ID" && ( -z "$H3_SOURCE_RUN_IDS" || ! "$H3_INVENTORY_RUN_ID" =~ ^[1-9][0-9]{0,19}$ ) ]]; then
echo 'Hardware reuse needs one inventory run ID and explicit source executions.' >&2
exit 1
@@ -88,7 +119,7 @@ jobs:
echo 'H3 priority scheduling requires node-slot admission for nodes:1.' >&2
exit 1
fi
- scored=$(printf '%s' '[{"runner":"cluster:h200-dgxc","framework":"sglang","node-count":1}]' |
+ scored=$(jq -nc --arg runner "cluster:$H3_CLUSTER" '[{runner:$runner,framework:"sglang","node-count":1}]' |
uv run --no-project --with pyyaml --python 3.12 utils/ci_priority.py)
echo "priority=$(jq -r '.[0].priority' <<<"$scored")" >> "$GITHUB_OUTPUT"
python3 - <<'PY'
@@ -105,18 +136,19 @@ jobs:
outputs:
mode: ${{ steps.execute.outputs.mode }}
if: ${{ inputs.inventory-run-id == '' }}
- name: p${{ needs.prepare.outputs.priority }} | ${{ inputs.source-run-ids != '' && 'H3 H200 hardware inventory' || 'H3 video H200 smoke' }}
+ name: p${{ needs.prepare.outputs.priority }} | ${{ inputs.source-run-ids != '' && 'H3 H200 hardware inventory' || format('H3 video {0} smoke', needs.prepare.outputs.gpu-model) }}
runs-on: >-
${{ fromJSON(
vars.PRIORITY_SCHEDULER_ENABLED == 'true' &&
- format('["self-hosted","cluster:h200-dgxc","nodes:1",{0},{1}]',
+ format('["self-hosted","cluster:{2}","nodes:1",{0},{1}]',
toJSON(format('ci-job-{0}-{1}', needs.prepare.outputs.priority, needs.prepare.outputs.queue-token)),
- toJSON(format('ci-attempt-{0}', github.run_attempt))) ||
- '["cluster:h200-dgxc"]'
+ toJSON(format('ci-attempt-{0}', github.run_attempt)), inputs.cluster) ||
+ format('["cluster:{0}"]', inputs.cluster)
) }}
- timeout-minutes: 105
+ timeout-minutes: 255
env:
- H3_SITE_CONFIG: ${{ vars.H3_SITE_CONFIG }}
+ H3_SITE_CONFIG: ${{ inputs.site-config || vars.H3_SITE_CONFIG }}
+ H3_CLUSTER: ${{ inputs.cluster }}
H3_SOURCE_SHA: ${{ github.sha }}
H3_REPOSITORY: ${{ github.repository }}
H3_RUN_ID: ${{ github.run_id }}
@@ -143,9 +175,11 @@ jobs:
python3 - <<'PY'
import json, os, sys
sys.path.insert(0, 'experimental/video-generation')
- from ci import validate_config
+ from ci import DEFAULT_SITE, validate_config
with open(os.environ['H3_SITE_CONFIG']) as stream:
config = validate_config(json.load(stream))
+ if config.get('site', DEFAULT_SITE)['cluster'] != os.environ['H3_CLUSTER']:
+ raise ValueError('Prepared site does not match the selected CI hardware cluster')
with open(os.environ['GITHUB_OUTPUT'], 'a') as stream:
stream.write('mode=' + config['mode'] + '\n')
PY
diff --git a/experimental/video-generation/README.md b/experimental/video-generation/README.md
index 043f6c881e..715e9a542f 100644
--- a/experimental/video-generation/README.md
+++ b/experimental/video-generation/README.md
@@ -3,7 +3,7 @@
**English** | [中文](README_zh.md)
This experimental lane runs the existing H3 supervisor inside InferenceX CI on
-SemiAnalysis H200 resources. Its first target is a bounded same-build smoke:
+prepared SemiAnalysis NVIDIA resources. Its first target is a bounded same-build smoke:
original generated MP4s, full video/audio validation, measured requests, and
verified cleanup. It does not publish a native InferenceX database/UI result.
Successful executions also publish a [versioned frontend result contract](RESULTS.md)
@@ -29,7 +29,9 @@ record, entry-only script and SHA256, container Python, frozen supervisor spec
and SHA256, resource limits, task identity, and optional prior allocation receipts.
The spec must record actual compute/model-use approval. Selecting the manual H3
route requests only that configured, reviewed workload; dispatch accepts no shell
-command, model path, arbitrary config contents, or alternate provider.
+command, model path, arbitrary config contents, or alternate provider. The
+optional `h3-site-config` dispatch input selects an existing reviewed JSON file;
+`h3-cluster` must match its declared site before any allocation.
The runner requires Python 3.11+, Git, the Slurm tools, and access to the declared
shared paths. PyAV/NumPy and the pinned H3 runtime/model must already be prepared
@@ -54,12 +56,16 @@ locks and telemetry continue to use the assigned UUIDs.
The adapter recovers task-owned allocation receipts before allocating. Imported
receipts must match task identity, Unix ownership, and the scheduler's exact
-allocation identity; ambiguous intent blocks another submission. The fixed site
-is `main` / `sa-shared`. A new exclusive allocation reserves eight GPUs;
+allocation identity; ambiguous intent blocks another submission. The default H200 site
+is `main` / `sa-shared`. An explicit `site` records the cluster, partition, account,
+and expected GPU model. Currently admitted clusters are `h200-dgxc`, `h100-dgxc`,
+and `b200-nscale`; admission is implementation support, not a completed hardware run.
+`resources.allocated_gpus` records the full allocation separately from participating
+`resources.gpus`; set it to eight on whole-node H100. A paired allocation reserves eight GPUs;
the example step selects four GPUs, 32 CPUs, and 1 TiB of host memory. The pinned
four-rank loader exceeded 256 GiB during CPU weight staging; 1 TiB is a tested
working allowance, not a measured minimum. Charge reserved capacity.
-`resources.minutes` is the total allocation cap, at most 90 minutes. The step
+`resources.minutes` is the total allocation cap, at most 240 minutes. The step
reserves five minutes for outer cleanup, and the supervisor plus ten minutes
must fit the allocation. For example: 90-minute allocation, 85-minute step,
75-minute supervisor. Reused allocations need enough remaining time. Preserve
@@ -153,11 +159,11 @@ Serving runs require an uncalibrated policy. CPU fixtures test the harness;
they do not establish H3 concurrency support or hardware performance.
Set the reviewed site configuration to `"mode": "serving-smoke"` for the bounded
-C1/C2/C4 matrix. Its plan must contain exactly four measured requests, plus
-explicit warmups. It boots the baseline runtime once per cell in one allocation,
-runs twelve measured requests total, and stops after a failed cell. Allocation
-GPU count equals the requested count; ordinary paired smoke keeps its existing
-allocation behavior. `serving-smoke.json`, `gpu/cN/` and `report/index.html` retain
+C1/C2/C4 matrix. Its plan contains 4–200 measured requests per cell, plus
+explicit warmups. It boots the baseline runtime once per cell in one allocation
+and stops after a failed cell. Allocation GPU count defaults to the participating
+count; `resources.allocated_gpus` declares a larger required allocation explicitly.
+Ordinary paired smoke keeps its existing allocation behavior. `serving-smoke.json`, `gpu/cN/` and `report/index.html` retain
the matrix, original request/media/telemetry evidence and playable report. An
interrupted attempt is counted separately from an unstarted request. This mode
skips the paired frontend export and cannot claim regression acceptance.
@@ -202,3 +208,15 @@ bash -n runtime-entry.example.sh
[Test H3 Video](../../.github/workflows/test-h3-video.yml) runs these CPU checks
and workflow linting on relevant changes. They make no model, scheduler, or GPU
calls. Real CI execution and artifact inspection are separate acceptance evidence.
+
+## Cross-hardware serving matrices
+
+The existing `serving-smoke` mode accepts 4–200 measured requests per concurrency
+from `plan.cases × plan.repetitions`; concurrency remains 1, 2, and 4. Twenty per
+cell produces sixty measured requests plus three separate warmups when
+`warmup_runs: 1`. Failures and unstarted requests remain in the declared denominator.
+Freeze identical model files, prompts/seeds, video settings, and quality requirements
+across sites. Record different runtime builds and deployment topology explicitly.
+Small-sample percentiles are preliminary; this closed-loop sweep does not establish
+sustainable open-loop arrival capacity. AMD runtime/device admission is not yet
+implemented. Missing sites and measurements must not be represented by fixture data.
diff --git a/experimental/video-generation/README_zh.md b/experimental/video-generation/README_zh.md
index a44fe887b1..a7b2e9db8b 100644
--- a/experimental/video-generation/README_zh.md
+++ b/experimental/video-generation/README_zh.md
@@ -46,12 +46,12 @@ launcher 当作进入脚本。
设备枚举不一致时启动失败;归属锁和遥测仍使用分配的 UUID。
adapter 在申请资源前恢复本任务的分配收据。导入的收据必须匹配任务标识、Unix
-所有者和调度器中的精确分配身份;提交结果不明确时禁止重复申请。固定站点是
-`main` / `sa-shared`。新建独占分配预留八张 GPU;示例 step 使用四张
+所有者和调度器中的精确分配身份;提交结果不明确时禁止重复申请。默认 H200 站点是
+`main` / `sa-shared`,其他站点通过 `site` 明确记录,见下文。新建独占分配预留八张 GPU;示例 step 使用四张
GPU、32 个 CPU 和 1 TiB 主机内存。固定版本的四 rank 加载器在 CPU 暂存权重时
超过了 256 GiB;1 TiB 是实际运行验证过的额度,并非测得的最低需求。预算按预留容量计算。
-`resources.minutes` 是整个分配的时间上限,最多 90 分钟。step 为外层清理
+`resources.minutes` 是整个分配的时间上限,最多 240 分钟。step 为外层清理
预留五分钟,supervisor 的上限加十分钟必须不超过分配上限。例如:分配
90 分钟、step 85 分钟、supervisor 75 分钟。复用分配必须有足够剩余时间。
保留准备好的 rootfs,仅清理属于本次任务的进程和 step,仅释放本次执行拥有的
@@ -164,3 +164,20 @@ bash -n runtime-entry.example.sh
artifact 检查是独立的验收证据。
编译缓存保留在持久化存储中,不上传为测量证据。
+
+## 跨硬件服务测量
+
+现有 `serving-smoke` 模式支持每档 4–200 条测量请求,数量由
+`plan.cases × plan.repetitions` 决定;并发档位保持 1、2、4。每档 20 条
+产生 60 条测量请求,`warmup_runs: 1` 时另有 3 条独立预热。失败和未启动
+请求保留在预先确定的分母中。跨硬件固定相同模型文件、提示词/种子、视频规格
+和质量要求,明确记录运行时构建与部署拓扑差异。小样本分位数属于初步结果,
+闭环并发扫描不能证明持续开放到达负载下的服务容量。
+
+默认配置保持 H200 的 `main` / `sa-shared`。可选 `site` 明确记录 `cluster`、
+`partition`、`account` 和 `gpu_model`;当前允许 `h200-dgxc`、`h100-dgxc`、
+`b200-nscale`。允许配置不等于实测通过。通过 `h3-cluster` 选择站点,
+`h3-site-config` 指向 runner 上已准备并审核的 JSON 文件;二者必须匹配。
+`resources.allocated_gpus` 单独记录全部分配卡数,`resources.gpus` 记录实际参与卡数;
+H100 整节点分配需记录 8 张卡。总分配上限提高至 240 分钟,保留原有清理余量。
+AMD 的运行时与设备接入尚未实现,不可用的硬件或指标不能用 fixture 数据代替。
diff --git a/experimental/video-generation/ci.py b/experimental/video-generation/ci.py
index dcbdea3009..1ab802ba24 100644
--- a/experimental/video-generation/ci.py
+++ b/experimental/video-generation/ci.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""InferenceX H200 Slurm adapter for a prepared, trusted H3 runtime.
+"""InferenceX Slurm adapter for a prepared, trusted H3 runtime.
No SSH, image import, dependency installation, or model download. The submit
host and compute node share workspace.host, mounted at workspace.container by
@@ -28,6 +28,8 @@
PARTITION = "main"
ACCOUNT = "sa-shared"
+DEFAULT_SITE = {"cluster": "h200-dgxc", "partition": PARTITION, "account": ACCOUNT, "gpu_model": "H200"}
+NVIDIA_CLUSTERS = {"h100-dgxc": "H100", "h200-dgxc": "H200", "b200-nscale": "B200"}
NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,79}")
SHA = re.compile(r"[0-9a-f]{64}")
IDENTITY = ("JobId", "JobName", "Comment", "WorkDir", "Account", "Partition", "UserId")
@@ -84,9 +86,15 @@ def host_path(config: dict, container_path: str) -> Path:
def validate_config(config: dict) -> dict:
- need(set(config) == {"schema_version", "task_id", "workspace", "runtime", "spec", "resources", "allocation_receipts", "mode"}, "Unknown or missing site configuration fields")
+ required = {"schema_version", "task_id", "workspace", "runtime", "spec", "resources", "allocation_receipts", "mode"}
+ need(required <= set(config) <= required | {"site"}, "Unknown or missing site configuration fields")
+ site = config.get("site", DEFAULT_SITE)
+ need(isinstance(site, dict) and set(site) == set(DEFAULT_SITE), "Invalid site fields")
+ need(site["cluster"] in NVIDIA_CLUSTERS and site["gpu_model"] == NVIDIA_CLUSTERS[site["cluster"]], "Unsupported cluster or GPU model")
+ need(all(isinstance(site[key], str) and NAME.fullmatch(site[key]) for key in ("account", "partition")), "Explicit scheduler account and partition required")
need(config["schema_version"] == 1 and NAME.fullmatch(config["task_id"]), "Invalid schema_version/task_id")
need(config["mode"] in {"smoke", "regression", "serving-smoke"}, "mode must be smoke, regression or serving-smoke")
+ need(site["cluster"] == "h200-dgxc" or config["mode"] == "serving-smoke", "Cross-hardware sites currently require serving-smoke; paired export remains H200-only")
need(set(config["workspace"]) == {"host", "container"}, "Invalid workspace mapping")
for value in config["workspace"].values():
path = absolute(value)
@@ -99,15 +107,23 @@ def validate_config(config: dict) -> dict:
need(set(config["spec"]) == {"path", "sha256"} and SHA.fullmatch(config["spec"]["sha256"]), "Pinned prepared spec required")
absolute(config["spec"]["path"])
resources = config["resources"]
- need(set(resources) == {"gpus", "cpus", "memory_gb", "minutes"}, "Invalid resource request")
- for key, low, high in (("gpus", 1, 8), ("cpus", 1, 128), ("memory_gb", 1, 1400), ("minutes", 10, 90)):
- need(type(resources[key]) is int and low <= resources[key] <= high, "Resource outside bounded H200 budget: " + key)
+ required_resources = {"gpus", "cpus", "memory_gb", "minutes"}
+ need(required_resources <= set(resources) <= required_resources | {"allocated_gpus"}, "Invalid resource request")
+ for key, low, high in (("gpus", 1, 8), ("cpus", 1, 128), ("memory_gb", 1, 1400), ("minutes", 10, 240)):
+ need(type(resources[key]) is int and low <= resources[key] <= high, "Resource outside bounded GPU budget: " + key)
+ reserved = allocation_gpus(config)
+ need(type(reserved) is int and resources["gpus"] <= reserved <= 8, "Allocated GPU budget must cover participating GPUs")
+ need(config["mode"] == "serving-smoke" or reserved == 8, "Paired measurements require a full eight-GPU allocation")
need(isinstance(config["allocation_receipts"], list), "allocation_receipts must be a list")
for path in config["allocation_receipts"]:
absolute(path)
return config
+def allocation_gpus(config: dict) -> int:
+ return config["resources"].get("allocated_gpus", config["resources"]["gpus"] if config["mode"] == "serving-smoke" else 8)
+
+
def environment() -> dict[str, str]:
# Slurm defaults inherited from the runner must not change this request.
# The payload receives an explicit environment allowlist at the srun edge.
@@ -136,7 +152,8 @@ def verify_identity(receipt: dict, record: dict, task_id: str) -> None:
expected = receipt["identity"]
need(set(expected) == set(IDENTITY), "Incomplete allocation ownership receipt")
need(all(record.get(key) == expected[key] for key in IDENTITY), "Slurm allocation identity differs from receipt")
- need(record["Account"] == ACCOUNT and record["Partition"] == PARTITION, "Allocation is not in the SemiAnalysis H200 pool")
+ site = receipt.get("site", DEFAULT_SITE)
+ need(record["Account"] == site["account"] and record["Partition"] == site["partition"], "Allocation differs from its receipted scheduler pool")
need(re.fullmatch(r"[^()]+\(" + str(os.getuid()) + r"\)", record["UserId"]), "Allocation Unix owner differs")
@@ -175,6 +192,9 @@ def recover(config: dict, result_root: Path, *, node: str | None = None) -> dict
continue
record = job_record(job)
verify_identity(receipt, record, config["task_id"])
+ if receipt.get("site", DEFAULT_SITE) != config.get("site", DEFAULT_SITE):
+ reasons.append({"job_id": job, "reason": "allocation belongs to a different hardware site"})
+ continue
state = record["JobState"]
if state in TERMINAL:
reasons.append({"job_id": job, "reason": state})
@@ -203,11 +223,15 @@ def allocate(config: dict, run_dir: Path, *, node: str | None = None) -> dict:
need(NAME.fullmatch(job_name), "Invalid runner/job name")
comment = "h3:" + nonce
request = config["resources"]
+ site = config.get("site", DEFAULT_SITE)
intent = {"task_id": config["task_id"], "job_name": job_name, "comment": comment,
"work_dir": str(run_dir), "user_id": os.getuid(), "created_at": now()}
write(run_dir / "allocation-intent.json", intent)
- placement = ["--gres=gpu:" + str(request["gpus"])] if config["mode"] == "serving-smoke" else ["--exclusive", "--gres=gpu:8"]
- argv = ["salloc", "--no-shell", "--no-bell", "--partition=" + PARTITION, "--account=" + ACCOUNT,
+ reserved = allocation_gpus(config)
+ placement = ["--gres=gpu:" + str(reserved)]
+ if reserved == 8:
+ placement.insert(0, "--exclusive")
+ argv = ["salloc", "--no-shell", "--no-bell", "--partition=" + site["partition"], "--account=" + site["account"],
"--nodes=1", "--ntasks=1", *placement,
"--cpus-per-task=" + str(request["cpus"]), "--mem=" + str(request["memory_gb"]) + "G",
"--time=" + str(request["minutes"]), "--immediate=30",
@@ -223,9 +247,9 @@ def allocate(config: dict, run_dir: Path, *, node: str | None = None) -> dict:
job = granted[0]
# Save the expected identity before querying, so a lost query can be recovered.
user = command(["id", "-un"]).strip()
- receipt = {"task_id": config["task_id"], "created_at": now(), "identity": {
+ receipt = {"task_id": config["task_id"], "created_at": now(), "site": site, "identity": {
"JobId": job, "JobName": job_name, "Comment": comment, "WorkDir": str(run_dir),
- "Account": ACCOUNT, "Partition": PARTITION, "UserId": f"{user}({os.getuid()})"}}
+ "Account": site["account"], "Partition": site["partition"], "UserId": f"{user}({os.getuid()})"}}
write(run_dir / "allocation.json", receipt)
need(result.returncode == 0, "Slurm returned a failure after granting an allocation; reconcile receipt")
return receipt
@@ -400,9 +424,9 @@ def launch(config: dict, output: Path) -> int:
results.mkdir(parents=True, exist_ok=True)
control.mkdir(parents=True, exist_ok=True)
run_dir = results / f"github-{run_id}-{attempt}"
- reserved_gpus = config["resources"]["gpus"] if config["mode"] == "serving-smoke" else 8
+ reserved_gpus = allocation_gpus(config)
state = {"schema_version": 1, "task_id": config["task_id"], "run_id": run_id, "run_attempt": attempt,
- "source_sha": sha, "started_at": now(), "phase": "preparing", "mode": config["mode"],
+ "source_sha": sha, "started_at": now(), "phase": "preparing", "mode": config["mode"], "site": config.get("site", DEFAULT_SITE),
"ci_accepted": False, "release_qualified": False, "persistent_output": str(run_dir),
"excluded_cache_paths": list(CACHE_PATHS),
"ci": {"repository": os.environ.get("GITHUB_REPOSITORY"),
@@ -486,7 +510,8 @@ def launch(config: dict, output: Path) -> int:
write(run_dir / "manifest.json", {"schema_version": 1, "task_id": config["task_id"],
"git_commit": sha, "ci": state["ci"], "run_id": run_id, "run_attempt": attempt,
"slurm_allocation": receipt, "runtime": config["runtime"], "prepared_spec": config["spec"],
- "workload_plan": spec.get("plan"), "mode": config["mode"], "resources": state["resources"], "exit_code": code,
+ "workload_plan": spec.get("plan"), "mode": config["mode"], "site": config.get("site", DEFAULT_SITE),
+ "resources": state["resources"], "exit_code": code,
"evidence": {path: digest(run_dir / path) for path in links if (run_dir / path).is_file()},
"artifact_checksums": "SHA256SUMS", "excluded_persistent_caches": list(CACHE_PATHS)})
collect(run_dir, output)
@@ -505,7 +530,7 @@ def enter(run_dir: Path) -> None:
need(digest(config["runtime"]["entry"]) == config["runtime"]["entry_sha256"], "Entry changed on compute node")
argv = ["/bin/bash", config["runtime"]["entry"], config["runtime"]["python"],
str(mapped(config, Path(__file__).parent) / "ci.py"), "--inside", str(mapped(config, run_dir))]
- os.execv(argv[0], argv)
+ os.execve(argv[0], argv, {**os.environ, "H3_EXPECTED_GPU_MODEL": config.get("site", DEFAULT_SITE)["gpu_model"]})
def workload_complete(verified: dict) -> bool:
diff --git a/experimental/video-generation/evaluator/mvp_power.py b/experimental/video-generation/evaluator/mvp_power.py
index 9926367dd6..3cc02f6b0c 100644
--- a/experimental/video-generation/evaluator/mvp_power.py
+++ b/experimental/video-generation/evaluator/mvp_power.py
@@ -332,7 +332,7 @@ def analyze_power(role: dict, run: dict, samples: list[dict], events: list[dict]
"submit_to_observed_provider_terminal; excludes_client_download_and_decode"),
"peak": "maximum_observed_sensor_sample_in_window; not_instantaneous_electrical_peak",
"energy_per_valid_clip": "sum_generation_energy_including_failed_or_invalid_completed_attempts_divided_by_technically_valid_clips",
- "sensor": "nvidia-smi power.draw; H200 NVML trailing_one_second_average; phase_edges_have_sensor_averaging_uncertainty",
+ "sensor": "nvidia-smi power.draw; hardware_sensor_averaging_not_calibrated; phase_edges_have_sensor_averaging_uncertainty",
"clock_agreement_limit_seconds": _CLOCK_TOLERANCE_SECONDS,
"legacy_journal_agreement_limit_seconds": _LEGACY_JOURNAL_TOLERANCE_SECONDS},
"clock_alignment": {"utc_minus_monotonic_seconds": offset, "observed_offset_spread_seconds": spread},
diff --git a/experimental/video-generation/evaluator/mvp_serving_smoke.py b/experimental/video-generation/evaluator/mvp_serving_smoke.py
index 7a08483b9f..d877e2aebe 100644
--- a/experimental/video-generation/evaluator/mvp_serving_smoke.py
+++ b/experimental/video-generation/evaluator/mvp_serving_smoke.py
@@ -20,8 +20,9 @@
def validate_spec(spec: dict) -> dict:
frozen = gpu.validate_gpu_job(spec)
- if not frozen.get("serving") or len(frozen["plan"]["cases"]) * frozen["plan"]["repetitions"] != 4:
- raise ValueError("serving smoke requires exactly four measured requests per configuration")
+ count = len(frozen["plan"]["cases"]) * frozen["plan"]["repetitions"]
+ if not frozen.get("serving") or not 4 <= count <= 200:
+ raise ValueError("serving matrix requires 4–200 measured requests per configuration")
return frozen
@@ -48,9 +49,9 @@ def _report(root: Path, matrix: dict) -> None:
(report / "index.html").write_text(
''
f'H3 serving smokeH3 serving smoke
'
- 'One hardware configuration; four measured requests at each concurrency. Warmups are separate. '
+ f'
One hardware configuration; {matrix["requests_per_configuration"]} measured requests at each concurrency. Warmups are separate. '
'Latency is submit → downloaded media for technically valid clips. Throughput is valid clips / delivery wall seconds. '
- 'Four samples do not establish P90/P95 or sustainable capacity. Failed and unstarted requests remain counted.
'
+ 'Percentiles from small samples are preliminary and do not establish sustainable capacity. Failed and unstarted requests remain counted.
'
'Download summary and raw-evidence links
'
'| Concurrency | Status | Scheduled | '
'Attempted | Valid | Failed | Not started | Delivery median (s) | Valid clips/s | '
@@ -64,13 +65,15 @@ def run_matrix(spec: dict, root: Path) -> dict:
from .mvp_power import analyze_power
spec = validate_spec(spec)
+ count = len(spec["plan"]["cases"]) * spec["plan"]["repetitions"]
deadline = time.monotonic() + spec["limits"]["job_seconds"]
matrix = {"schema_version": "1.0.0", "bundle_type": "h3_serving_smoke_matrix", "status": "running",
- "started_at": gpu._now(), "plan": spec["plan"], "runtime": spec["baseline"], "gpu_uuids": spec["gpu_uuids"],
- "scheduled": 12, "warmup_per_configuration": spec["plan"]["warmup_runs"],
+ "started_at": gpu._now(), "plan": spec["plan"], "runtime": spec["baseline"], "server": spec["server"], "gpu_uuids": spec["gpu_uuids"],
+ "scheduled": count * len(CONCURRENCIES), "requests_per_configuration": count,
+ "warmup_per_configuration": spec["plan"]["warmup_runs"],
"ci_accepted": False, "release_qualified": False,
"cells": [{"concurrency": concurrency, "status": "not_started", "verified": False,
- "completion": {"scheduled": 4, "attempted": 0, "completed": 0, "valid": 0, "failed": 4, "not_started": 4, "unfinished": 0}}
+ "completion": {"scheduled": count, "attempted": 0, "completed": 0, "valid": 0, "failed": count, "not_started": count, "unfinished": 0}}
for concurrency in CONCURRENCIES]}
path = root / "serving-smoke.json"
gpu._write(path, matrix)
@@ -93,19 +96,19 @@ def run_matrix(spec: dict, root: Path) -> dict:
if run_path.is_file():
raw = gpu._read(run_path)
cell["run"] = {"path": run_path.relative_to(root).as_posix(), "sha256": gpu._hash(run_path)}
- summary = _summary(raw["records"], 4, raw["measurement"]["wall_seconds"])
+ summary = _summary(raw["records"], count, raw["measurement"]["wall_seconds"])
cell["completion"] = {key: summary[key] for key in ("scheduled", "completed", "valid", "failed")}
finished = {r["slot_id"] for r in raw["records"] if r["phase"] == "measurement" and r["attempted"]}
journal = directory / "baseline/events.jsonl"
events = [json.loads(line) for line in journal.read_text().splitlines()] if journal.exists() else []
started = finished | {event["slot_id"] for event in events if event["event"] == "attempt_started" and event["slot_id"].startswith("measurement-")}
- cell["completion"].update(attempted=len(started), not_started=4-len(started), unfinished=len(started-finished))
+ cell["completion"].update(attempted=len(started), not_started=count-len(started), unfinished=len(started-finished))
gpu._write(path, matrix)
verified = verify_measurement_job(directory, deadline=deadline, require_success=True, serving_smoke=True)
run, role = verified["runs"]["baseline"], receipt["roles"]["baseline"]
cell.update(status="complete", verified=True, metrics={
"client_ready_p50_seconds": run["serving"]["client_ready_latency_seconds"]["p50"],
- "valid_clips_per_second": _summary(run["records"], 4, run["measurement"]["wall_seconds"])["valid_clips_per_second"],
+ "valid_clips_per_second": _summary(run["records"], count, run["measurement"]["wall_seconds"])["valid_clips_per_second"],
"serving": run["serving"], "measurement": run["measurement"],
})
samples = [json.loads(line) for line in (directory / role["telemetry_path"]).read_text().splitlines()]
diff --git a/experimental/video-generation/runtime-entry.example.sh b/experimental/video-generation/runtime-entry.example.sh
index e8421a13a7..2c52d35f0a 100644
--- a/experimental/video-generation/runtime-entry.example.sh
+++ b/experimental/video-generation/runtime-entry.example.sh
@@ -34,7 +34,7 @@ for part in value.split(','):
elif 0 <= bounds[0] <= bounds[1] < 8:
ids.extend(range(bounds[0], bounds[1] + 1))
else:
- raise SystemExit('GPU range outside the single H200 node')
+ raise SystemExit('GPU range outside the single eight-GPU node')
if len(ids) != len(set(ids)) or not ids or any(i >= 8 for i in ids):
raise SystemExit('Invalid global GPU assignment')
devices = {}
@@ -52,10 +52,13 @@ PY
)
h3_gpu_rows=$(nvidia-smi --id="$h3_gpu_uuids" --query-gpu=uuid,name --format=csv,noheader)
H3_ASSIGNED_GPU_UUIDS=$(python3 - "$h3_gpu_rows" "$h3_gpu_uuids" <<'PY'
-import csv, re, sys
+import csv, os, re, sys
rows = list(csv.reader(sys.argv[1].splitlines()))
-if not rows or any(len(row) != 2 or 'H200' not in row[1] or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}', row[0].strip()) for row in rows):
- raise SystemExit('Assigned hardware is not a physical H200 GPU set')
+expected = os.environ.get('H3_EXPECTED_GPU_MODEL', 'H200')
+if expected not in {'H100', 'H200', 'B200'}:
+ raise SystemExit('Unsupported expected NVIDIA GPU model')
+if not rows or any(len(row) != 2 or not re.search(r'\b' + expected + r'\b', row[1]) or not re.fullmatch(r'GPU-[0-9a-fA-F-]{36}', row[0].strip()) for row in rows):
+ raise SystemExit('Assigned hardware does not match expected physical ' + expected + ' GPUs')
observed = [row[0].strip() for row in rows]
if sorted(observed) != sorted(sys.argv[2].split(',')):
raise SystemExit('NVIDIA query differs from assigned physical UUIDs')
diff --git a/experimental/video-generation/tests/test_ci.py b/experimental/video-generation/tests/test_ci.py
index c8420d13b3..aff9769069 100644
--- a/experimental/video-generation/tests/test_ci.py
+++ b/experimental/video-generation/tests/test_ci.py
@@ -38,6 +38,44 @@ def save_receipt(root, receipt):
return path
+def test_h100_site_keeps_full_allocation_separate_from_participating_gpus(tmp_path, monkeypatch):
+ monkeypatch.setenv("RUNNER_NAME", "h3-test-runner")
+ cfg = config(tmp_path)
+ cfg.update(mode="serving-smoke", site={"cluster": "h100-dgxc", "partition": "hpc-gpu-1", "account": "customer", "gpu_model": "H100"})
+ cfg["resources"].update(gpus=4, allocated_gpus=8)
+ ci.validate_config(cfg)
+ commands = []
+ def run(argv, **kwargs):
+ commands.append(argv)
+ return SimpleNamespace(stdout="salloc: Granted job allocation 123", stderr="", returncode=0)
+ monkeypatch.setattr(ci.subprocess, "run", run)
+ monkeypatch.setattr(ci, "command", lambda argv: "tester")
+ receipt = ci.allocate(cfg, tmp_path)
+ assert "--partition=hpc-gpu-1" in commands[0] and "--account=customer" in commands[0]
+ assert "--exclusive" in commands[0] and "--gres=gpu:8" in commands[0]
+ assert receipt["site"] == cfg["site"]
+ _, record = allocation(tmp_path)
+ record.update(receipt["identity"])
+ ci.verify_identity(receipt, record, cfg["task_id"])
+ step = ci.step_argv(cfg, receipt, record, tmp_path, tmp_path)
+ assert "--gpus-per-task=4" in step
+ record["Account"] = "other"
+ with pytest.raises(ValueError, match="identity differs"):
+ ci.verify_identity(receipt, record, cfg["task_id"])
+
+
+@pytest.mark.parametrize("change", [
+ {"site": {"cluster": "h100-dgxc", "partition": "hpc-gpu-1", "account": "customer", "gpu_model": "H200"}},
+ {"site": {"cluster": "unknown", "partition": "main", "account": "customer", "gpu_model": "H200"}},
+ {"resources": {"gpus": 4, "allocated_gpus": 2, "cpus": 32, "memory_gb": 512, "minutes": 90}},
+])
+def test_invalid_hardware_or_allocation_budget_is_rejected(tmp_path, change):
+ cfg = config(tmp_path)
+ cfg.update(change)
+ with pytest.raises(ValueError):
+ ci.validate_config(cfg)
+
+
@pytest.mark.parametrize("mode", ["smoke", "serving-smoke"])
def test_allocation_submits_from_receipted_work_directory(tmp_path, monkeypatch, mode):
run_dir = tmp_path / "results"
diff --git a/experimental/video-generation/tests/test_mvp_serving_smoke.py b/experimental/video-generation/tests/test_mvp_serving_smoke.py
index 2cefef538f..ca6c49b78d 100644
--- a/experimental/video-generation/tests/test_mvp_serving_smoke.py
+++ b/experimental/video-generation/tests/test_mvp_serving_smoke.py
@@ -38,9 +38,10 @@ def test_single_runtime_smoke_cannot_be_accepted_as_paired_evidence(spec, tmp_pa
@pytest.mark.parametrize("fail_second", [False, True])
-def test_matrix_preserves_twelve_requests_without_doubling_roles(spec, tmp_path, monkeypatch, fail_second):
+@pytest.mark.parametrize("requests", [4, 20])
+def test_matrix_preserves_scheduled_requests_without_doubling_roles(spec, tmp_path, monkeypatch, fail_second, requests):
spec["plan"]["cases"] = spec["plan"]["cases"][:1]
- spec["plan"]["repetitions"] = 4
+ spec["plan"]["repetitions"] = requests
spec["serving"] = {"concurrency": 1}
submitted = []
def execute(current, directory, *, serving_smoke):
@@ -51,15 +52,16 @@ def execute(current, directory, *, serving_smoke):
return saved_single(current, directory)
monkeypatch.setattr(gpu, "run_gpu_job", execute)
result = smoke.run_matrix(spec, tmp_path)
- assert result["completion"]["scheduled"] == 12
+ assert result["completion"]["scheduled"] == requests * 3
+ assert result["requests_per_configuration"] == requests
assert [s["serving"]["concurrency"] for s in submitted] == ([1, 2] if fail_second else [1, 2, 4])
assert all(s["plan"] == spec["plan"] for s in submitted)
- assert result["completion"]["valid"] == (4 if fail_second else 12)
- assert result["completion"]["not_started"] == (8 if fail_second else 0)
+ assert result["completion"]["valid"] == requests * (1 if fail_second else 3)
+ assert result["completion"]["not_started"] == (requests * 2 if fail_second else 0)
assert result["status"] == ("failed" if fail_second else "complete")
assert not result["ci_accepted"]
report = (tmp_path / "report/index.html").read_text()
- assert report.count("
|---|