diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 8f0e2a0b32..33e7328f2f 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -12,6 +12,22 @@ on:
required: false
type: boolean
default: false
+ h3-cluster:
+ description: "Prepared H3 site: h200-dgxc, h100-dgxc, b200-nscale; mi355x-amds retained-runtime C1"
+ required: false
+ type: string
+ default: h200-dgxc
+ h3-preparation:
+ description: "Optional preparation: amd-node 15m or amd-runtime 60m on 8 GPUs; amd-model/amd-serving need a source run; amd-serving uses one 120m lease"
+ required: false
+ type: choice
+ options: [none, site-preflight, amd-model, amd-node, amd-runtime, amd-site, amd-rootfs, amd-serving, fidelity]
+ default: none
+ 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 +138,21 @@ on:
required: false
type: boolean
default: false
+ h3-cluster:
+ description: "Prepared H3 site: h200-dgxc, h100-dgxc, b200-nscale; mi355x-amds retained-runtime C1"
+ required: false
+ type: string
+ default: h200-dgxc
+ h3-preparation:
+ description: "Optional preparation: amd-node 15m or amd-runtime 60m on 8 GPUs; amd-model/amd-serving need a source run; amd-serving uses one 120m lease"
+ required: false
+ type: string
+ default: none
+ 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
@@ -227,13 +258,31 @@ on:
default: "[]"
jobs:
+ h3-fidelity:
+ if: ${{ inputs.h3-video && inputs.h3-preparation == 'fidelity' }}
+ permissions:
+ contents: read
+ actions: read
+ uses: ./.github/workflows/h3-fidelity.yml
+ with:
+ source-run-ids: ${{ inputs.h3-reuse-run-ids }}
+
h3-video:
- if: ${{ inputs.h3-video }}
+ if: ${{ inputs.h3-video && inputs.h3-preparation != 'fidelity' }}
permissions:
contents: read
actions: read
uses: ./.github/workflows/h3-video.yml
with:
+ cluster: ${{ inputs.h3-cluster }}
+ preflight-only: ${{ inputs.h3-preparation == 'site-preflight' }}
+ stage-amd-model: ${{ inputs.h3-preparation == 'amd-model' }}
+ stage-amd-site: ${{ inputs.h3-preparation == 'amd-site' }}
+ inspect-amd-node: ${{ inputs.h3-preparation == 'amd-node' }}
+ prepare-amd-runtime: ${{ inputs.h3-preparation == 'amd-runtime' }}
+ recover-amd-rootfs: ${{ inputs.h3-preparation == 'amd-rootfs' }}
+ run-amd-serving: ${{ inputs.h3-preparation == 'amd-serving' }}
+ 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-fidelity.yml b/.github/workflows/h3-fidelity.yml
new file mode 100644
index 0000000000..36b902697c
--- /dev/null
+++ b/.github/workflows/h3-fidelity.yml
@@ -0,0 +1,60 @@
+name: H3 Retained Media Fidelity
+
+on:
+ workflow_call:
+ inputs:
+ source-run-ids:
+ description: "Two original H3 serving CI runs, baseline then candidate"
+ required: true
+ type: string
+
+permissions:
+ contents: read
+ actions: read
+
+jobs:
+ compare:
+ name: Compare original C1 video and audio on CPU
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ env:
+ GH_TOKEN: ${{ github.token }}
+ H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
+ PYTHONPATH: ${{ github.workspace }}
+ steps:
+ - name: Authorize manual repository execution
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ if (context.eventName !== 'workflow_dispatch' ||
+ context.repo.owner !== 'SemiAnalysisAI' || context.repo.repo !== 'InferenceX' ||
+ !context.ref.startsWith('refs/heads/')) {
+ throw new Error('H3 fidelity requires manual dispatch of an InferenceX repository branch.');
+ }
+ for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) {
+ const {data} = await github.rest.repos.getCollaboratorPermissionLevel({
+ ...context.repo, username,
+ });
+ if (!['write', 'maintain', 'admin'].includes(data.permission)) {
+ throw new Error(username + ' must have write, maintain, or admin permission.');
+ }
+ }
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ ref: ${{ github.sha }}
+ persist-credentials: false
+ - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
+ - name: Verify sealed sources and compare every C1 media pair
+ run: >-
+ uv run --no-project --python 3.12 --with 'av==16.1.0' --with 'numpy==2.3.5'
+ python experimental/video-generation/compare_serving_ci.py
+ --source-run-ids "$H3_SOURCE_RUN_IDS" --output "$RUNNER_TEMP/h3-fidelity"
+ - name: Preserve comparisons, original media and any failure
+ if: ${{ always() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: h3-fidelity-${{ github.run_id }}-${{ github.run_attempt }}
+ path: ${{ runner.temp }}/h3-fidelity/
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 14
diff --git a/.github/workflows/h3-video.yml b/.github/workflows/h3-video.yml
index f0d30bfef4..403eb3c1da 100644
--- a/.github/workflows/h3-video.yml
+++ b/.github/workflows/h3-video.yml
@@ -4,6 +4,52 @@ 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, mi355x-amds]
+ default: h200-dgxc
+ preflight-only:
+ description: "Inspect the trusted runner identity and public SSH host keys without allocating GPUs"
+ required: false
+ type: boolean
+ default: false
+ stage-amd-model:
+ description: "Stage frozen model files on AMD without allocating GPUs"
+ required: false
+ type: boolean
+ default: false
+ stage-amd-site:
+ description: "Seal inspected AMD runtime and benchmark inputs without GPUs"
+ required: false
+ type: boolean
+ default: false
+ inspect-amd-node:
+ description: "Inspect AMD node telemetry and cached images; at most 8 GPUs for 15 minutes"
+ required: false
+ type: boolean
+ default: false
+ recover-amd-rootfs:
+ description: "Recover the interrupted task-owned AMD rootfs without GPUs"
+ required: false
+ type: boolean
+ default: false
+ run-amd-serving:
+ description: "Reuse retained AMD runtime for one warmup and twenty C1 requests in one 120-minute lease"
+ required: false
+ type: boolean
+ default: false
+ prepare-amd-runtime:
+ description: "Inspect a task-owned cached AMD runtime; at most 8 GPUs for 60 minutes"
+ required: false
+ type: boolean
+ default: false
+ 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 +62,48 @@ on:
default: ""
workflow_call:
inputs:
+ cluster:
+ required: false
+ type: string
+ default: h200-dgxc
+ preflight-only:
+ required: false
+ type: boolean
+ default: false
+ stage-amd-model:
+ description: "Stage frozen model files on AMD without allocating GPUs"
+ required: false
+ type: boolean
+ default: false
+ stage-amd-site:
+ description: "Seal inspected AMD runtime and benchmark inputs without GPUs"
+ required: false
+ type: boolean
+ default: false
+ inspect-amd-node:
+ description: "Inspect AMD node telemetry and cached images; at most 8 GPUs for 15 minutes"
+ required: false
+ type: boolean
+ default: false
+ recover-amd-rootfs:
+ description: "Recover the interrupted task-owned AMD rootfs without GPUs"
+ required: false
+ type: boolean
+ default: false
+ run-amd-serving:
+ description: "Reuse retained AMD runtime for one warmup and twenty C1 requests in one 120-minute lease"
+ required: false
+ type: boolean
+ default: false
+ prepare-amd-runtime:
+ description: "Inspect a task-owned cached AMD runtime; at most 8 GPUs for 60 minutes"
+ required: false
+ type: boolean
+ default: false
+ site-config:
+ required: false
+ type: string
+ default: ""
inventory-run-id:
required: false
type: string
@@ -30,7 +118,7 @@ permissions:
actions: read
concurrency:
- group: h3-video-${{ github.repository }}
+ group: h3-video-${{ github.repository }}${{ inputs.cluster != 'h200-dgxc' && format('-{0}', inputs.cluster) || '' }}${{ (inputs.inspect-amd-node || inputs.prepare-amd-runtime) && '-node-inventory' || inputs.preflight-only && '-preflight' || '' }}
cancel-in-progress: false
jobs:
@@ -42,6 +130,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,18 +157,60 @@ 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 }}
+ H3_PREFLIGHT_ONLY: ${{ inputs.preflight-only }}
+ H3_STAGE_AMD_MODEL: ${{ inputs.stage-amd-model }}
+ H3_STAGE_AMD_SITE: ${{ inputs.stage-amd-site }}
+ H3_INSPECT_AMD_NODE: ${{ inputs.inspect-amd-node }}
+ H3_PREPARE_AMD_RUNTIME: ${{ inputs.prepare-amd-runtime }}
+ H3_RECOVER_AMD_ROOTFS: ${{ inputs.recover-amd-rootfs }}
+ H3_RUN_AMD_SERVING: ${{ inputs.run-amd-serving }}
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
+ preparations=0
+ for selected in "$H3_PREFLIGHT_ONLY" "$H3_STAGE_AMD_MODEL" "$H3_STAGE_AMD_SITE" "$H3_INSPECT_AMD_NODE" "$H3_PREPARE_AMD_RUNTIME" "$H3_RECOVER_AMD_ROOTFS" "$H3_RUN_AMD_SERVING"; do
+ if [[ "$selected" == true ]]; then preparations=$((preparations + 1)); fi
+ done
+ [[ "$preparations" -le 1 ]] || { echo 'Select one H3 preparation operation.' >&2; exit 1; }
+ 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" ;;
+ mi355x-amds)
+ [[ "$H3_PREFLIGHT_ONLY" == true || ( "$H3_STAGE_AMD_MODEL" == true || "$H3_STAGE_AMD_SITE" == true ) || "$H3_INSPECT_AMD_NODE" == true || "$H3_PREPARE_AMD_RUNTIME" == true || "$H3_RECOVER_AMD_ROOTFS" == true || "$H3_RUN_AMD_SERVING" == true ]] || { echo 'AMD H3 execution is not implemented.' >&2; exit 1; }
+ echo 'gpu-model=MI355X' >> "$GITHUB_OUTPUT" ;;
+ *) echo 'Unsupported H3 hardware site.' >&2; exit 1 ;;
+ esac
+ if [[ "$H3_RECOVER_AMD_ROOTFS" == true && ( "$H3_CLUSTER" != mi355x-amds || -n "$H3_SOURCE_RUN_IDS" || -n "$H3_INVENTORY_RUN_ID" ) ]]; then
+ echo 'AMD rootfs recovery requires mi355x-amds and no source runs.' >&2
+ exit 1
+ fi
+ if [[ ( "$H3_INSPECT_AMD_NODE" == true || "$H3_PREPARE_AMD_RUNTIME" == true ) && ( "$H3_CLUSTER" != mi355x-amds || "$H3_PREFLIGHT_ONLY" == true || ( "$H3_STAGE_AMD_MODEL" == true || "$H3_STAGE_AMD_SITE" == true ) || -n "$H3_SOURCE_RUN_IDS" || -n "$H3_INVENTORY_RUN_ID" ) ]]; then
+ echo 'AMD node inspection must be a separate operation on mi355x-amds.' >&2
+ exit 1
+ fi
+ if [[ ( "$H3_STAGE_AMD_MODEL" == true || "$H3_STAGE_AMD_SITE" == true || "$H3_RUN_AMD_SERVING" == true ) && ( "$H3_CLUSTER" != mi355x-amds || "$H3_PREFLIGHT_ONLY" == true || -n "$H3_INVENTORY_RUN_ID" || ! "$H3_SOURCE_RUN_IDS" =~ ^[1-9][0-9]{0,19}$ ) ]]; then
+ echo 'AMD source preparation or serving requires exactly one accepted source run and no other operation.' >&2
+ exit 1
+ fi
+ if [[ "$H3_PREFLIGHT_ONLY" == true && ( -n "$H3_SOURCE_RUN_IDS" || -n "$H3_INVENTORY_RUN_ID" ) ]]; then
+ echo 'Site preflight cannot also reprocess benchmark evidence.' >&2
+ exit 1
+ fi
+ if [[ "$H3_CLUSTER" != h200-dgxc && "$H3_STAGE_AMD_MODEL" != true && "$H3_STAGE_AMD_SITE" != true && "$H3_RUN_AMD_SERVING" != true && ( -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
fi
- [[ "$H3_SITE_CONFIG" = /* ]] || { echo 'Set H3_SITE_CONFIG to the reviewed runner-local JSON path.' >&2; exit 1; }
+ [[ "$H3_PREFLIGHT_ONLY" == true || ( "$H3_STAGE_AMD_MODEL" == true || "$H3_STAGE_AMD_SITE" == true ) || "$H3_INSPECT_AMD_NODE" == true || "$H3_PREPARE_AMD_RUNTIME" == true || "$H3_RECOVER_AMD_ROOTFS" == true || "$H3_RUN_AMD_SERVING" == true || "$H3_SITE_CONFIG" = /* ]] || { echo 'Set H3_SITE_CONFIG to the reviewed runner-local JSON path.' >&2; exit 1; }
if [[ -n "$H3_SOURCE_RUN_IDS" && ! "$H3_SOURCE_RUN_IDS" =~ ^[1-9][0-9]{0,19}(,[1-9][0-9]{0,19})?$ ]]; then
echo 'Expected one or two comma-separated source run IDs.' >&2
exit 1
@@ -88,7 +219,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'
@@ -103,20 +234,22 @@ jobs:
smoke:
needs: prepare
outputs:
- mode: ${{ steps.execute.outputs.mode }}
+ mode: ${{ steps.execute.outputs.mode || steps.amd-serving.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.preflight-only && format('H3 {0} site preflight', needs.prepare.outputs.gpu-model) || inputs.recover-amd-rootfs && 'H3 AMD rootfs recovery (no GPU)' || inputs.stage-amd-site && 'H3 AMD site preparation (no GPU)' || inputs.stage-amd-model && 'H3 AMD model preparation (no GPU)' || inputs.prepare-amd-runtime && 'H3 AMD runtime preparation' || inputs.inspect-amd-node && 'H3 AMD node inventory' || inputs.source-run-ids != '' && !inputs.run-amd-serving && 'H3 H200 hardware inventory' || format('H3 video {0} smoke', needs.prepare.outputs.gpu-model) }}
runs-on: >-
${{ fromJSON(
+ (inputs.preflight-only || inputs.stage-amd-model || inputs.stage-amd-site || inputs.recover-amd-rootfs) && format('["self-hosted","cluster:{0}"]', inputs.cluster) ||
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: ${{ inputs.preflight-only && 5 || inputs.recover-amd-rootfs && 12 || inputs.stage-amd-site && 10 || inputs.stage-amd-model && 45 || inputs.run-amd-serving && 145 || inputs.prepare-amd-runtime && 70 || inputs.inspect-amd-node && 20 || 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 }}
@@ -135,17 +268,71 @@ jobs:
ref: ${{ github.sha }}
path: h3-video-source-${{ github.run_id }}-${{ github.run_attempt }}
persist-credentials: false
+ - name: Inspect trusted runner identity without GPU allocation
+ if: ${{ inputs.preflight-only }}
+ run: >-
+ python3 experimental/video-generation/site_preflight.py
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
+ - name: Recover the interrupted AMD rootfs without GPUs
+ if: ${{ inputs.recover-amd-rootfs }}
+ run: >-
+ python3 experimental/video-generation/prepare_amd_runtime.py
+ --workspace /it-share/data/wenyao-minimax-h3/work
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
+ - name: Stage the accepted frozen model on AMD without GPUs
+ if: ${{ inputs.stage-amd-model }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
+ run: |
+ export PYTHONPATH="$PWD"
+ python3 experimental/video-generation/stage_model_ci.py \
+ --source-run-id "$H3_SOURCE_RUN_IDS" \
+ --workspace /it-share/data/wenyao-minimax-h3/work \
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
+ - name: Seal the inspected AMD runtime and frozen C1 inputs without GPUs
+ if: ${{ inputs.stage-amd-site }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
+ run: |
+ export PYTHONPATH="$PWD"
+ python3 experimental/video-generation/stage_amd_site.py \
+ --source-run-id "$H3_SOURCE_RUN_IDS" \
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
+ - name: Inspect AMD device identities and cached runtime candidates
+ if: ${{ inputs.inspect-amd-node || inputs.prepare-amd-runtime }}
+ run: >-
+ python3 experimental/video-generation/inspect_amd_node.py
+ --workspace /it-share/data/wenyao-minimax-h3/work
+ ${{ inputs.prepare-amd-runtime && '--prepare-runtime' || '' }}
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
+ - name: Recover AMD runtime and execute the frozen C1 campaign in one allocation
+ id: amd-serving
+ if: ${{ inputs.run-amd-serving }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
+ run: |
+ set -euo pipefail
+ export PYTHONPATH="$PWD"
+ echo 'mode=serving-smoke' >> "$GITHUB_OUTPUT"
+ python3 experimental/video-generation/run_amd_serving_ci.py \
+ --source-run-id "$H3_SOURCE_RUN_IDS" \
+ --output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
- name: Execute configured H3 smoke
id: execute
- if: ${{ inputs.source-run-ids == '' }}
+ if: ${{ inputs.source-run-ids == '' && !inputs.preflight-only && !inputs.stage-amd-model && !inputs.stage-amd-site && !inputs.inspect-amd-node && !inputs.prepare-amd-runtime && !inputs.recover-amd-rootfs && !inputs.run-amd-serving }}
run: |
set -euo pipefail
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
@@ -153,7 +340,7 @@ jobs:
--config "$H3_SITE_CONFIG" \
--output "$RUNNER_TEMP/h3-video-$H3_RUN_ID-$H3_RUN_ATTEMPT"
- name: Inspect current H200 hardware and power configuration
- if: ${{ inputs.source-run-ids != '' }}
+ if: ${{ inputs.source-run-ids != '' && !inputs.preflight-only && !inputs.stage-amd-model && !inputs.stage-amd-site && !inputs.inspect-amd-node && !inputs.prepare-amd-runtime && !inputs.recover-amd-rootfs && !inputs.run-amd-serving }}
env:
GH_TOKEN: ${{ github.token }}
H3_SOURCE_RUN_IDS: ${{ inputs.source-run-ids }}
@@ -168,7 +355,7 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: ${{ inputs.source-run-ids != '' && 'h3-hardware' || 'h3-video' }}-${{ github.run_id }}-${{ github.run_attempt }}
+ name: ${{ inputs.recover-amd-rootfs && 'h3-amd-rootfs-recovery' || inputs.preflight-only && 'h3-site-preflight' || inputs.stage-amd-site && 'h3-amd-site-preparation' || inputs.stage-amd-model && 'h3-model-preparation' || inputs.prepare-amd-runtime && 'h3-amd-runtime-preparation' || inputs.inspect-amd-node && 'h3-amd-node-inventory' || inputs.source-run-ids != '' && !inputs.run-amd-serving && 'h3-hardware' || 'h3-video' }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/h3-video-${{ github.run_id }}-${{ github.run_attempt }}/
if-no-files-found: error
compression-level: 0
@@ -176,7 +363,7 @@ jobs:
export:
needs: [prepare, smoke]
- if: ${{ always() && needs.prepare.result == 'success' && needs.smoke.outputs.mode != 'serving-smoke' && (needs.smoke.result == 'success' || (inputs.inventory-run-id != '' && needs.smoke.result == 'skipped')) }}
+ if: ${{ always() && !inputs.preflight-only && !inputs.stage-amd-model && !inputs.stage-amd-site && !inputs.inspect-amd-node && !inputs.prepare-amd-runtime && !inputs.recover-amd-rootfs && !inputs.run-amd-serving && needs.prepare.result == 'success' && needs.smoke.outputs.mode != 'serving-smoke' && (needs.smoke.result == 'success' || (inputs.inventory-run-id != '' && needs.smoke.result == 'skipped')) }}
name: Verify and export retained H3 results
runs-on: ubuntu-latest
timeout-minutes: 15
diff --git a/.github/workflows/test-h3-video.yml b/.github/workflows/test-h3-video.yml
index 669abb573e..70ea5874a4 100644
--- a/.github/workflows/test-h3-video.yml
+++ b/.github/workflows/test-h3-video.yml
@@ -7,6 +7,7 @@ on:
- 'experimental/video-generation/**'
- 'utils/aggregate_power.py'
- '.github/workflows/h3-video.yml'
+ - '.github/workflows/h3-fidelity.yml'
- '.github/workflows/test-h3-video.yml'
- '.github/workflows/e2e-tests.yml'
push:
@@ -14,6 +15,7 @@ on:
- 'experimental/video-generation/**'
- 'utils/aggregate_power.py'
- '.github/workflows/h3-video.yml'
+ - '.github/workflows/h3-fidelity.yml'
- '.github/workflows/test-h3-video.yml'
- '.github/workflows/e2e-tests.yml'
@@ -44,4 +46,4 @@ jobs:
run: >-
go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -shellcheck=
.github/workflows/h3-video.yml .github/workflows/test-h3-video.yml
- .github/workflows/e2e-tests.yml
+ .github/workflows/e2e-tests.yml .github/workflows/h3-fidelity.yml
diff --git a/experimental/video-generation/README.md b/experimental/video-generation/README.md
index 043f6c881e..81d9e035a7 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,39 @@ 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
+
+`h3-preparation=site-preflight` records the selected CI runner's identity, public SSH
+host keys, scheduler account, shared runtime cache candidates, and command
+availability without reserving or querying GPUs. Its
+separate `h3-site-preflight` artifact is not benchmark or runtime qualification
+evidence. It also accepts `mi355x-amds`; actual AMD generation remains unsupported.
+Preflight cannot be combined with historical result reuse. Hardware sites have
+independent workflow concurrency groups; each retains its existing Slurm checks.
+
+For AMD model preparation without local SSH, set `h3-preparation=amd-model`,
+`h3-cluster=mi355x-amds`, and one successful H200 source in `h3-reuse-run-ids`.
+This CPU-only job reuses verified shared weights or stages the exact source
+manifest under `/it-share/data/wenyao-minimax-h3/work`, checking every size and SHA256.
+Its `h3-model-preparation` artifact and persistent `model-ready.json` are preparation
+receipts; they do not admit an AMD runtime or allocate GPUs.
+
+`h3-preparation=amd-node` separately inspects the actual AMD node and cached image
+metadata through the existing queue and Slurm receipts. Its cap is eight allocated
+GPUs for 15 minutes, with a 10-minute read-only step. It retains device/telemetry
+output and cleanup evidence; it neither imports an image nor runs H3 generation.
+
+The existing `serving-smoke` mode accepts 4–200 measured requests per concurrency
+and an optional site-level `concurrencies` subset, such as `[4]`, to complete a
+missing cell in a new run without repeating finished cells. Omission keeps
+`[1, 2, 4]`; the result contract and per-cell evidence remain unchanged.
+The request count is derived
+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..36708d2d7d 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,38 @@ bash -n runtime-entry.example.sh
artifact 检查是独立的验收证据。
编译缓存保留在持久化存储中,不上传为测量证据。
+
+## 跨硬件服务测量
+
+`h3-preparation=site-preflight` 仅记录所选 CI runner 的身份、SSH 主机公钥、调度账户、共享运行时缓存候选和命令路径,
+不分配或查询 GPU。独立的 `h3-site-preflight` 产物不代表性能结果或运行时验收。
+该模式支持 `mi355x-amds`;AMD 视频生成仍未接入。预检不可同时重放历史结果。
+各硬件站点使用独立的工作流并发组,并保留原有 Slurm 校验。
+
+本地无法 SSH 到 AMD 时,可设置 `h3-preparation=amd-model`、
+`h3-cluster=mi355x-amds`,并通过 `h3-reuse-run-ids` 指定一个成功的 H200 来源。
+该 CPU 任务优先复用已校验的共享权重,否则在 `/it-share/data/wenyao-minimax-h3/work`
+准备来源清单中的模型,逐文件校验大小和 SHA256。`h3-model-preparation` 产物与
+持久化的 `model-ready.json` 仅记录准备结果,不代表 AMD 运行时通过验收,也不分配 GPU。
+
+`h3-preparation=amd-node` 通过原有队列和 Slurm 所有权回执单独检查实际 AMD 节点、
+设备遥测格式与缓存镜像元数据。上限为分配 8 张 GPU、15 分钟,读取步骤最多 10 分钟。
+保留查询与资源清理证据,不导入镜像,也不执行 H3 视频生成。
+
+站点配置可指定 `concurrencies` 子集(如 `[4]`),用新执行补齐缺失档位,
+无需重复已完成结果。省略时仍运行 `[1, 2, 4]`,产物与逐档证据格式保持不变。
+
+现有 `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/campaigns/h3-cross-hardware/entry-only-amd.sh b/experimental/video-generation/campaigns/h3-cross-hardware/entry-only-amd.sh
new file mode 100644
index 0000000000..e1ef4607c3
--- /dev/null
+++ b/experimental/video-generation/campaigns/h3-cross-hardware/entry-only-amd.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# Enter only the existing task-owned runtime. This never allocates or installs.
+set -euo pipefail
+runtime_root=/it-share/data/wenyao-minimax-h3
+workspace=$runtime_root/work
+container_name=wenyao-minimax-h3-rocm
+: "${SLURM_JOB_ID:?must enter through srun}"
+: "${SLURM_STEP_ID:?must enter through an allocated step}"
+: "${SLURM_STEP_GPUS:?Slurm must bind the full AMD node}"
+: "${H3_AMD_ALLOCATION_UUIDS:?physical allocation proof required}"
+: "${ROCR_VISIBLE_DEVICES:?HIP binding required}"
+[[ -d "$runtime_root/enroot-data/$container_name" && -f "$workspace/campaigns/h3-cross-hardware/runtime-inspected.json" && $# -gt 0 ]]
+exec 9>"$runtime_root/.session.lock"
+flock -n 9 || { echo "Prepared runtime has an active session" >&2; exit 2; }
+export ENROOT_DATA_PATH="$runtime_root/enroot-data"
+export ENROOT_CACHE_PATH="$runtime_root/cache"
+export ENROOT_RUNTIME_PATH="/tmp/inferencex-h3-${SLURM_JOB_ID}-${SLURM_STEP_ID}/runtime"
+export ENROOT_TEMP_PATH="/tmp/inferencex-h3-${SLURM_JOB_ID}-${SLURM_STEP_ID}/tmp"
+export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/rocm/bin
+# ROCR supplies the physical mask; the supervisor sets logical HIP/CUDA ordinals.
+unset HIP_VISIBLE_DEVICES CUDA_VISIBLE_DEVICES
+h3_env=(ROCR_VISIBLE_DEVICES H3_AMD_ALLOCATION_UUIDS H3_AMD_MONITOR_RECEIPT PYTHONDONTWRITEBYTECODE
+ SLURM_JOB_ID SLURM_STEP_ID SLURMD_NODENAME SLURM_PROCID SLURM_NTASKS
+ SLURM_JOB_GPUS SLURM_STEP_GPUS SLURM_CPU_BIND SLURM_CPUS_PER_TASK)
+h3_args=()
+for name in "${h3_env[@]}"; do
+ if [[ -v "$name" ]]; then h3_args+=(--env "$name"); fi
+done
+exec /usr/local/bin/enroot start --rw --mount "$workspace:/work" \
+ --mount /dev/kfd:/dev/kfd --mount /dev/dri:/dev/dri \
+ "${h3_args[@]}" --env SGLANG_USE_AITER=1 --env HF_HOME=/work/.cache/huggingface \
+ -- "$container_name" @ENTRY@
diff --git a/experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json b/experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json
new file mode 100644
index 0000000000..616d403baf
--- /dev/null
+++ b/experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json
@@ -0,0 +1,162 @@
+{
+ "cases": [
+ {
+ "case_id": "spoken-greeting-s11",
+ "prompt": "A single continuous medium close-up of an adult presenter facing the camera in a quiet studio. The presenter smiles naturally and says clearly, \"Good morning. It is a beautiful day to try something new.\" Mouth movements follow the spoken sentence. The camera remains still. No background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 11
+ },
+ {
+ "case_id": "bicycle-pass-s11",
+ "prompt": "A single continuous eye-level shot of a cyclist riding from left to right across a paved park path, passing the camera and continuing into the distance. The bicycle wheels keep turning and the leaves move gently in the breeze. Tire sounds, wind, and distant birds are audible. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 11
+ },
+ {
+ "case_id": "spoken-greeting-s29",
+ "prompt": "A single continuous medium close-up of an adult presenter facing the camera in a quiet studio. The presenter smiles naturally and says clearly, \"Good morning. It is a beautiful day to try something new.\" Mouth movements follow the spoken sentence. The camera remains still. No background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 29
+ },
+ {
+ "case_id": "piano-notes-s47",
+ "prompt": "A single continuous close-up of two human hands playing a short, slow melody on an acoustic piano. The fingers visibly press and release the keys, with each note ringing naturally in the room. The camera remains still. The piano is the only prominent sound. No speech, background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 47
+ },
+ {
+ "case_id": "piano-notes-s29",
+ "prompt": "A single continuous close-up of two human hands playing a short, slow melody on an acoustic piano. The fingers visibly press and release the keys, with each note ringing naturally in the room. The camera remains still. The piano is the only prominent sound. No speech, background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 29
+ },
+ {
+ "case_id": "drum-taps-s29",
+ "prompt": "A single continuous medium shot of a drummer tapping a snare drum three times in a quiet rehearsal room. The drumsticks visibly contact the drumhead on each tap. The three crisp snare hits and the natural room ambience are audible. The camera remains still. No music, speech, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 29
+ },
+ {
+ "case_id": "bicycle-pass-s29",
+ "prompt": "A single continuous eye-level shot of a cyclist riding from left to right across a paved park path, passing the camera and continuing into the distance. The bicycle wheels keep turning and the leaves move gently in the breeze. Tire sounds, wind, and distant birds are audible. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 29
+ },
+ {
+ "case_id": "water-pour-s47",
+ "prompt": "A single continuous close-up of a person slowly pouring water from a clear glass pitcher into a ceramic cup on a wooden table. The water stream, rising water level, and small ripples are visible. The pouring and gentle splashing are audible in a quiet kitchen. The camera remains still. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 47
+ },
+ {
+ "case_id": "water-pour-s83",
+ "prompt": "A single continuous close-up of a person slowly pouring water from a clear glass pitcher into a ceramic cup on a wooden table. The water stream, rising water level, and small ripples are visible. The pouring and gentle splashing are audible in a quiet kitchen. The camera remains still. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 83
+ },
+ {
+ "case_id": "piano-notes-s11",
+ "prompt": "A single continuous close-up of two human hands playing a short, slow melody on an acoustic piano. The fingers visibly press and release the keys, with each note ringing naturally in the room. The camera remains still. The piano is the only prominent sound. No speech, background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 11
+ },
+ {
+ "case_id": "spoken-greeting-s47",
+ "prompt": "A single continuous medium close-up of an adult presenter facing the camera in a quiet studio. The presenter smiles naturally and says clearly, \"Good morning. It is a beautiful day to try something new.\" Mouth movements follow the spoken sentence. The camera remains still. No background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 47
+ },
+ {
+ "case_id": "bicycle-pass-s83",
+ "prompt": "A single continuous eye-level shot of a cyclist riding from left to right across a paved park path, passing the camera and continuing into the distance. The bicycle wheels keep turning and the leaves move gently in the breeze. Tire sounds, wind, and distant birds are audible. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 83
+ },
+ {
+ "case_id": "bicycle-pass-s47",
+ "prompt": "A single continuous eye-level shot of a cyclist riding from left to right across a paved park path, passing the camera and continuing into the distance. The bicycle wheels keep turning and the leaves move gently in the breeze. Tire sounds, wind, and distant birds are audible. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 47
+ },
+ {
+ "case_id": "water-pour-s11",
+ "prompt": "A single continuous close-up of a person slowly pouring water from a clear glass pitcher into a ceramic cup on a wooden table. The water stream, rising water level, and small ripples are visible. The pouring and gentle splashing are audible in a quiet kitchen. The camera remains still. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 11
+ },
+ {
+ "case_id": "drum-taps-s11",
+ "prompt": "A single continuous medium shot of a drummer tapping a snare drum three times in a quiet rehearsal room. The drumsticks visibly contact the drumhead on each tap. The three crisp snare hits and the natural room ambience are audible. The camera remains still. No music, speech, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 11
+ },
+ {
+ "case_id": "spoken-greeting-s83",
+ "prompt": "A single continuous medium close-up of an adult presenter facing the camera in a quiet studio. The presenter smiles naturally and says clearly, \"Good morning. It is a beautiful day to try something new.\" Mouth movements follow the spoken sentence. The camera remains still. No background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 83
+ },
+ {
+ "case_id": "drum-taps-s83",
+ "prompt": "A single continuous medium shot of a drummer tapping a snare drum three times in a quiet rehearsal room. The drumsticks visibly contact the drumhead on each tap. The three crisp snare hits and the natural room ambience are audible. The camera remains still. No music, speech, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 83
+ },
+ {
+ "case_id": "water-pour-s29",
+ "prompt": "A single continuous close-up of a person slowly pouring water from a clear glass pitcher into a ceramic cup on a wooden table. The water stream, rising water level, and small ripples are visible. The pouring and gentle splashing are audible in a quiet kitchen. The camera remains still. No speech, music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 29
+ },
+ {
+ "case_id": "piano-notes-s83",
+ "prompt": "A single continuous close-up of two human hands playing a short, slow melody on an acoustic piano. The fingers visibly press and release the keys, with each note ringing naturally in the room. The camera remains still. The piano is the only prominent sound. No speech, background music, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 83
+ },
+ {
+ "case_id": "drum-taps-s47",
+ "prompt": "A single continuous medium shot of a drummer tapping a snare drum three times in a quiet rehearsal room. The drumsticks visibly contact the drumhead on each tap. The three crisp snare hits and the natural room ambience are audible. The camera remains still. No music, speech, subtitles, or scene cuts.",
+ "requires_motion": true,
+ "requires_sound": true,
+ "seed": 47
+ }
+ ],
+ "generation": {
+ "aspect_ratio": "16:9",
+ "audio_channels": 2,
+ "audio_flow_shift": 3.0,
+ "audio_sample_rate_hz": 32000,
+ "duration_seconds": 8,
+ "flow_shift": 12.0,
+ "fps": 24,
+ "frame_count": 192,
+ "height": 768,
+ "num_inference_steps": 50,
+ "width": 1344
+ },
+ "model_id": "MiniMaxAI/MiniMax-H3",
+ "model_revision": "42ed227ee7df40d41602854ae760620d6eb651fe",
+ "plan_id": "h3-cross-hardware-formal-8s-v1",
+ "repetitions": 1,
+ "warmup_runs": 1
+}
diff --git a/experimental/video-generation/ci.py b/experimental/video-generation/ci.py
index dcbdea3009..98f5d0e738 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,9 @@
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"}
+AMD_SITE = {"cluster": "mi355x-amds", "partition": "compute", "account": "cameronamd@semianalysis.com", "gpu_model": "MI355X"}
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 +87,19 @@ 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", "concurrencies"}, "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 == AMD_SITE or (site["cluster"] in NVIDIA_CLUSTERS and site["gpu_model"] == NVIDIA_CLUSTERS[site["cluster"]]), "Unsupported cluster or GPU model")
+ need(site == AMD_SITE or 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")
+ if "concurrencies" in config:
+ need(config["mode"] == "serving-smoke", "Concurrency selection requires serving-smoke")
+ from evaluator.mvp_serving_smoke import validate_concurrencies
+ validate_concurrencies(config["concurrencies"])
+ 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 +112,24 @@ 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(site != AMD_SITE or reserved == 8, "AMD requires a full eight-GPU allocation before selecting participating HIP devices")
+ 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,10 +158,25 @@ 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")
+def allocated_gpu_count(record: dict) -> int | None:
+ tres = dict(item.split("=", 1) for item in record.get("AllocTRES", "").split(",") if "=" in item)
+ if "gres/gpu" in tres:
+ return int(tres["gres/gpu"])
+ # This AMD site omits GPU AllocTRES. The granted full-node job still records
+ # TresPerNode; a subsequent eight-GPU step and physical UUID inventory are
+ # required before generation. Missing partial allocations remain unknown.
+ if (record.get("Partition") == AMD_SITE["partition"] and record.get("Account") == AMD_SITE["account"]
+ and record.get("OverSubscribe") == "NO" and record.get("NumNodes") == "1"
+ and record.get("TresPerNode") == "gres/gpu:8"):
+ return 8
+ return None
+
+
def capacity(record: dict, resources: dict, timestamp: datetime | None = None) -> str | None:
need(record.get("NumNodes") == "1" and NAME.fullmatch(record.get("NodeList", "")), "Reuse requires one explicit node")
tres = dict(item.split("=", 1) for item in record["AllocTRES"].split(","))
@@ -150,7 +187,7 @@ def capacity(record: dict, resources: dict, timestamp: datetime | None = None) -
remaining = (end - (timestamp or datetime.now(timezone.utc))).total_seconds()
if remaining < resources["minutes"] * 60 - 300 + 30:
return "insufficient remaining allocation time"
- if int(tres.get("gres/gpu", "0")) < resources["gpus"] or int(record["NumCPUs"]) < resources["cpus"] or memory_gb < resources["memory_gb"]:
+ if (allocated_gpu_count(record) or 0) < resources["gpus"] or int(record["NumCPUs"]) < resources["cpus"] or memory_gb < resources["memory_gb"]:
return "insufficient allocated GPU/CPU/memory capacity"
return None
@@ -175,6 +212,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 +243,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 +267,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
@@ -298,7 +342,8 @@ def collect(run_dir: Path, output: Path) -> None:
def stage_package(source: Path, destination: Path) -> dict[str, str]:
- selected = {p.relative_to(source).as_posix(): p for p in [*source.glob("*.py"), *(source / "evaluator").glob("*.py")]}
+ selected = {p.relative_to(source).as_posix(): p for p in [*source.glob("*.py"), *(source / "evaluator").glob("*.py"),
+ *(source / "runtime-patches").glob("*.patch")]}
shared_power = source.parents[1] / "utils" / "aggregate_power.py"
if shared_power.is_file():
selected["utils/aggregate_power.py"] = shared_power
@@ -316,10 +361,12 @@ def stage_package(source: Path, destination: Path) -> dict[str, str]:
def step_argv(config: dict, receipt: dict, record: dict, run_dir: Path, package: Path) -> list[str]:
request = config["resources"]
+ gpu_flags = ["--gres=gpu:8"] if config.get("site") == AMD_SITE else [
+ "--gpus-per-task=" + str(request["gpus"]), "--gpu-bind=verbose,per_task:" + str(request["gpus"])]
return ["srun", "--jobid=" + receipt["identity"]["JobId"], "--nodelist=" + record["NodeList"],
"--exclusive", "--exact", "--nodes=1", "--ntasks=1", "--immediate=30", "--kill-on-bad-exit=1",
"--cpus-per-task=" + str(request["cpus"]), "--cpu-bind=verbose,cores",
- "--gpus-per-task=" + str(request["gpus"]), "--gpu-bind=verbose,per_task:" + str(request["gpus"]),
+ *gpu_flags,
"--mem=" + str(request["memory_gb"]) + "G", "--time=" + str(request["minutes"] - 5),
"--chdir=" + str(run_dir), "--export=PATH,PYTHONDONTWRITEBYTECODE,TZ,LC_ALL",
"python3", str(package / "ci.py"), "--enter", str(run_dir)]
@@ -360,7 +407,10 @@ def prepared_spec(config: dict) -> dict:
need(digest(config["spec"]["path"]) == config["spec"]["sha256"], "Prepared GPU specification changed")
spec = read(config["spec"]["path"])
# Slurm assigns physical devices later. Validate the rest without a GPU call.
- spec["gpu_uuids"] = [f"GPU-00000000-0000-0000-0000-{i:012d}" for i in range(config["resources"]["gpus"])]
+ amd = config.get("site") == AMD_SITE
+ need(spec.get("gpu_vendor", "nvidia") == ("amd" if amd else "nvidia"), "Prepared GPU vendor differs from the admitted site")
+ prefix = "" if amd else "GPU-"
+ spec["gpu_uuids"] = [f"{prefix}00000000-0000-0000-0000-{i:012d}" for i in range(config["resources"]["gpus"])]
from evaluator.mvp_gpu_job import validate_gpu_job
spec = validate_gpu_job(spec)
if config["mode"] == "serving-smoke":
@@ -373,7 +423,11 @@ def prepared_spec(config: dict) -> dict:
# Cheap inventory checks happen before salloc; full pinned source/weight
# hashing remains in the existing supervisor immediately before execution.
for role in ("baseline", "candidate"):
- need(host_path(config, spec[role]["source"]).is_dir(), "Prepared runtime source missing: " + role)
+ source = host_path(config, spec[role]["source"])
+ need(source.is_dir(), "Prepared runtime source missing: " + role)
+ if spec.get("server_timing"):
+ from evaluator.mvp_runtime_timing import validate_source
+ validate_source(source)
model = host_path(config, spec["model"]["path"])
for item in spec["model"]["files"]:
path = model / item["path"]
@@ -382,7 +436,7 @@ def prepared_spec(config: dict) -> dict:
return spec
-def launch(config: dict, output: Path) -> int:
+def launch(config: dict, output: Path, *, required_allocation: str | None = None) -> int:
config = validate_config(config)
run_id = os.environ.get("GITHUB_RUN_ID", "")
attempt = os.environ.get("GITHUB_RUN_ATTEMPT", "")
@@ -400,9 +454,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"),
@@ -426,6 +480,9 @@ def launch(config: dict, output: Path) -> int:
package_files = stage_package(source, package)
decision = recover(config, results)
write(run_dir / "recovery.json", decision)
+ if required_allocation is not None:
+ need(decision["action"] == "reuse" and decision["receipt"]["identity"]["JobId"] == required_allocation,
+ "Serving continuation must reuse its original allocation; no replacement requested")
need(decision["action"] != "wait", "A task-owned allocation is pending or suspended; no duplicate submitted")
if decision["action"] == "reuse":
receipt, reused = decision["receipt"], True
@@ -437,8 +494,7 @@ def launch(config: dict, output: Path) -> int:
need(record["JobState"] == "RUNNING", "Owned allocation is not RUNNING")
need(capacity(record, config["resources"]) is None, "Allocation cannot serve bounded step")
if config["mode"] == "serving-smoke":
- tres = dict(item.split("=", 1) for item in record["AllocTRES"].split(","))
- need(int(tres.get("gres/gpu", "0")) == reserved_gpus, "Serving allocation GPU count exceeds the declared budget")
+ need(allocated_gpu_count(record) == reserved_gpus, "Serving allocation GPU count exceeds the declared budget")
need(digest(config["runtime"]["entry"]) == config["runtime"]["entry_sha256"], "Entry changed after preflight")
state.update(phase="starting", allocation_reused=reused, allocation=receipt, slurm_job=record)
write(run_dir / "ci.json", state)
@@ -446,7 +502,7 @@ def launch(config: dict, output: Path) -> int:
active_steps = command(["squeue", "--steps", "--noheader", "--jobs=" + record["JobId"], "--format=%i|%N"])
write(run_dir / "context.json", {"config": config, "spec": spec, "allocation": receipt,
"node": record["NodeList"], "active_steps": active_steps, "source_sha": sha,
- "exclusive_node": record.get("OverSubscribe") == "NO" and "gres/gpu=8" in record.get("AllocTRES", "").split(","),
+ "exclusive_node": record.get("OverSubscribe") == "NO" and allocated_gpu_count(record) == 8,
"package_files": package_files, "run_id": f"github-{run_id}-{attempt}"})
argv = step_argv(config, receipt, record, run_dir, package)
write(run_dir / "step-command.json", argv)
@@ -486,7 +542,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)
@@ -503,9 +560,22 @@ def enter(run_dir: Path) -> None:
write(run_dir / "binding.json", {"job_id": job, "step_id": step, "node": context["node"],
"cpu_affinity": sorted(os.sched_getaffinity(0)), "observed_at": now(), "phase": "entering_runtime"})
need(digest(config["runtime"]["entry"]) == config["runtime"]["entry_sha256"], "Entry changed on compute node")
+ env = {**os.environ, "H3_EXPECTED_GPU_MODEL": config.get("site", DEFAULT_SITE)["gpu_model"]}
+ if config.get("site") == AMD_SITE:
+ from inspect_amd_node import step_gpu_indices
+ from evaluator.mvp_amd_gpu import inventory as amd_inventory, smi, observe_system_monitor
+ need(step_gpu_indices(os.environ.get("SLURM_STEP_GPUS", "")) == set(range(8)), "AMD requires all eight GPUs bound to this step")
+ observed = amd_inventory(smi("list", 10))
+ need(len(observed) == 8, "AMD allocated physical inventory is incomplete")
+ write(run_dir / "amd-allocated-devices.json", list(observed.values()))
+ monitor_path = run_dir / "amd-system-monitor.json"
+ write(monitor_path, observe_system_monitor(10))
+ env.update(H3_AMD_ALLOCATION_UUIDS=",".join(observed),
+ H3_AMD_MONITOR_RECEIPT=str(mapped(config, monitor_path)),
+ ROCR_VISIBLE_DEVICES=",".join(str(i) for i in range(config["resources"]["gpus"])))
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, env)
def workload_complete(verified: dict) -> bool:
@@ -541,9 +611,17 @@ def inside(run_dir: Path) -> int:
and os.environ.get("SLURM_PROCID") == "0" and os.environ.get("SLURM_NTASKS") == "1",
"Payload is not the exact single-node Slurm task")
need(inventory(Path(__file__).parent) == context["package_files"], "Staged harness bytes changed")
- devices = cuda_devices()
- assigned = os.environ.get("H3_ASSIGNED_GPU_UUIDS", "").split(",")
- need(set(devices) == set(assigned), "CUDA-visible UUIDs differ from the Slurm global GPU assignment")
+ if config.get("site") == AMD_SITE:
+ from evaluator.mvp_amd_gpu import hip_devices
+ devices = hip_devices()
+ allocated = os.environ.get("H3_AMD_ALLOCATION_UUIDS", "").split(",")
+ physical = read(run_dir / "amd-allocated-devices.json")
+ need(len(set(allocated)) == 8 and set(allocated) == {row["uuid"] for row in physical}
+ and set(devices) <= set(allocated), "HIP devices differ from the full Slurm-owned AMD inventory")
+ else:
+ devices = cuda_devices()
+ assigned = os.environ.get("H3_ASSIGNED_GPU_UUIDS", "").split(",")
+ need(set(devices) == set(assigned), "CUDA-visible UUIDs differ from the Slurm global GPU assignment")
cpus = sorted(os.sched_getaffinity(0))
binding = read(run_dir / "binding.json")
need(binding["job_id"] == expected and binding["step_id"] == step and binding["cpu_affinity"] == cpus,
@@ -552,14 +630,14 @@ def inside(run_dir: Path) -> int:
need(len(cpus) >= config["resources"]["cpus"], "Bound step CPU set is too small")
write(run_dir / "binding.json", {"job_id": expected, "step_id": step, "node": context["node"],
"gpu_uuids": devices, "cpu_affinity": cpus,
- "slurm": {key: os.environ.get(key) for key in ("CUDA_VISIBLE_DEVICES", "H3_ORIGINAL_CUDA_VISIBLE_DEVICES", "SLURM_JOB_GPUS", "SLURM_STEP_GPUS", "SLURM_CPU_BIND", "SLURM_CPUS_PER_TASK")}, "observed_at": now()})
+ "slurm": {key: os.environ.get(key) for key in ("CUDA_VISIBLE_DEVICES", "H3_ORIGINAL_CUDA_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES", "H3_AMD_ALLOCATION_UUIDS", "SLURM_JOB_GPUS", "SLURM_STEP_GPUS", "SLURM_CPU_BIND", "SLURM_CPUS_PER_TASK")}, "observed_at": now()})
spec = context["spec"]
spec["gpu_uuids"], spec["job_id"] = devices, context["run_id"]
active = [line for line in context["active_steps"].splitlines() if not re.match(r"[0-9]+\.(batch|extern)\|", line)]
spec["allocation"] = {"mode": "dedicated_ci" if not active and context["exclusive_node"] else "cooperative_shared", "label": f"Slurm {expected}.{step} on {context['node']}"}
if config["mode"] == "serving-smoke":
from evaluator.mvp_serving_smoke import run_matrix
- matrix = run_matrix(spec, run_dir)
+ matrix = run_matrix(spec, run_dir, concurrencies=config.get("concurrencies", (1, 2, 4)))
complete = matrix["status"] == "complete"
result.update(exit_code=0 if complete else 1, smoke_completed=complete,
measurement_status="complete" if complete else "incomplete",
diff --git a/experimental/video-generation/compare_serving_ci.py b/experimental/video-generation/compare_serving_ci.py
new file mode 100644
index 0000000000..8c4c4e137d
--- /dev/null
+++ b/experimental/video-generation/compare_serving_ci.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+"""Retain original sources so paired fidelity is auditable without GPU reruns."""
+from __future__ import annotations
+
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+import ci
+import export_ci
+from evaluator.mvp_compare import compare_runs
+from evaluator.mvp_report import write_report
+
+
+def selected_run(root: Path, source: dict) -> Path:
+ expected = {}
+ for line in (root / "SHA256SUMS").read_text().splitlines():
+ match = re.fullmatch(r"([0-9a-f]{64}) (.+)", line)
+ ci.need(match is not None, "Malformed source checksum entry")
+ digest, name = match.groups()
+ ci.need(name not in expected, "Duplicate source checksum entry")
+ expected[name] = digest
+ ci.need(expected and ci.inventory(root) == expected, "Source artifact checksum mismatch")
+ record = ci.read(root / "ci.json")
+ ci.need(str(record.get("run_id")) == str(source["databaseId"])
+ and str(record.get("run_attempt")) == str(source["runAttempt"])
+ and record.get("source_sha") == source["headSha"], "Source CI identity mismatch")
+ manifest = ci.read(root / "manifest.json")
+ ci.need(str(manifest.get("run_id")) == str(source["databaseId"])
+ and str(manifest.get("run_attempt")) == str(source["runAttempt"])
+ and manifest.get("git_commit") == source["headSha"]
+ and manifest.get("mode") == "serving-smoke", "Source manifest identity mismatch")
+ matrix = ci.read(root / "serving-smoke.json")
+ ci.need(matrix.get("bundle_type") == "h3_serving_smoke_matrix"
+ and matrix.get("schema_version") == "1.0.0", "Expected the existing serving matrix contract")
+ cells = [cell for cell in matrix["cells"] if cell.get("concurrency") == 1]
+ ci.need(len(cells) == 1, "Source requires exactly one C1 cell")
+ cell = cells[0]
+ relative = cell.get("run", {}).get("path")
+ ci.need(isinstance(relative, str) and relative in expected, "C1 run is missing from the sealed artifact")
+ path = (root / relative).resolve()
+ ci.need(path.is_relative_to(root.resolve()) and path.name == "run.json"
+ and expected[relative] == cell["run"]["sha256"], "C1 run identity mismatch")
+ run = ci.read(path)
+ ci.need(run.get("configuration", {}).get("serving", {}).get("concurrency") == 1,
+ "Selected run is not C1")
+ return path.parent
+
+
+def publish(run_ids: list[str], output: Path) -> None:
+ ci.need(len(run_ids) == 2 and run_ids == export_ci.source_ids(",".join(run_ids)),
+ "Exactly two distinct source runs are required")
+ sha = os.environ.get("GITHUB_SHA", "")
+ ci.need(re.fullmatch(r"[0-9a-f]{40}", sha)
+ and ci.command(["git", "rev-parse", "HEAD"]).strip() == sha, "Exact exporter checkout required")
+ run_id, attempt = os.environ.get("GITHUB_RUN_ID", ""), os.environ.get("GITHUB_RUN_ATTEMPT", "")
+ ci.need(run_id.isdigit() and attempt.isdigit()
+ and os.environ.get("GITHUB_REPOSITORY") == export_ci.REPOSITORY, "GitHub producer identity required")
+ output.mkdir(parents=True, exist_ok=False)
+ with tempfile.TemporaryDirectory(prefix="h3-fidelity-sources-") as scratch:
+ compare_sources(run_ids, output, Path(scratch), sha, run_id, attempt)
+
+
+def compare_sources(run_ids: list[str], output: Path, scratch: Path, sha: str, run_id: str, attempt: str) -> None:
+ sources, directories = [], []
+ for source in run_ids:
+ metadata, artifact = export_ci.verified_execution(source)
+ target = scratch / ("source-" + source)
+ subprocess.run(["gh", "run", "download", source, "--repo", export_ci.REPOSITORY,
+ "--name", artifact["name"], "--dir", str(target)], check=True, timeout=300)
+ selected = selected_run(target, metadata)
+ directories.append(selected)
+ snapshot = output / "sources" / source
+ snapshot.mkdir(parents=True)
+ for name in ("ci.json", "manifest.json", "serving-smoke.json"):
+ shutil.copyfile(target / name, snapshot / name)
+ shutil.copyfile(target / "SHA256SUMS", snapshot / "original-SHA256SUMS")
+ shutil.copyfile(selected / "run.json", snapshot / "c1-run.json")
+ sources.append({"ci": metadata, "artifact": artifact,
+ "source_seal_sha256": ci.digest(target / "SHA256SUMS")})
+ policy = ci.read(Path(__file__).parent / "mvp/example-uncalibrated.policy.json")
+ comparison = compare_runs(*directories, policy=policy)
+ comparison["producer"] = {"git_commit": sha, "run_id": run_id, "run_attempt": attempt,
+ "run_url": f"https://github.com/{export_ci.REPOSITORY}/actions/runs/{run_id}",
+ "mode": "CPU-only comparison of original C1 media; no new generation"}
+ comparison["source_artifacts"] = sources
+ ci.write(output / "comparison.json", comparison)
+ write_report(comparison, output / "report/index.html")
+ ci.write(output / "reprocessing.json", {"status": "complete", "generation_executed": False,
+ "calibration_status": policy["calibration_status"], "release_qualified": False,
+ "threshold_outcome": comparison["overall_status"], "matched_pairs": comparison["summary"]["matched_valid_pairs"],
+ "interpretation": "CI success means report generation succeeded, not that fidelity or regression passed."})
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--source-run-ids", required=True)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ if args.output.exists():
+ parser.error("output must be a new directory")
+ status = 0
+ try:
+ publish(export_ci.source_ids(args.source_run_ids), args.output)
+ except Exception as error:
+ args.output.mkdir(parents=True, exist_ok=True)
+ ci.write(args.output / "reprocessing-error.json", {"status": "failed", "error": str(error),
+ "generation_executed": False, "release_qualified": False})
+ status = 2
+ finally:
+ files = export_ci.files_with_nested_seals(args.output)
+ (args.output / "SHA256SUMS").write_text("".join(f"{sha} {path}\n" for path, sha in files.items()))
+ return status
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/experimental/video-generation/evaluator/mvp_amd_gpu.py b/experimental/video-generation/evaluator/mvp_amd_gpu.py
new file mode 100644
index 0000000000..f60b336049
--- /dev/null
+++ b/experimental/video-generation/evaluator/mvp_amd_gpu.py
@@ -0,0 +1,203 @@
+"""AMD SMI observations normalized to the existing GPU telemetry contract."""
+from __future__ import annotations
+
+import ctypes
+import hashlib
+import json
+import math
+import os
+from pathlib import Path
+import re
+import time
+
+from .mvp_gpu_job import _check_deadline, _command, _now, _proc_identity
+
+
+def monitor_process(pid: int) -> dict:
+ before = _proc_identity(pid)
+ status = Path(f"/proc/{pid}/status").read_text()
+ cgroup = Path(f"/proc/{pid}/cgroup").read_text()
+ with Path(f"/proc/{pid}/cmdline").open("rb") as stream:
+ executable = stream.read(4096).split(b"\0", 1)[0].decode()
+ after = _proc_identity(pid)
+ if (not before or not after or before["start_ticks"] != after["start_ticks"]
+ or before["ppid"] != 1 or not re.search(r"^Uid:\s+0\s+0\s+0\s+0\s*$", status, re.MULTILINE)
+ or not any(line.endswith(":/system.slice/gpuagent.service") for line in cgroup.splitlines())
+ or not executable.startswith("/")):
+ raise ValueError("GPU monitor process identity is not established")
+ try:
+ if os.readlink(f"/proc/{pid}/exe") != executable:
+ raise ValueError("GPU monitor executable changed")
+ method = "proc exe and systemd ExecStart"
+ except PermissionError:
+ method = "systemd ExecMainPID/ExecStart and proc argv0; proc exe access denied"
+ return {"pid": pid, "start_ticks": before["start_ticks"], "uid": 0,
+ "cgroup": "/system.slice/gpuagent.service", "executable": executable,
+ "executable_verification": method}
+
+
+def observe_system_monitor(timeout: float) -> dict:
+ record = {"service": "gpuagent.service", "observed_at": _now(), "status": "unverified"}
+ try:
+ raw = _command(["systemctl", "show", "gpuagent.service", "--property=MainPID,ExecMainPID,ExecStart,ActiveState,SubState,ControlGroup,Type"], timeout=timeout)
+ properties = dict(line.split("=", 1) for line in raw.splitlines() if "=" in line)
+ pid = int(properties["MainPID"])
+ executable = re.search(r"(?:^|[ {])path=(/[^ ;}]+)", properties["ExecStart"])
+ if (properties["ActiveState"] != "active" or properties["SubState"] != "running"
+ or properties["ControlGroup"] != "/system.slice/gpuagent.service"
+ or properties["Type"] not in {"simple", "exec", "notify"}
+ or pid <= 1 or int(properties["ExecMainPID"]) != pid or executable is None):
+ raise ValueError("GPU monitoring service is not an active direct systemd process")
+ process = monitor_process(pid)
+ binary = Path(executable[1])
+ info = binary.stat()
+ if process["executable"] != str(binary) or not binary.is_file() or info.st_uid != 0 or info.st_mode & 0o022:
+ raise ValueError("GPU monitor executable is not the root-owned service binary")
+ record.update(status="verified", process=process,
+ executable_sha256=hashlib.sha256(binary.read_bytes()).hexdigest(),
+ policy="Only this unchanged root service with zero per-process VRAM is excluded from workload contexts; board power still includes its overhead")
+ except Exception as error:
+ record.update(error_type=type(error).__name__, error=str(error))
+ return record
+
+
+def is_system_monitor(app: dict, receipt: dict | None) -> bool:
+ if not receipt or receipt.get("status") != "verified" or app["memory_used_mib"] != 0:
+ return False
+ expected = receipt.get("process", {})
+ if app["pid"] != expected.get("pid"):
+ return False
+ try:
+ return monitor_process(app["pid"]) == expected
+ except (OSError, ValueError):
+ return False
+
+
+def smi(option: str, timeout: float) -> object:
+ return json.loads(_command(["amd-smi", option, "--json"], timeout=timeout))
+
+
+def inventory(rows: object) -> dict[str, dict]:
+ if not isinstance(rows, list) or not rows:
+ raise RuntimeError("AMD SMI GPU inventory unavailable")
+ values = {}
+ for row in rows:
+ if (not isinstance(row, dict) or not isinstance(row.get("uuid"), str)
+ or not re.fullmatch(r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}", row["uuid"])
+ or type(row.get("gpu")) is not int or not 0 <= row["gpu"] < 8
+ or row.get("partition_id") != 0
+ or not isinstance(row.get("bdf"), str)
+ or not re.fullmatch(r"[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-7]", row["bdf"])):
+ raise RuntimeError("AMD inventory is malformed or partitioned")
+ values[row["uuid"]] = row
+ if (len(values) != len(rows) or len({r["gpu"] for r in rows}) != len(rows)
+ or len({r["bdf"] for r in rows}) != len(rows)):
+ raise RuntimeError("AMD inventory contains duplicate identities")
+ return values
+
+
+def hip_devices() -> list[str]:
+ # HIP and AMD SMI ordinals need not agree; join physical devices by PCI BDF.
+ observed = inventory(smi("list", 10))
+ by_bdf = {row["bdf"]: key for key, row in observed.items()}
+ hip = ctypes.CDLL("libamdhip64.so")
+ hip.hipGetDeviceCount.argtypes = [ctypes.POINTER(ctypes.c_int)]
+ hip.hipDeviceGetPCIBusId.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
+ count = ctypes.c_int()
+ if hip.hipGetDeviceCount(ctypes.byref(count)) != 0 or not 1 <= count.value <= 8:
+ raise RuntimeError("HIP device enumeration failed")
+ devices = []
+ for ordinal in range(count.value):
+ bdf = ctypes.create_string_buffer(32)
+ if hip.hipDeviceGetPCIBusId(bdf, len(bdf), ordinal) != 0:
+ raise RuntimeError("HIP PCI device identity unavailable")
+ key = bdf.value.decode().lower()
+ if key not in by_bdf:
+ raise RuntimeError("HIP device is absent from AMD SMI inventory")
+ devices.append(by_bdf[key])
+ if len(set(devices)) != len(devices):
+ raise RuntimeError("HIP device identities are duplicated")
+ return devices
+
+
+def number(value: object, unit: str, *, optional: bool = False) -> float | None:
+ if isinstance(value, dict) and value.get("unit") == unit:
+ raw = value.get("value")
+ if type(raw) in (int, float) and math.isfinite(raw) and raw >= 0:
+ return float(raw)
+ if optional:
+ return None
+ raise RuntimeError("AMD telemetry value or unit unavailable: " + unit)
+
+
+class AmdGpuProbe:
+ def __init__(self, devices: list[str], timeout: float):
+ self.devices, self.timeout = devices, timeout
+ monitor = os.environ.get("H3_AMD_MONITOR_RECEIPT")
+ self.monitor = json.loads(Path(monitor).read_text()) if monitor else None
+ self.identity = inventory(smi("list", timeout))
+ self.static = {row["gpu"]: row for row in smi("static", timeout)}
+ if not set(devices) <= self.identity.keys():
+ raise RuntimeError("Assigned AMD UUIDs absent from physical inventory")
+ for key in devices:
+ row = self.identity[key]
+ static = self.static[row["gpu"]]
+ if static["bus"]["bdf"] != row["bdf"] or static["asic"]["market_name"] != "AMD Instinct MI355X":
+ raise RuntimeError("AMD device identity changed or unsupported GPU model")
+
+ def power_configuration(self, *, deadline: float | None = None) -> dict:
+ _check_deadline(deadline)
+ timeout = self.timeout if deadline is None else min(self.timeout, max(0.001, deadline - time.monotonic()))
+ rows = {row["gpu"]: row for row in smi("static", timeout)}
+ return {"observed_at": _now(), "query": "amd-smi static --json", "status": "recorded",
+ "gpus": [{"uuid": key,
+ "configured_limit_w": number(rows[self.identity[key]["gpu"]]["limit"]["socket_power"], "W", optional=True),
+ "maximum_limit_w": number(rows[self.identity[key]["gpu"]]["limit"]["max_power"], "W", optional=True),
+ "enforced_limit_w": None, "default_limit_w": None} for key in self.devices]}
+
+ def snapshot(self, *, deadline: float | None = None) -> dict:
+ def query(option):
+ _check_deadline(deadline)
+ timeout = self.timeout if deadline is None else min(self.timeout, max(0.001, deadline - time.monotonic()))
+ return smi(option, timeout)
+ observed = inventory(query("list"))
+ if any(observed.get(key) != self.identity[key] for key in self.devices):
+ raise RuntimeError("AMD GPU inventory changed during measurement")
+ begin, utc = time.monotonic(), _now()
+ metric = query("metric")
+ end = time.monotonic()
+ rows = metric["gpu_data"]
+ metrics = {row["gpu"]: row for row in rows}
+ processes = query("process")
+ by_gpu = {row["gpu"]: row["process_list"] for row in processes}
+ if len(metrics) != len(rows) or len(by_gpu) != len(processes):
+ raise RuntimeError("AMD telemetry contains duplicate GPU records")
+ gpus, apps, monitors = [], [], []
+ for key in self.devices:
+ index = self.identity[key]["gpu"]
+ raw, static = metrics[index], self.static[index]
+ # AMD SMI 26.2 labels these MB but divides bytes by 1024**2.
+ # See ROCm/amdsmi rocm-7.1.1 amdsmi_commands.py mem_usage.
+ gpus.append({"uuid": key, "index": index, "name": static["asic"]["market_name"],
+ "memory_total_mib": number(raw["mem_usage"]["total_vram"], "MB"),
+ "memory_used_mib": number(raw["mem_usage"]["used_vram"], "MB"),
+ "utilization_percent": number(raw["usage"]["gfx_activity"], "%"),
+ "power_watts": number(raw["power"]["socket_power"], "W", optional=True),
+ "temperature_celsius": number(raw["temperature"]["hotspot"], "C", optional=True),
+ "driver_version": static["driver"]["version"], "mig_mode": "N/A",
+ "vendor": "amd", "pci_bdf": self.identity[key]["bdf"]})
+ for item in by_gpu[index]:
+ proc = item["process_info"]
+ if type(proc.get("pid")) is not int or proc["pid"] <= 0:
+ raise RuntimeError("AMD process identity unavailable")
+ memory = number(proc["memory_usage"]["vram_mem"], "B", optional=True)
+ app = {"gpu_uuid": key, "pid": proc["pid"],
+ "memory_used_mib": memory / 1024**2 if memory is not None else None}
+ if is_system_monitor(app, self.monitor):
+ monitors.append({**app, "identity": self.monitor})
+ else:
+ apps.append(app)
+ return {"at": _now(), "monotonic_seconds": time.monotonic(), "gpus": gpus, "compute_apps": apps,
+ "excluded_system_monitor_contexts": monitors,
+ "power_query": {"start_utc": utc, "start_monotonic_seconds": begin,
+ "end_monotonic_seconds": end, "field": "amd-smi power.socket_power"}}
diff --git a/experimental/video-generation/evaluator/mvp_gpu_evidence.py b/experimental/video-generation/evaluator/mvp_gpu_evidence.py
index c0af7c8245..fcbe83d1d3 100644
--- a/experimental/video-generation/evaluator/mvp_gpu_evidence.py
+++ b/experimental/video-generation/evaluator/mvp_gpu_evidence.py
@@ -134,6 +134,8 @@ def verify_measurement_job(directory: Path, *, deadline: float, require_success:
records = run.get("records", [])
if not isinstance(records, list) or len(records) != len(expected_slots):
raise ValueError("run must retain every planned warmup and measured outcome")
+ from .mvp_runtime_timing import verify_evidence as verify_server_timings
+ verify_server_timings(directory, role, run, required=spec.get("server_timing", False))
valid_measured = 0
attempted_seconds = 0.0
for record, slot in zip(records, expected_slots):
diff --git a/experimental/video-generation/evaluator/mvp_gpu_job.py b/experimental/video-generation/evaluator/mvp_gpu_job.py
index 3afbfdc906..0b08c90336 100644
--- a/experimental/video-generation/evaluator/mvp_gpu_job.py
+++ b/experimental/video-generation/evaluator/mvp_gpu_job.py
@@ -43,10 +43,15 @@
_SHA = re.compile(r"[0-9a-f]{64}")
_REV = re.compile(r"[0-9a-f]{40}")
_GPU = re.compile(r"GPU-[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
+_AMD_GPU = re.compile(r"[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}")
_LAUNCH = """import os
from evaluator.mvp_gpu_job import cuda_devices
-if cuda_devices() != os.environ["VGBENCH_GPU_UUIDS"].split(","):
- raise RuntimeError("Runtime CUDA device UUIDs differ from assigned GPUs")
+devices = cuda_devices
+if os.environ.get("VGBENCH_GPU_VENDOR") == "amd":
+ from evaluator.mvp_amd_gpu import hip_devices
+ devices = hip_devices
+if devices() != os.environ["VGBENCH_GPU_UUIDS"].split(","):
+ raise RuntimeError("Runtime device UUIDs differ from assigned GPUs")
from sglang.cli.main import main
main()
"""
@@ -154,7 +159,12 @@ def _number(value: Any, label: str, minimum: float, maximum: float, integer: boo
def validate_gpu_job(spec: dict) -> dict:
"""Pure validation: no filesystem probes, network, GPU calls, or execution."""
frozen = json.loads(canonical_json_bytes(spec))
- _keys(frozen, {"schema_version", "job_id", "authorization", "allocation", "gpu_uuids", "port", "lock_directory", "baseline", "candidate", "model", "server", "plan", "policy", "limits"}, "GPU job", optional={"serving"})
+ _keys(frozen, {"schema_version", "job_id", "authorization", "allocation", "gpu_uuids", "port", "lock_directory", "baseline", "candidate", "model", "server", "plan", "policy", "limits"}, "GPU job", optional={"serving", "gpu_vendor", "server_timing"})
+ if "server_timing" in frozen and type(frozen["server_timing"]) is not bool:
+ raise ValueError("server_timing must be an explicit boolean")
+ vendor = frozen.get("gpu_vendor", "nvidia")
+ if vendor not in {"nvidia", "amd"}:
+ raise ValueError("unsupported GPU vendor")
if "serving" in frozen:
from .mvp_serving import settings
load = frozen["serving"]
@@ -177,8 +187,9 @@ def validate_gpu_job(spec: dict) -> dict:
if allocation["mode"] not in {"cooperative_shared", "dedicated_ci"} or not isinstance(allocation["label"], str) or not allocation["label"].strip() or len(allocation["label"]) > 500:
raise ValueError("allocation requires an explicit operator-declared supported mode and label")
devices = frozen["gpu_uuids"]
- if not isinstance(devices, list) or not 1 <= len(devices) <= 8 or any(not isinstance(item, str) or not _GPU.fullmatch(item) for item in devices) or len(set(devices)) != len(devices):
- raise ValueError("gpu_uuids must contain 1–8 distinct full NVIDIA GPU UUIDs; MIG is unsupported")
+ uuid_pattern = _AMD_GPU if vendor == "amd" else _GPU
+ if not isinstance(devices, list) or not 1 <= len(devices) <= 8 or any(not isinstance(item, str) or not uuid_pattern.fullmatch(item) for item in devices) or len(set(devices)) != len(devices):
+ raise ValueError("gpu_uuids must contain 1–8 distinct full GPU UUIDs for the declared vendor; partitioned GPUs are unsupported")
_number(frozen["port"], "port", 1024, 65535, True)
_absolute(frozen["lock_directory"], "lock_directory")
for role in _ROLES:
@@ -214,7 +225,12 @@ def validate_gpu_job(spec: dict) -> dict:
frozen["model"]["files"] = sorted(files, key=lambda item: item["path"])
server = frozen["server"]
_keys(server, {"ulysses_degree", "tp_size", "encoder_parallel", "performance_mode"}, "server",
- optional={"dit_cpu_offload", "layerwise_offload"})
+ optional={"dit_cpu_offload", "layerwise_offload", "attention_backend"})
+ if vendor == "amd":
+ if server.get("attention_backend") != "aiter" or server["tp_size"] != 1 or server["ulysses_degree"] != len(devices):
+ raise ValueError("AMD H3 currently requires the documented AITER / pure Ulysses layout")
+ elif "attention_backend" in server:
+ raise ValueError("Explicit attention backend is currently supported only for AMD AITER")
_number(server["ulysses_degree"], "server.ulysses_degree", 1, len(devices), True)
_number(server["tp_size"], "server.tp_size", 1, len(devices), True)
if len(devices) % server["ulysses_degree"] or len(devices) % server["tp_size"]:
@@ -277,6 +293,8 @@ def _server_argv(spec: dict, role: str) -> list[str]:
"--dit-layerwise-resident-layers", str(layerwise["resident_layers"])])
else:
args.extend(["--dit-cpu-offload", str(spec["server"]["dit_cpu_offload"]).lower()])
+ if spec["server"].get("attention_backend"):
+ args.extend(["--attention-backend", spec["server"]["attention_backend"]])
return args
@@ -389,7 +407,7 @@ def ok(code):
return values
-def _runtime_env(source: str, gpu_uuids: list[str], nonce: str, cache: Path) -> dict[str, str]:
+def _runtime_env(source: str, gpu_uuids: list[str], nonce: str, cache: Path, gpu_vendor: str = "nvidia") -> dict[str, str]:
# Do not inherit authentication, PYTHONPATH, remote endpoints, LD_PRELOAD, or
# performance overrides. Never overwrite HOME/CODEX_HOME or user caches.
result = {key: os.environ[key] for key in ("PATH", "HOME", "LANG", "LC_ALL", "TMPDIR") if key in os.environ}
@@ -406,6 +424,13 @@ def _runtime_env(source: str, gpu_uuids: list[str], nonce: str, cache: Path) ->
"FLASHINFER_WORKSPACE_BASE": str(cache / "flashinfer"),
"HF_HOME": str(cache / "huggingface"), "HF_HUB_CACHE": str(cache / "huggingface" / "hub"),
"TRITON_CACHE_DIR": str(cache / "triton"), "CUDA_CACHE_PATH": str(cache / "cuda")})
+ if gpu_vendor == "amd":
+ mask = os.environ.get("ROCR_VISIBLE_DEVICES", "")
+ if not re.fullmatch(r"[0-7](?:,[0-7])*", mask) or len(set(mask.split(","))) != len(gpu_uuids):
+ raise ValueError("AMD runtime requires an explicit bound ROCR device mask")
+ logical = ",".join(str(index) for index in range(len(gpu_uuids)))
+ result.update(ROCR_VISIBLE_DEVICES=mask, HIP_VISIBLE_DEVICES=logical, CUDA_VISIBLE_DEVICES=logical,
+ VGBENCH_GPU_VENDOR="amd", SGLANG_USE_AITER="1")
return result
@@ -1053,11 +1078,22 @@ def _role(spec: dict, label: str, directory: Path, supervisor: _Supervisor, prob
cache = metadata / "cache"
cache.mkdir()
nonce = uuid.uuid4().hex
- env = _runtime_env(spec[label]["source"], spec["gpu_uuids"], nonce, cache)
+ env = _runtime_env(spec[label]["source"], spec["gpu_uuids"], nonce, cache, spec.get("gpu_vendor", "nvidia"))
role = receipt["roles"][label] = {"status": "preflight", "source_identity": None, "cleanup": {"status": "not_started"}, "run_path": f"{label}/run.json"}
_write(directory / "gpu-job.json", receipt)
identity = _source_identity(spec, label, env, supervisor.deadline, supervisor.cancelled)
role["source_identity"] = identity
+ if spec.get("server_timing"):
+ from . import mvp_runtime_timing as timing
+ timing.validate_source(Path(spec[label]["source"]))
+ timing_path = metadata / "server-timings.jsonl"
+ timing_path.touch(mode=0o600, exist_ok=False)
+ env.update(VGBENCH_SERVER_TIMING_PATH=str(timing_path.resolve()), VGBENCH_SERVER_TIMING_INSTANCE=nonce)
+ role["server_timing_evidence"] = {**timing.identity(), "instance_id": nonce,
+ "path": timing_path.relative_to(directory).as_posix(), "sha256": None,
+ "boundary": "HTTP handler receipt / accepted job / singleton forward / validated stored media ready",
+ "limitations": ["CPU monotonic boundaries; no additional GPU synchronization.",
+ "HTTP receipt follows framework routing/form parsing; excludes network ingress."]}
snapshot = probe.snapshot()
role["gpu_before"] = snapshot
if not _idle(snapshot, spec["limits"]["max_idle_memory_mib"]):
@@ -1069,10 +1105,14 @@ def _role(spec: dict, label: str, directory: Path, supervisor: _Supervisor, prob
selected = [indices[device] for device in spec["gpu_uuids"]]
if len(set(selected)) != len(selected) or any(type(index) is not int or index < 0 for index in selected):
raise RuntimeError("GPU inventory has invalid or duplicate runtime indices")
- env["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, selected))
+ if spec.get("gpu_vendor") != "amd":
+ env["CUDA_VISIBLE_DEVICES"] = ",".join(map(str, selected))
env["VGBENCH_GPU_UUIDS"] = ",".join(spec["gpu_uuids"])
role["runtime_gpu_binding"] = {"cuda_visible_devices": env["CUDA_VISIBLE_DEVICES"], "expected_gpu_uuids": spec["gpu_uuids"],
"verification": "child CUDA driver UUID check before SGLang import"}
+ if spec.get("gpu_vendor") == "amd":
+ role["runtime_gpu_binding"].update(rocr_visible_devices=env["ROCR_VISIBLE_DEVICES"],
+ hip_visible_devices=env["HIP_VISIBLE_DEVICES"], verification="child HIP PCI BDF to AMD SMI UUID check before SGLang import")
_port_available(spec["port"])
owner = None
sampler = None
@@ -1104,6 +1144,8 @@ def _role(spec: dict, label: str, directory: Path, supervisor: _Supervisor, prob
sampler.begin_measurement()
client_env = _runtime_env("", [], nonce, cache)
client_env["PYTHONPATH"] = str(Path(__file__).resolve().parent.parent)
+ if spec.get("server_timing"):
+ client_env.update({name: env[name] for name in ("VGBENCH_SERVER_TIMING_PATH", "VGBENCH_SERVER_TIMING_INSTANCE")})
hardware = ",".join(spec["gpu_uuids"])
argv = [sys.executable, "-m", "evaluator.cli", "run", str(directory / "plan.json"), "--runtime", "sglang",
"--endpoint", f"http://127.0.0.1:{spec['port']}", "--runtime-revision", spec[label]["revision"],
@@ -1158,6 +1200,11 @@ def _role(spec: dict, label: str, directory: Path, supervisor: _Supervisor, prob
role["cleanup"] = owner.close(deadline=cleanup_end)
role["cleanup"].update(status="failed", idle_after=False, reason="telemetry did not start; GPU idle unverified")
role["finished_at"] = _now()
+ if spec.get("server_timing"):
+ timing_path = directory / role["server_timing_evidence"]["path"]
+ with timing_path.open("rb") as timing_stream:
+ os.fsync(timing_stream.fileno())
+ role["server_timing_evidence"]["sha256"] = _hash(timing_path, supervisor.total_deadline)
role["power_configuration_after"] = probe.power_configuration(deadline=supervisor.total_deadline)
partial = directory / label / "run.json"
if partial.is_file():
@@ -1316,7 +1363,11 @@ def run_gpu_job(spec: dict, output_dir: Path, *, serving_smoke: bool = False) ->
_write(directory / "gpu-job.json", receipt)
receipt["model_identity"] = _model_manifest(spec, supervisor.deadline, supervisor.cancelled)
supervisor.check()
- probe = GpuProbe(spec["gpu_uuids"], spec["limits"]["command_seconds"])
+ if spec.get("gpu_vendor") == "amd":
+ from .mvp_amd_gpu import AmdGpuProbe
+ probe = AmdGpuProbe(spec["gpu_uuids"], spec["limits"]["command_seconds"])
+ else:
+ probe = GpuProbe(spec["gpu_uuids"], spec["limits"]["command_seconds"])
with GpuLease(Path(spec["lock_directory"]), spec["gpu_uuids"], spec["job_id"]) as lease:
try:
for role in (("baseline",) if serving_smoke else _ROLES):
diff --git a/experimental/video-generation/evaluator/mvp_power.py b/experimental/video-generation/evaluator/mvp_power.py
index 9926367dd6..b09061b2e9 100644
--- a/experimental/video-generation/evaluator/mvp_power.py
+++ b/experimental/video-generation/evaluator/mvp_power.py
@@ -174,6 +174,17 @@ def analyze_power(role: dict, run: dict, samples: list[dict], events: list[dict]
clock offset within 100 ms and journal/latency agreement within 250 ms.
"""
global_reasons = []
+ sensors = set()
+ for sample in samples:
+ query = sample.get("power_query")
+ field = query.get("field", "power.draw") if isinstance(query, dict) else "power.draw" if query is None else "unverified"
+ sensors.add(field if isinstance(field, str) else "unverified")
+ sensor = "nvidia-smi power.draw"
+ if sensors == {"amd-smi power.socket_power"}:
+ sensor = "amd-smi power.socket_power"
+ elif sensors - {"power.draw"}:
+ sensor = "unverified or mixed power sources"
+ global_reasons.append("power_sensor_source_inconsistent")
if not gpu_uuids or any(not isinstance(device, str) or not device for device in gpu_uuids) or len(set(gpu_uuids)) != len(gpu_uuids):
global_reasons.append("invalid_gpu_inventory")
if not _number(interval_seconds) or interval_seconds <= 0:
@@ -332,7 +343,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": sensor + "; 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_runner.py b/experimental/video-generation/evaluator/mvp_runner.py
index 64424c2d2b..11d09f0dfb 100644
--- a/experimental/video-generation/evaluator/mvp_runner.py
+++ b/experimental/video-generation/evaluator/mvp_runner.py
@@ -701,6 +701,8 @@ def attempt(slot: dict, *, defer_validation: bool = False) -> dict:
if defer_validation:
record["timing_window"]["transport_end_monotonic_seconds"] = time.monotonic()
_event(journal, "transport_finished", slot_id=record["slot_id"])
+ from .mvp_runtime_timing import collect
+ record["server_timings"] = collect(record["job_id"])
return record
order = {slot["slot_id"]: index for index, slot in enumerate(slots)}
diff --git a/experimental/video-generation/evaluator/mvp_runtime_timing.py b/experimental/video-generation/evaluator/mvp_runtime_timing.py
new file mode 100644
index 0000000000..ccc0569870
--- /dev/null
+++ b/experimental/video-generation/evaluator/mvp_runtime_timing.py
@@ -0,0 +1,185 @@
+"""Opt-in, request-correlated timing for the pinned H3 singleton runtime.
+
+The two-file runtime patch calls this stdlib-only module in HTTP and scheduler
+processes. No GPU synchronization, tracing backend, or network call is added.
+"""
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+from functools import lru_cache
+import hashlib
+import json
+import os
+from pathlib import Path
+import re
+import stat
+import time
+
+
+VERSION = "1.0.0"
+BASE_REVISION = "71de97b264b04dcd514cf904003028aefe9775c8"
+PATCH = Path(__file__).parents[1] / "runtime-patches/sglang-71de97b-h3-server-timing.patch"
+PATCHED_FILES = {
+ "python/sglang/multimodal_gen/runtime/entrypoints/openai/video_api.py": "27c4c13ad1417161d0b3f9f9cfaa4a259c86bdc3614450a43e8e07f87c37ed05",
+ "python/sglang/multimodal_gen/runtime/managers/scheduler.py": "1cfb5d6db81dd8cfb1eea7444971f92140639fc98bb8b4a882a2ab2ba65cab6b",
+}
+STAGES = ("http_received", "http_accepted", "scheduler_dispatch", "forward_finished", "media_ready")
+DURATIONS = {
+ "prequeue_seconds": ("http_received", "http_accepted"),
+ "queue_delay_seconds": ("http_accepted", "scheduler_dispatch"),
+ "execution_seconds": ("scheduler_dispatch", "forward_finished"),
+ "postprocess_seconds": ("forward_finished", "media_ready"),
+ "server_ready_latency_seconds": ("http_received", "media_ready"),
+}
+_SAFE_ID = re.compile(r"[A-Za-z0-9_-][A-Za-z0-9_.:-]{0,199}")
+_MAX_BYTES = 4 * 1024 * 1024
+
+
+def identity() -> dict:
+ return {"schema_version": VERSION, "base_runtime_revision": BASE_REVISION,
+ "patch_sha256": hashlib.sha256(PATCH.read_bytes()).hexdigest(),
+ "helper_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+ "patched_runtime_files": dict(PATCHED_FILES)}
+
+
+def validate_source(source: Path) -> None:
+ for name, expected in PATCHED_FILES.items():
+ if hashlib.sha256((source / name).read_bytes()).hexdigest() != expected:
+ raise ValueError("server timing requires the exact committed H3 instrumentation patch")
+
+
+@lru_cache(maxsize=1)
+def _clock_id() -> str:
+ # Linux boot + time namespace identify a shared monotonic clock across the
+ # HTTP and GPU-worker processes. Never subtract these from client clocks.
+ boot = Path("/proc/sys/kernel/random/boot_id").read_text().strip()
+ namespace = os.readlink("/proc/self/ns/time")
+ return f"linux:{boot}:{namespace}:CLOCK_MONOTONIC"
+
+
+def emit(request_id: str, event: str, **fields) -> None:
+ path = os.environ.get("VGBENCH_SERVER_TIMING_PATH")
+ if not path:
+ return
+ timestamp = time.monotonic_ns()
+ if not isinstance(request_id, str) or not _SAFE_ID.fullmatch(request_id) or event not in STAGES:
+ raise ValueError("invalid H3 server timing event identity")
+ value = {"schema_version": VERSION, "request_id": request_id, "event": event,
+ "monotonic_ns": timestamp, "clock_id": _clock_id(), "pid": os.getpid(),
+ "instance_id": os.environ["VGBENCH_SERVER_TIMING_INSTANCE"], **fields}
+ payload = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + b"\n"
+ descriptor = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_NOFOLLOW)
+ try:
+ # One small O_APPEND write per event. The supervisor owns/fsyncs the
+ # final ledger; per-request fsync would perturb the measured workload.
+ if os.write(descriptor, payload) != len(payload):
+ raise OSError("incomplete H3 timing event write")
+ finally:
+ os.close(descriptor)
+
+
+@contextmanager
+def forward(requests: list, *, replica_id: int, leader: bool):
+ enabled = bool(os.environ.get("VGBENCH_SERVER_TIMING_PATH")) and leader
+ if enabled:
+ if len(requests) != 1 or requests[0].num_outputs_per_prompt != 1:
+ raise ValueError("H3 timing patch only qualifies singleton forward execution")
+ request_id = requests[0].request_id
+ emit(request_id, "scheduler_dispatch", observed_batch_size=len(requests), replica_id=replica_id)
+ try:
+ yield
+ finally:
+ if enabled:
+ emit(request_id, "forward_finished")
+
+
+def read_events(path: Path) -> list[dict]:
+ descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
+ with os.fdopen(descriptor, "rb") as stream:
+ if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
+ raise ValueError("server timing ledger must be a regular file")
+ data = stream.read(_MAX_BYTES + 1)
+ if len(data) > _MAX_BYTES:
+ raise ValueError("server timing ledger exceeds its size bound")
+ # Another request may be writing the final line. Its incomplete event is
+ # unavailable until the next read, never a fabricated complete timestamp.
+ events = [json.loads(line) for line in data.split(b"\n")[:-1]]
+ if any(not isinstance(event, dict) for event in events):
+ raise ValueError("server timing events must be objects")
+ return events
+
+
+def derive(events: list[dict], request_id: str, instance_id: str) -> dict | None:
+ selected = [row for row in events if row.get("request_id") == request_id]
+ if not selected:
+ return None
+ stages = {}
+ clock_ids = set()
+ for row in selected:
+ stage, ns = row.get("event"), row.get("monotonic_ns")
+ if (row.get("schema_version") != VERSION or row.get("instance_id") != instance_id
+ or stage not in STAGES or stage in stages or type(ns) is not int or ns <= 0
+ or not isinstance(row.get("clock_id"), str) or not row["clock_id"]):
+ raise ValueError("invalid, duplicate or mismatched server timing event")
+ stages[stage] = row
+ clock_ids.add(row["clock_id"])
+ ordered = [stages[name]["monotonic_ns"] for name in STAGES if name in stages]
+ if len(clock_ids) != 1 or ordered != sorted(ordered):
+ raise ValueError("server timing clocks differ or stages are reversed")
+ dispatch = stages.get("scheduler_dispatch", {})
+ batch, replica = dispatch.get("observed_batch_size"), dispatch.get("replica_id")
+ if dispatch and (type(batch) is not int or batch != 1 or type(replica) is not int or replica < 0):
+ raise ValueError("invalid observed H3 batch or replica identity")
+ result = {"schema_version": VERSION, "status": "complete" if len(stages) == len(STAGES) else "partial",
+ "request_id": request_id, "instance_id": instance_id, "clock_id": next(iter(clock_ids)),
+ "clock": "time.monotonic_ns; same Linux boot and time namespace; nanoseconds",
+ "observed_batch_size": batch, "replica_id": replica,
+ "timestamps_ns": {name: stages.get(name, {}).get("monotonic_ns") for name in STAGES}}
+ for name, (begin, end) in DURATIONS.items():
+ result[name] = ((stages[end]["monotonic_ns"] - stages[begin]["monotonic_ns"]) / 1e9
+ if begin in stages and end in stages else None)
+ return result
+
+
+def collect(request_id: str | None) -> dict | None:
+ path = os.environ.get("VGBENCH_SERVER_TIMING_PATH")
+ if not path or request_id is None:
+ return None
+ try:
+ return derive(read_events(Path(path)), request_id, os.environ["VGBENCH_SERVER_TIMING_INSTANCE"])
+ except (OSError, ValueError, TypeError, KeyError):
+ # Preserve the video outcome. The independent evidence verifier rejects
+ # malformed timing rather than making generation appear to have failed.
+ return {"schema_version": VERSION, "status": "invalid", "request_id": request_id}
+
+
+def verify_evidence(directory: Path, role: dict, run: dict, *, required: bool = False) -> None:
+ evidence = role.get("server_timing_evidence")
+ if not evidence:
+ if required or any(record.get("server_timings") is not None for record in run["records"]):
+ raise ValueError("server timings lack a supervisor-owned evidence ledger")
+ return
+ if (evidence.get("schema_version") != VERSION or evidence.get("base_runtime_revision") != BASE_REVISION
+ or evidence.get("patch_sha256") != hashlib.sha256(PATCH.read_bytes()).hexdigest()
+ or evidence.get("patched_runtime_files") != PATCHED_FILES
+ or any(not isinstance(evidence.get(key), str) or not re.fullmatch(r"[0-9a-f]{64}", evidence[key])
+ for key in ("patch_sha256", "helper_sha256"))
+ or evidence.get("instance_id") != role.get("process_identity", {}).get("launch_nonce")):
+ raise ValueError("server timing instrumentation identity is missing or inconsistent")
+ raw = evidence.get("path")
+ if (not isinstance(raw, str) or Path(raw).is_absolute() or ".." in Path(raw).parts
+ or "\\" in raw):
+ raise ValueError("invalid server timing ledger path")
+ path = (directory / raw).resolve(strict=True)
+ if not path.is_relative_to(directory.resolve()):
+ raise ValueError("server timing ledger escapes its job directory")
+ events = read_events(path)
+ if path.stat().st_size > _MAX_BYTES or hashlib.sha256(path.read_bytes()).hexdigest() != evidence.get("sha256"):
+ raise ValueError("server timing ledger hash mismatch")
+ for record in run["records"]:
+ expected = derive(events, record.get("job_id"), evidence["instance_id"])
+ if record.get("server_timings") != expected:
+ raise ValueError("server timings differ from request-correlated raw events")
+ if required and record.get("status") == "succeeded" and (expected or {}).get("status") != "complete":
+ raise ValueError("successful request lacks complete server timing evidence")
diff --git a/experimental/video-generation/evaluator/mvp_serving.py b/experimental/video-generation/evaluator/mvp_serving.py
index 71896fdf38..e30d2ac34a 100644
--- a/experimental/video-generation/evaluator/mvp_serving.py
+++ b/experimental/video-generation/evaluator/mvp_serving.py
@@ -76,7 +76,7 @@ def summarize(run: dict) -> dict:
on_time = sum(value <= deadline for value in values) if deadline is not None and (complete or not valid) else None
durations = [(r.get("media") or {}).get("video", {}).get("duration_seconds") for r in valid]
seconds = sum(durations) if all(_finite(value) and value > 0 for value in durations) else None
- return {
+ result = {
**load, "capacity_qualified": False,
"client_ready_latency_seconds": {
"values": values, "sample_count": len(values), "valid_clip_count": len(valid),
@@ -104,3 +104,25 @@ def summarize(run: dict) -> dict:
"Client polling observations are not server-side queue or execution timestamps.",
"Deadline goodput requires technical validity, not calibrated perceptual quality."],
}
+ if any(record.get("server_timings") is not None for record in records):
+ def distribution(field):
+ samples = [(record.get("server_timings") or {}).get(field) for record in valid]
+ present = [value for value in samples if _finite(value) and value >= 0]
+ ordered = sorted(present) if len(present) == len(samples) else []
+ return {"values": sorted(present), "sample_count": len(present), "valid_clip_count": len(valid),
+ "missing_count": len(samples) - len(present),
+ "p50": statistics.median(ordered) if ordered else None,
+ "p90": ordered[math.ceil(len(ordered) * .9) - 1] if len(ordered) >= 10 else None,
+ "p95": ordered[math.ceil(len(ordered) * .95) - 1] if len(ordered) >= 20 else None}
+
+ result.update(queue_delay_seconds=distribution("queue_delay_seconds"),
+ server_ready_latency_seconds=distribution("server_ready_latency_seconds"),
+ server_execution_seconds=distribution("execution_seconds"),
+ server_prequeue_seconds=distribution("prequeue_seconds"),
+ server_postprocess_seconds=distribution("postprocess_seconds"))
+ timings = [record.get("server_timings") or {} for record in valid]
+ complete = bool(timings) and all(value.get("status") == "complete" for value in timings)
+ result["observed_batch_sizes"] = [value["observed_batch_size"] for value in timings] if complete else None
+ result["observed_replica_ids"] = sorted({value["replica_id"] for value in timings}) if complete else None
+ result["server_timing_population"] = "technically valid measured requests; partial coverage withholds percentiles"
+ return result
diff --git a/experimental/video-generation/evaluator/mvp_serving_smoke.py b/experimental/video-generation/evaluator/mvp_serving_smoke.py
index 7a08483b9f..6bf633d154 100644
--- a/experimental/video-generation/evaluator/mvp_serving_smoke.py
+++ b/experimental/video-generation/evaluator/mvp_serving_smoke.py
@@ -18,10 +18,19 @@
CONCURRENCIES = (1, 2, 4)
+def validate_concurrencies(values) -> tuple[int, ...]:
+ if (not isinstance(values, (list, tuple)) or not values
+ or any(type(value) is not int or value not in CONCURRENCIES for value in values)
+ or len(set(values)) != len(values)):
+ raise ValueError("select unique concurrency values from 1, 2, 4")
+ return tuple(values)
+
+
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 +57,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 | '
@@ -60,18 +69,21 @@ def _report(root: Path, matrix: dict) -> None:
+ '', encoding="utf-8")
-def run_matrix(spec: dict, root: Path) -> dict:
+def run_matrix(spec: dict, root: Path, *, concurrencies=CONCURRENCIES) -> dict:
from .mvp_power import analyze_power
spec = validate_spec(spec)
+ concurrencies = validate_concurrencies(concurrencies)
+ 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}}
- for concurrency in CONCURRENCIES]}
+ "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)
try:
@@ -93,19 +105,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/export_ci.py b/experimental/video-generation/export_ci.py
index 9874e82cd4..45f970dccb 100644
--- a/experimental/video-generation/export_ci.py
+++ b/experimental/video-generation/export_ci.py
@@ -57,9 +57,9 @@ def verified_execution(run_id: str, *, inventory: bool = False) -> tuple[dict, d
and ((run["status"] == "completed" and run["conclusion"] == "success") or current_export),
"Source must be a successful manual InferenceX execution or this run's completed H3 job")
jobs = api(f"actions/runs/{run_id}/attempts/{run['run_attempt']}/jobs")
- job_name = "H3 H200 hardware inventory" if inventory else "H3 video H200 smoke"
+ job_pattern = re.escape("H3 H200 hardware inventory") if inventory else r"H3 video (?:H100|H200|B200|MI355X) smoke"
selected = [job for job in jobs["jobs"] if re.fullmatch(
- r"(?:h3-video / )?p[0-9]+(?:\.[0-9]+)? \| " + re.escape(job_name), job["name"])]
+ r"(?:h3-video / )?p[0-9]+(?:\.[0-9]+)? \| " + job_pattern, job["name"])]
ci.need(len(selected) == 1 and selected[0]["status"] == "completed"
and selected[0]["conclusion"] == "success", "Source lacks a successful H3 Slurm job")
name = f"h3-{'hardware' if inventory else 'video'}-{run_id}-{run['run_attempt']}"
diff --git a/experimental/video-generation/inspect_amd_node.py b/experimental/video-generation/inspect_amd_node.py
new file mode 100644
index 0000000000..6ad873494b
--- /dev/null
+++ b/experimental/video-generation/inspect_amd_node.py
@@ -0,0 +1,185 @@
+"""Bounded AMD node inventory using the existing Slurm ownership receipts."""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+from pathlib import Path
+import pwd
+import re
+import shutil
+import subprocess
+
+import ci
+
+
+def step_gpu_indices(value: str) -> set[int]:
+ indices = set()
+ for part in value.split(","):
+ ci.need(re.fullmatch(r"\d{1,2}(?:-\d{1,2})?", part) is not None, "Missing AMD step GPU assignment")
+ limits = [int(item) for item in part.split("-")]
+ first, last = limits[0], limits[-1]
+ ci.need(0 <= first <= last < 8, "Unexpected AMD step GPU assignment")
+ indices.update(range(first, last + 1))
+ return indices
+
+
+def observation(argv: list[str]) -> dict:
+ try:
+ result = subprocess.run(argv, capture_output=True, text=True, timeout=30)
+ return {"argv": argv, "exit_code": result.returncode, "stdout": result.stdout, "stderr": result.stderr}
+ except (OSError, subprocess.TimeoutExpired) as error:
+ return {"argv": argv, "error": str(error)}
+
+
+def inspect_node(run_dir: Path) -> None:
+ context = ci.read(run_dir / "context.json")
+ job = context["allocation"]["identity"]["JobId"]
+ ci.need(os.environ.get("SLURM_JOB_ID") == job
+ and os.environ.get("SLURMD_NODENAME") == context["node"], "Wrong AMD inventory allocation")
+ ci.need(step_gpu_indices(os.environ.get("SLURM_STEP_GPUS", "")) == set(range(8)),
+ "AMD inventory requires the full eight-GPU step binding")
+ binding = {"job_id": job, "step_id": os.environ.get("SLURM_STEP_ID"),
+ "node": context["node"], "cpu_affinity": sorted(os.sched_getaffinity(0)),
+ "slurm": {name: os.environ.get(name) for name in
+ ("SLURM_JOB_GPUS", "SLURM_STEP_GPUS", "ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES")}}
+ ci.write(run_dir / "binding.json", binding)
+ from evaluator.mvp_amd_gpu import observe_system_monitor
+ ci.write(run_dir / "amd-system-monitor.json", observe_system_monitor(10))
+ commands = [["uname", "-a"], ["enroot", "list", "-f"], ["srun", "--help"],
+ ["bash", "-c", "command -v amd-smi rocm-smi rocminfo python3; ls -ld /opt/rocm* /var/lib/enroot /run/enroot /etc/enroot 2>/dev/null"]]
+ smi = shutil.which("amd-smi")
+ if not smi and Path("/opt/rocm/bin/amd-smi").is_file():
+ smi = "/opt/rocm/bin/amd-smi"
+ if smi:
+ commands += [[smi, option, "--json"] for option in ("list", "static", "metric", "process")]
+ commands += [[smi, "version", "--json"]]
+ processes = observation([smi, "process", "--json"])
+ if processes.get("exit_code") == 0:
+ pids = {item["process_info"]["pid"] for gpu in json.loads(processes["stdout"])
+ for item in gpu.get("process_list", []) if isinstance(item.get("process_info", {}).get("pid"), int)}
+ identities = []
+ for pid in sorted(pids):
+ proc = Path("/proc") / str(pid)
+ identity = {"pid": pid}
+ for field in ("exe", "comm", "status", "cgroup"):
+ try:
+ identity[field] = str((proc / field).resolve(strict=True)) if field == "exe" else (proc / field).read_text().strip()
+ except OSError as error:
+ identity[field + "_error"] = str(error)
+ identities.append(identity)
+ ci.write(run_dir / "observed-processes.json", identities)
+ cache = Path("/var/lib/squash")
+ images = [{"path": str(p), "size_bytes": p.stat().st_size} for p in sorted(cache.glob("*sglang*rocm*.sqsh"))]
+ ci.write(run_dir / "node-inventory.json", {
+ "schema_version": 1, "bundle_type": "h3_amd_node_inventory", "binding": binding,
+ "cached_images": images, "observations": [observation(argv) for argv in commands],
+ "generation_executed": False, "runtime_compatibility": "not_tested", "observed_at": ci.now(),
+ })
+ if context.get("prepare_runtime"):
+ from prepare_amd_runtime import prepare_on_node
+ prepare_on_node(Path(context["workspace"]), run_dir)
+
+
+def inspect(workspace: Path, output: Path, *, prepare_runtime: bool = False, serving_continuation: bool = False) -> int:
+ ci.need(not serving_continuation or prepare_runtime, "Serving continuation requires runtime inspection")
+ ci.need(workspace.is_absolute(), "Persistent workspace must be absolute")
+ run_id, attempt = os.environ["H3_RUN_ID"], os.environ["H3_RUN_ATTEMPT"]
+ ci.need(run_id.isdigit() and attempt.isdigit(), "Invalid CI identity")
+ root = workspace / "results/h3-cross-hardware"
+ root.mkdir(parents=True, exist_ok=True)
+ run_dir = root / (f"github-{run_id}-{attempt}" + ("-runtime" if serving_continuation else ""))
+ run_dir.mkdir(exist_ok=False)
+ account = pwd.getpwuid(os.getuid()).pw_name
+ ci.need(account == "cameronamd@semianalysis.com", "Unexpected AMD scheduler identity")
+ config = {"task_id": "h3-cross-hardware", "mode": "serving-smoke", "allocation_receipts": [],
+ "site": {"cluster": "mi355x-amds", "partition": "compute", "account": account, "gpu_model": "MI355X"},
+ "resources": {"gpus": 8, "allocated_gpus": 8, "cpus": 8, "memory_gb": 64, "minutes": 15}}
+ if prepare_runtime:
+ config["resources"].update(cpus=32, memory_gb=256, minutes=60)
+ if serving_continuation:
+ config["resources"].update(memory_gb=1024, minutes=120)
+ state = {"status": "starting", "started_at": ci.now(), "config": config,
+ "purpose": "Observe actual AMD device identities, telemetry formats and cached runtimes before adding a GPU adapter",
+ "source_sha": os.environ.get("H3_SOURCE_SHA"), "generation_executed": False}
+ receipt, reused, code = None, False, 2
+ control = workspace / "campaigns/h3-cross-hardware"
+ control.mkdir(parents=True, exist_ok=True)
+ with ci.task_lock(control / ".node-inventory.lock"):
+ try:
+ if prepare_runtime:
+ from prepare_amd_runtime import prepare_source, interrupted_rootfs, CONTAINER, IMAGE
+ prepare_source(workspace)
+ rootfs = workspace.parent / "enroot-data" / CONTAINER
+ if rootfs.is_dir():
+ origin = ci.read(rootfs.with_suffix(".image.json"))
+ if origin == {"image": str(IMAGE), "status": "creating"}:
+ interrupted_rootfs(workspace)
+ else:
+ ci.need(origin in ({"image": str(IMAGE), "status": "created"},
+ {"image": str(IMAGE), "status": "recovered"}), "AMD rootfs identity differs")
+ if not serving_continuation:
+ config["resources"]["minutes"] = 15
+ ci.need(not serving_continuation or rootfs.is_dir(), "Serving continuation requires the retained rootfs before allocation")
+ recovery = ci.recover(config, root)
+ ci.write(run_dir / "recovery.json", recovery)
+ ci.need(recovery["action"] != "wait", "Task-owned AMD allocation is waiting; do not submit another")
+ if recovery["action"] == "reuse":
+ ci.need(not recovery["active_steps"].strip(), "Task-owned AMD allocation has active steps")
+ receipt, reused = recovery["receipt"], True
+ ci.write(run_dir / "allocation.json", receipt)
+ else:
+ receipt = ci.allocate(config, run_dir)
+ record = ci.job_record(receipt["identity"]["JobId"])
+ state.update(allocation=receipt, allocation_reused=reused, slurm_job=record)
+ ci.write(run_dir / "slurm-job.json", record)
+ ci.verify_identity(receipt, record, config["task_id"])
+ ci.need(record["JobState"] == "RUNNING", "Owned AMD allocation is " + record["JobState"])
+ # AMD AllocTRES omits GPU accounting even for a granted --gres=gpu:8
+ # request. This read-only inventory checks CPU/memory/time here and
+ # verifies the full eight-GPU Slurm step binding before device queries.
+ reason = ci.capacity(record, {**config["resources"], "gpus": 0})
+ ci.need(reason is None, "Owned AMD allocation: " + str(reason))
+ ci.write(run_dir / "context.json", {"allocation": receipt, "node": record["NodeList"],
+ "workspace": str(workspace), "prepare_runtime": prepare_runtime})
+ # The source checkout and result directory are on the shared filesystem.
+ step_minutes = 10 if serving_continuation else config["resources"]["minutes"] - 5
+ argv = ["srun", "--jobid=" + record["JobId"], "--nodelist=" + record["NodeList"],
+ "--nodes=1", "--ntasks=1", "--gres=gpu:8", "--cpus-per-task=" + str(config["resources"]["cpus"]), "--cpu-bind=cores",
+ "--time=" + str(step_minutes), "--export=NONE", "/usr/bin/python3", str(Path(__file__).resolve()),
+ "--inside", str(run_dir)]
+ ci.write(run_dir / "step-command.json", argv)
+ code = ci.run_step(argv, run_dir / "srun.log", (step_minutes + 1) * 60)
+ ci.need(code == 0 and (run_dir / "node-inventory.json").is_file(), "AMD inventory step failed; inspect retained logs")
+ state["status"] = "complete"
+ except Exception as error:
+ state.update(status="failed", error=str(error))
+ code = 2
+ finally:
+ if receipt is None and (run_dir / "allocation.json").exists():
+ receipt = ci.read(run_dir / "allocation.json")
+ try:
+ if receipt:
+ state["step_cleanup"] = ci.drain_step(receipt, config["task_id"], run_dir)
+ retain = reused or (serving_continuation and state["status"] == "complete")
+ state["allocation_cleanup"] = {"status": "retained"} if retain else ci.stop_allocation(receipt, config["task_id"])
+ except Exception as error:
+ state.update(status="failed", cleanup_error=str(error))
+ code = 2
+ state.update(finished_at=ci.now(), exit_code=code)
+ ci.write(run_dir / "inventory-status.json", state)
+ ci.collect(run_dir, output)
+ return code
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--workspace", type=Path)
+ parser.add_argument("--output", type=Path)
+ parser.add_argument("--inside", type=Path)
+ parser.add_argument("--prepare-runtime", action="store_true")
+ args = parser.parse_args()
+ if args.inside:
+ inspect_node(args.inside)
+ else:
+ raise SystemExit(inspect(args.workspace, args.output, prepare_runtime=args.prepare_runtime))
diff --git a/experimental/video-generation/prepare_amd_runtime.py b/experimental/video-generation/prepare_amd_runtime.py
new file mode 100644
index 0000000000..85c4dcfa3a
--- /dev/null
+++ b/experimental/video-generation/prepare_amd_runtime.py
@@ -0,0 +1,228 @@
+"""Prepare one task-owned cached ROCm runtime; generation is a separate gate."""
+from __future__ import annotations
+
+import argparse
+import os
+from pathlib import Path
+import subprocess
+
+import ci
+
+REVISION = "71de97b264b04dcd514cf904003028aefe9775c8"
+IMAGE = Path("/var/lib/squash/lmsysorg_sglang-rocm_v0.5.18-rocm720-mi35x-20260828.sqsh")
+CONTAINER = "wenyao-minimax-h3-rocm"
+
+
+CPU_PROBE = r'''
+import importlib, importlib.metadata as metadata, json, sys
+from pathlib import Path
+result = {"python": sys.executable, "packages": {}, "imports": {}, "gpu_execution": False}
+for name in ("torch", "torchvision", "av", "numpy", "diffusers", "transformers", "sglang", "aiter", "triton", "amdsmi"):
+ try:
+ module = importlib.import_module(name)
+ result["imports"][name] = {"path": getattr(module, "__file__", None)}
+ try: result["packages"][name] = metadata.version(name)
+ except metadata.PackageNotFoundError: pass
+ except Exception as error:
+ result["imports"][name] = {"error": str(error)}
+Path(sys.argv[1]).write_text(json.dumps(result, indent=2) + "\n")
+'''
+
+
+def interrupted_rootfs(workspace: Path) -> None:
+ rootfs = workspace.parent / "enroot-data" / CONTAINER
+ previous = workspace / "results/h3-cross-hardware/github-34344130223-1"
+ ci.need(rootfs.is_dir() and rootfs.stat().st_uid == os.getuid(), "Task-owned partial rootfs missing")
+ ci.need(ci.read(rootfs.with_suffix(".image.json")) == {"image": str(IMAGE), "status": "creating"},
+ "Unexpected rootfs creation receipt")
+ failed = ci.read(previous / "inventory-status.json")
+ ci.need(failed["allocation_cleanup"]["status"] == "released", "Prior allocation cleanup is not recorded")
+ log = (previous / "srun.log").read_text()
+ ci.need("Ignoring xattrs in filesystem" in log and "created 464452 files" in log
+ and "created 11757 symlinks" in log, "Prior extraction did not reach the recorded completion footer")
+ ci.need((rootfs / "etc/rc").is_file(), "Extracted Enroot entrypoint missing")
+
+
+def recover_rootfs(workspace: Path, output: Path) -> None:
+ root = workspace.parent
+ rootfs = root / "enroot-data" / CONTAINER
+ origin = rootfs.with_suffix(".image.json")
+ previous = workspace / "results/h3-cross-hardware/github-34344130223-1"
+ record = {"rootfs": str(rootfs), "image": str(IMAGE), "source_revision": REVISION,
+ "started_at": ci.now(), "gpu_allocation": False, "gpu_execution": False,
+ "predecessor_run": str(previous), "new_extraction": False}
+ output.mkdir(parents=True, exist_ok=True)
+ control = workspace / "campaigns/h3-cross-hardware"
+ with ci.task_lock(control / ".node-inventory.lock"), ci.task_lock(root / ".session.lock"):
+ try:
+ interrupted_rootfs(workspace)
+ record["entrypoint"] = (rootfs / "etc/rc").read_text()
+ prepare_source(workspace)
+ env = {**os.environ, "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "ENROOT_DATA_PATH": str(root / "enroot-data"), "ENROOT_CACHE_PATH": str(root / "cache"),
+ "ENROOT_RUNTIME_PATH": str(root / "runtime-cpu-recovery"), "ENROOT_TEMP_PATH": str(root / "tmp-cpu-recovery"),
+ "ROCR_VISIBLE_DEVICES": "", "HIP_VISIBLE_DEVICES": "", "CUDA_VISIBLE_DEVICES": ""}
+ record["permission_normalization"] = "Prior full-tree normalization exceeded 300s; test entry before repairing any specific permission"
+ probe = output / "runtime-cpu-probe.py"
+ probe.write_text(CPU_PROBE)
+ persistent = control / f"rootfs-recovery-{os.environ['H3_RUN_ID']}-{os.environ['H3_RUN_ATTEMPT']}"
+ persistent.mkdir(exist_ok=False)
+ (persistent / probe.name).write_text(CPU_PROBE)
+ inside = Path("/work") / persistent.relative_to(workspace)
+ argv = ["/usr/local/bin/enroot", "start", "--rw", "--mount", str(workspace) + ":/work",
+ "--env", "PYTHONDONTWRITEBYTECODE=1", "--env", "SGLANG_USE_AITER=1",
+ "--env", "PYTHONPATH=/work/runtime-sglang-" + REVISION + "/python",
+ "--env", "ROCR_VISIBLE_DEVICES=", "--env", "HIP_VISIBLE_DEVICES=", "--env", "CUDA_VISIBLE_DEVICES=",
+ CONTAINER, "python3", str(inside / probe.name), str(inside / "runtime-cpu-probe.json")]
+ ci.write(output / "runtime-cpu-command.json", argv)
+ subprocess.run(argv, env=env, check=True, timeout=300)
+ result = ci.read(persistent / "runtime-cpu-probe.json")
+ ci.write(output / "runtime-cpu-probe.json", result)
+ record.update(status="recovered", probe=result,
+ compatibility="CPU entry and imports only; HIP and H3 generation unverified")
+ ci.write(origin, {"image": str(IMAGE), "status": "recovered"})
+ ci.write(control / "rootfs-recovered.json", record)
+ except Exception as error:
+ record.update(status="failed", error=str(error))
+ raise
+ finally:
+ record["finished_at"] = ci.now()
+ ci.write(output / "rootfs-recovery.json", record)
+
+
+def prepare_source(workspace: Path) -> Path:
+ source = workspace / ("runtime-sglang-" + REVISION)
+ if not source.exists():
+ subprocess.run(["git", "init", "-b", "feat/h3-runtime-preparation", str(source)], check=True)
+ subprocess.run(["git", "-C", str(source), "remote", "add", "origin", "https://github.com/sgl-project/sglang.git"], check=True)
+ subprocess.run(["git", "-C", str(source), "fetch", "--depth", "1", "origin", REVISION], check=True, timeout=300)
+ subprocess.run(["git", "-C", str(source), "checkout", "--detach", REVISION], check=True)
+ ci.need(ci.command(["git", "-C", str(source), "rev-parse", "HEAD"]).strip() == REVISION, "Prepared AMD source has a different revision")
+ ci.need(not ci.command(["git", "-C", str(source), "status", "--porcelain", "--untracked-files=all"]).strip(), "Prepared AMD source is not clean")
+ return source
+
+
+PROBE = r'''
+import ctypes, importlib, importlib.metadata as metadata, json, os, shutil, subprocess, sys
+from pathlib import Path
+out = Path(sys.argv[1])
+result = {"python": sys.executable, "packages": {}, "imports": {}, "generation_executed": False}
+for name in ("torch", "torchvision", "av", "numpy", "diffusers", "transformers", "sglang", "aiter", "triton", "amdsmi"):
+ try:
+ module = importlib.import_module(name)
+ result["imports"][name] = {"path": getattr(module, "__file__", None)}
+ try: result["packages"][name] = metadata.version(name)
+ except metadata.PackageNotFoundError: pass
+ except Exception as error:
+ result["imports"][name] = {"error": str(error)}
+try:
+ import torch
+ result["torch_hip"] = torch.version.hip
+ result["torch_devices"] = [{"ordinal": i, "name": torch.cuda.get_device_name(i),
+ "total_memory_bytes": torch.cuda.get_device_properties(i).total_memory} for i in range(torch.cuda.device_count())]
+ hip = ctypes.CDLL("libamdhip64.so")
+ hip.hipGetDeviceCount.argtypes = [ctypes.POINTER(ctypes.c_int)]
+ hip.hipDeviceGetPCIBusId.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
+ count = ctypes.c_int()
+ assert hip.hipGetDeviceCount(ctypes.byref(count)) == 0
+ devices = []
+ for i in range(count.value):
+ bdf = ctypes.create_string_buffer(32)
+ assert hip.hipDeviceGetPCIBusId(bdf, len(bdf), i) == 0
+ devices.append({"hip_ordinal": i, "pci_bdf": bdf.value.decode().lower()})
+ result["hip_devices"] = devices
+except Exception as error:
+ result["device_error"] = str(error)
+result["smi_observations"] = []
+smi = shutil.which("amd-smi")
+if smi:
+ for option in ("version", "list", "static", "metric", "process"):
+ try:
+ call = subprocess.run([smi, option, "--json"], capture_output=True, text=True, timeout=20)
+ result["smi_observations"].append({"option": option, "path": smi, "exit_code": call.returncode,
+ "stdout": call.stdout, "stderr": call.stderr})
+ except Exception as error:
+ result["smi_observations"].append({"option": option, "error": str(error)})
+try:
+ import amdsmi
+ amdsmi.amdsmi_init()
+ result["smi_memory_bytes"] = [{"pci_bdf": amdsmi.amdsmi_get_gpu_device_bdf(handle),
+ "uuid": amdsmi.amdsmi_get_gpu_device_uuid(handle),
+ "total_bytes": amdsmi.amdsmi_get_gpu_memory_total(handle, amdsmi.AmdSmiMemoryType.VRAM),
+ "used_bytes": amdsmi.amdsmi_get_gpu_memory_usage(handle, amdsmi.AmdSmiMemoryType.VRAM)}
+ for handle in amdsmi.amdsmi_get_processor_handles()]
+ amdsmi.amdsmi_shut_down()
+except Exception as error:
+ result["smi_api_error"] = str(error)
+result["environment"] = {k: os.environ.get(k) for k in ("ROCR_VISIBLE_DEVICES", "HIP_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES", "PYTHONPATH")}
+out.write_text(json.dumps(result, indent=2) + "\n")
+print(json.dumps(result))
+'''
+
+
+def prepare_on_node(workspace: Path, run_dir: Path) -> None:
+ # Called only after inspect_amd_node has verified this job, node and all 8 GPUs.
+ root = workspace.parent
+ enroot = Path("/usr/local/bin/enroot")
+ ci.need(enroot.is_file(), "Expected approved /usr/local/bin/enroot is unavailable")
+ rootfs = root / "enroot-data" / CONTAINER
+ record = {"rootfs": str(rootfs), "existed": rootfs.is_dir(), "image": str(IMAGE),
+ "source_revision": REVISION, "generation_executed": False, "started_at": ci.now()}
+ ci.write(run_dir / "runtime-preparation.json", record)
+ env = os.environ.copy()
+ env.update(PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ ENROOT_DATA_PATH=str(root / "enroot-data"), ENROOT_CACHE_PATH=str(root / "cache"),
+ ENROOT_RUNTIME_PATH=str(root / "runtime-preparation"), ENROOT_TEMP_PATH=str(root / "tmp-preparation"),
+ ENROOT_MAX_PROCESSORS="8", PYTHONDONTWRITEBYTECODE="1")
+ for key in ("ENROOT_DATA_PATH", "ENROOT_CACHE_PATH", "ENROOT_RUNTIME_PATH", "ENROOT_TEMP_PATH"):
+ Path(env[key]).mkdir(parents=True, exist_ok=True)
+ origin = rootfs.with_suffix(".image.json")
+ if rootfs.is_dir():
+ if origin.is_file() and ci.read(origin) == {"image": str(IMAGE), "status": "creating"}:
+ interrupted_rootfs(workspace)
+ record["recovery"] = "Reuse completed extraction after CPU runner denied user namespaces; no extraction or full-tree permission rescan"
+ else:
+ ci.need(origin.is_file() and ci.read(origin) in (
+ {"image": str(IMAGE), "status": "created"}, {"image": str(IMAGE), "status": "recovered"}),
+ "Existing AMD rootfs has no completed task-owned image receipt; inspect before reuse")
+ if not rootfs.is_dir():
+ ci.need(IMAGE.is_file(), "Validated cached ROCm image is missing on this node")
+ record.update(create_reason="Task-owned named rootfs is missing", image_size_bytes=IMAGE.stat().st_size)
+ ci.write(run_dir / "runtime-preparation.json", record)
+ ci.write(origin, {"image": str(IMAGE), "status": "creating"})
+ subprocess.run([str(enroot), "create", "--name", CONTAINER, str(IMAGE)], env=env, check=True, timeout=2400)
+ ci.write(origin, {"image": str(IMAGE), "status": "created"})
+ rc = rootfs / "etc/rc"
+ record["entrypoint"] = rc.read_text() if rc.is_file() else None
+ ci.write(run_dir / "runtime-preparation.json", record)
+ probe = run_dir / "runtime-probe.py"
+ probe.write_text(PROBE)
+ container_dir = Path("/work") / run_dir.relative_to(workspace)
+ source = Path("/work") / ("runtime-sglang-" + REVISION)
+ # Discover the image's Python and retained version module before importing the
+ # pinned checkout. No package upgrades or model inference occur in this step.
+ script = 'set -eu; command -v python3; python3 "$1" "$2"'
+ entry = ["bash", "-c", script, "probe", str(container_dir / probe.name), str(container_dir / "runtime-probe.json")]
+ if record["entrypoint"] and 'exec bash "$@"' in record["entrypoint"]:
+ entry = entry[1:]
+ argv = [str(enroot), "start", "--rw", "--mount", str(workspace) + ":/work",
+ "--mount", "/dev/kfd:/dev/kfd", "--mount", "/dev/dri:/dev/dri",
+ "--env", "PYTHONDONTWRITEBYTECODE=1", "--env", "SGLANG_USE_AITER=1",
+ "--env", "PYTHONPATH=" + str(source / "python"),
+ "--env", "ROCR_VISIBLE_DEVICES=" + os.environ["ROCR_VISIBLE_DEVICES"], CONTAINER, *entry]
+ ci.write(run_dir / "runtime-command.json", argv)
+ subprocess.run(argv, env=env, check=True, timeout=300)
+ result = ci.read(run_dir / "runtime-probe.json")
+ ci.write(origin, {"image": str(IMAGE), "status": "recovered"})
+ record.update(finished_at=ci.now(), probe=result,
+ status="inspected", compatibility="Imports and device enumeration only; H3 generation untested")
+ ci.write(run_dir / "runtime-preparation.json", record)
+ ci.write(workspace / "campaigns/h3-cross-hardware/runtime-inspected.json", record)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--workspace", required=True, type=Path)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ recover_rootfs(args.workspace, args.output)
diff --git a/experimental/video-generation/run_amd_serving_ci.py b/experimental/video-generation/run_amd_serving_ci.py
new file mode 100644
index 0000000000..e56d84b31a
--- /dev/null
+++ b/experimental/video-generation/run_amd_serving_ci.py
@@ -0,0 +1,147 @@
+"""Keep AMD runtime inspection, warmup and twenty C1 requests in one lease."""
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timedelta, timezone
+import os
+from pathlib import Path
+import shutil
+import signal
+
+import ci
+import inspect_amd_node
+import stage_amd_site
+from prepare_amd_runtime import prepare_source
+from stage_model_ci import source_spec
+
+
+def allocation_budget(job: dict) -> tuple[int, datetime]:
+ end = datetime.fromisoformat(job["EndTime"]).replace(tzinfo=timezone.utc)
+ minutes = min(110, int((end - datetime.now(timezone.utc)).total_seconds() // 60) - 1)
+ ci.need(minutes >= 75, "Fewer than 75 serving minutes remain; do not start the twenty-request campaign")
+ ready_by = datetime.fromisoformat(job["StartTime"]).replace(tzinfo=timezone.utc) + timedelta(minutes=15)
+ ci.need(ready_by > datetime.now(timezone.utc), "Allocation-wide runtime readiness deadline expired")
+ return minutes, ready_by
+
+
+def ready_before(run_dir: Path, deadline: datetime) -> dict:
+ path = run_dir / "gpu/c1/gpu-job.json"
+ ci.need(path.is_file(), "AMD runtime was not ready within 15 minutes of allocation start")
+ role = ci.read(path).get("roles", {}).get("baseline", {})
+ seconds = role.get("startup_seconds")
+ start = role.get("startup_timing_window", {}).get("start_utc")
+ ci.need(type(seconds) in (int, float) and seconds >= 0 and isinstance(start, str),
+ "AMD runtime readiness was not recorded before the deadline")
+ ready_at = datetime.fromisoformat(start) + timedelta(seconds=seconds)
+ ci.need(ready_at <= deadline, "AMD runtime exceeded the 15-minute readiness deadline")
+ return {"ready_at": ready_at.isoformat(), "deadline": deadline.isoformat(),
+ "evidence": "gpu/c1/gpu-job.json: roles.baseline.startup_timing_window and startup_seconds"}
+
+
+def run(source_run_id: str, output: Path) -> int:
+ workspace = stage_amd_site.WORKSPACE
+ run_id = f"github-{os.environ['H3_RUN_ID']}-{os.environ['H3_RUN_ATTEMPT']}"
+ control = workspace / "campaigns/h3-cross-hardware" / run_id
+ control.mkdir(parents=True, exist_ok=False)
+ output.mkdir(parents=True, exist_ok=True)
+ preparation = control / "preparation"
+ run_dir = workspace / "results/h3-cross-hardware" / run_id
+ prep_dir = run_dir.with_name(run_id + "-runtime")
+ record = {"status": "preparing", "started_at": ci.now(), "allocation_minutes_cap": 120,
+ "allocated_gpus": 8, "participating_gpus": 4, "planned_measured_requests": 20,
+ "warmups": 1, "runtime_probe_seconds_cap": 660, "allocation_to_readiness_seconds_cap": 900,
+ "cleanup_reserve_minutes": 10, "replacement_allocation_allowed": False}
+ receipt, owned, code, ready_by = None, False, 2, None
+ def interrupted(signum, frame):
+ raise InterruptedError("AMD serving orchestration interrupted")
+ def readiness_expired(signum, frame):
+ record["readiness"] = ready_before(run_dir, ready_by)
+ previous = {sig: signal.signal(sig, handler) for sig, handler in
+ ((signal.SIGTERM, interrupted), (signal.SIGALRM, readiness_expired))}
+ try:
+ source_output = control / "source"
+ source_output.mkdir()
+ spec, record["source_provenance"] = source_spec(source_run_id, source_output)
+ ci.need(spec["plan"] == ci.read(stage_amd_site.INPUTS / "formal-8s-plan.json"),
+ "Source workload differs from the frozen twenty-request plan; no allocation requested")
+ stage_amd_site.timing_source(prepare_source(workspace))
+ code = inspect_amd_node.inspect(workspace, preparation, prepare_runtime=True, serving_continuation=True)
+ inspected = ci.read(preparation / "inventory-status.json")
+ receipt = inspected.get("allocation")
+ owned = receipt is not None and not inspected.get("allocation_reused", False)
+ record["runtime_inspection"] = inspected
+ ci.need(code == 0, "AMD runtime inspection failed; measured requests were not started")
+ stage_amd_site.runtime_probe(ci.read(workspace / "campaigns/h3-cross-hardware/runtime-inspected.json"))
+ job = ci.job_record(receipt["identity"]["JobId"])
+ ci.verify_identity(receipt, job, "h3-cross-hardware")
+ minutes, ready_by = allocation_budget(job)
+ record["readiness_deadline"] = ready_by.isoformat()
+ signal.setitimer(signal.ITIMER_REAL, (ready_by - datetime.now(timezone.utc)).total_seconds())
+ staged = control / "prepared-site"
+ staged.mkdir()
+ with ci.task_lock(workspace / "campaigns/h3-cross-hardware/.site-preparation.lock"):
+ site = stage_amd_site.stage(spec, staged, server_timing=True, allocation_minutes=minutes,
+ destination=control / "formal-c1")
+ config = ci.read(site["site_config"])
+ record.update(status="running", prepared_site=site, allocation=receipt,
+ serving_minutes_cap=minutes, supervisor_seconds_cap=minutes * 60 - 600)
+ ci.write(control / "amd-serving.json", record)
+ code = ci.launch(config, output, required_allocation=receipt["identity"]["JobId"])
+ if code == 0:
+ record["readiness"] = ready_before(run_dir, ready_by)
+ record["status"] = "complete" if code == 0 else "failed"
+ except (Exception, KeyboardInterrupt) as error:
+ record.update(status="failed", error=str(error))
+ code = 2
+ finally:
+ signal.setitimer(signal.ITIMER_REAL, 0)
+ try:
+ # Recover the persistent receipt even when artifact collection itself failed.
+ inspected = ci.read(prep_dir / "inventory-status.json") if (prep_dir / "inventory-status.json").is_file() else {}
+ if receipt is None:
+ receipt = inspected.get("allocation")
+ if receipt is None and (prep_dir / "allocation.json").is_file():
+ receipt = ci.read(prep_dir / "allocation.json")
+ recovery = ci.read(prep_dir / "recovery.json") if (prep_dir / "recovery.json").is_file() else {}
+ owned = receipt is not None and recovery.get("action") != "reuse"
+ if receipt and owned:
+ record["allocation_cleanup"] = (inspected["allocation_cleanup"]
+ if inspected.get("allocation_cleanup", {}).get("status") == "released"
+ else ci.stop_allocation(receipt, "h3-cross-hardware"))
+ elif receipt:
+ record["allocation_cleanup"] = {"status": "retained", "reason": "Borrowed allocation remains with its owner"}
+ except Exception as error:
+ record.update(status="failed", cleanup_error=str(error))
+ code = 2
+ record.update(finished_at=ci.now(), exit_code=code)
+ try:
+ ci.write(control / "amd-serving.json", record)
+ run_dir.mkdir(parents=True, exist_ok=True)
+ ci.write(run_dir / "amd-serving.json", record)
+ if prep_dir.is_dir():
+ ci.collect(prep_dir, run_dir / "preparation")
+ if (control / "prepared-site").is_dir():
+ shutil.copytree(control / "prepared-site", run_dir / "prepared-site", dirs_exist_ok=True)
+ if (run_dir / "ci.json").is_file():
+ state = ci.read(run_dir / "ci.json")
+ state.update(allocation_cleanup=record.get("allocation_cleanup"), exit_code=code)
+ if code != 0:
+ state.update(phase="failed", ci_accepted=False)
+ ci.write(run_dir / "ci.json", state)
+ manifest = ci.read(run_dir / "manifest.json")
+ manifest.update(exit_code=code, allocation_cleanup=record.get("allocation_cleanup"))
+ manifest["evidence"].update({name: ci.digest(run_dir / name) for name in ("ci.json", "amd-serving.json")})
+ ci.write(run_dir / "manifest.json", manifest)
+ ci.collect(run_dir, output)
+ finally:
+ for sig, handler in previous.items():
+ signal.signal(sig, handler)
+ return code
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--source-run-id", required=True)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ raise SystemExit(run(args.source_run_id, args.output))
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/runtime-patches/sglang-71de97b-h3-server-timing.patch b/experimental/video-generation/runtime-patches/sglang-71de97b-h3-server-timing.patch
new file mode 100644
index 0000000000..f9e8b9667e
--- /dev/null
+++ b/experimental/video-generation/runtime-patches/sglang-71de97b-h3-server-timing.patch
@@ -0,0 +1,55 @@
+--- a/python/sglang/multimodal_gen/runtime/entrypoints/openai/video_api.py
++++ b/python/sglang/multimodal_gen/runtime/entrypoints/openai/video_api.py
+@@ -6,6 +6,7 @@
+ import shutil
+ import tempfile
+ import time
++from evaluator import mvp_runtime_timing as vgbench_timing
+ from collections.abc import Coroutine
+ from contextlib import suppress
+ from typing import Any, Dict, Optional
+@@ -527,6 +528,7 @@
+ update_fields, request_id=job_id, result=result
+ )
+ update_fields.update(final_media_fields)
++ vgbench_timing.emit(job_id, "media_ready")
+ await VIDEO_STORE.update_fields(job_id, update_fields)
+ except Exception as e:
+ logger.exception("Video job %s failed", job_id)
+@@ -599,6 +601,7 @@
+ ):
+ content_type = request.headers.get("content-type", "").lower()
+ request_id = generate_request_id()
++ vgbench_timing.emit(request_id, "http_received")
+
+ server_args = get_global_server_args()
+ task_type = server_args.pipeline_config.task_type
+@@ -861,6 +864,7 @@
+ )
+ job.update(sampling_params.project_video_queued_job_fields(batch))
+ await VIDEO_STORE.upsert(request_id, job)
++ vgbench_timing.emit(request_id, "http_accepted")
+ except Exception as e:
+ if batch is not None:
+ try:
+--- a/python/sglang/multimodal_gen/runtime/managers/scheduler.py
++++ b/python/sglang/multimodal_gen/runtime/managers/scheduler.py
+@@ -4,6 +4,7 @@
+ import dataclasses
+ import pickle
+ import time
++from evaluator import mvp_runtime_timing as vgbench_timing
+ from collections import deque
+ from contextlib import contextmanager
+ from copy import deepcopy
+@@ -303,7 +304,9 @@
+ )
+
+ if len(reqs) == 1 or not allow_dynamic_batching:
+- return self.worker.execute_forward(reqs)
++ with vgbench_timing.forward(reqs, replica_id=self.dp_replica,
++ leader=self.receiver is not None):
++ return self.worker.execute_forward(reqs)
+
+ if self.server_args.pipeline_config.supports_native_grouped_requests():
+ return self._execute_generation_grouped(reqs)
diff --git a/experimental/video-generation/site_preflight.py b/experimental/video-generation/site_preflight.py
new file mode 100644
index 0000000000..12d66f38e5
--- /dev/null
+++ b/experimental/video-generation/site_preflight.py
@@ -0,0 +1,134 @@
+"""Retain runner identity so SSH trust can be checked against exact CI provenance."""
+
+import argparse
+import json
+import os
+from pathlib import Path
+import platform
+import pwd
+import shutil
+import subprocess
+
+
+def amd_runtime_observations() -> dict:
+ """A failed extraction can leave reusable files; inspect before rebuilding."""
+ root = Path("/it-share/data/wenyao-minimax-h3")
+ rootfs = root / "enroot-data/wenyao-minimax-h3-rocm"
+ files = {}
+ for path in (rootfs.with_suffix(".image.json"), rootfs / "etc/rc",
+ rootfs / "etc/os-release", root / "work/campaigns/h3-cross-hardware/model-ready.json"):
+ files[str(path)] = path.read_text()[:16000] if path.is_file() else None
+ entries = {}
+ for name in ("usr/bin/python3", "usr/local/bin/python3", "opt/venv/bin/python3", "bin/bash", "etc/rc", "dev", "proc", "sys"):
+ path = rootfs / name
+ entries[name] = {"exists": path.exists(), "symlink": str(path.readlink()) if path.is_symlink() else None}
+ installed_sources = {}
+ for directory in (Path("/usr/local/lib/enroot"), Path("/usr/lib/enroot")):
+ for path in sorted(directory.glob("*.sh")):
+ lines = path.read_text().splitlines()
+ selected = set()
+ for i, line in enumerate(lines):
+ if any(word in line for word in ("unsquashfs", "runtime::create", "mksquashfs", "xattr")):
+ selected.update(range(max(0, i - 4), min(len(lines), i + 60)))
+ if selected:
+ installed_sources[str(path)] = "\n".join(f"{i + 1}: {lines[i]}" for i in sorted(selected))[:30000]
+ return {"rootfs": str(rootfs), "exists": rootfs.is_dir(), "files": files,
+ "entries": entries, "installed_enroot_sources": installed_sources,
+ "gpu_allocation": False, "rootfs_changed": False}
+
+
+def allocation_observations(root: Path) -> list[dict]:
+ """Read this task's saved scheduler receipts without requesting resources."""
+ records = []
+ for path in sorted(root.glob("*/allocation.json"))[-8:]:
+ receipt = json.loads(path.read_text())
+ identity = receipt.get("identity", {})
+ job = identity.get("JobId", "")
+ if receipt.get("task_id") != "h3-cross-hardware" or not str(job).isdigit():
+ continue
+ result = subprocess.run(["scontrol", "show", "job", "-o", str(job)],
+ capture_output=True, text=True, timeout=10,
+ env={**os.environ, "TZ": "UTC", "LC_ALL": "C"})
+ records.append({"receipt": str(path), "identity": identity, "exit_code": result.returncode,
+ "stdout": result.stdout, "stderr": result.stderr})
+ if result.returncode:
+ accounting = subprocess.run(
+ ["sacct", "-X", "-j", str(job), "--noheader", "--parsable2",
+ "--format=JobID,State,Start,End,AllocCPUS,AllocTRES,ReqTRES,NodeList,Elapsed,ExitCode"],
+ capture_output=True, text=True, timeout=10,
+ env={**os.environ, "TZ": "UTC", "LC_ALL": "C"})
+ records[-1]["accounting"] = {"exit_code": accounting.returncode, "stdout": accounting.stdout,
+ "stderr": accounting.stderr}
+ return records
+
+
+def inspect_site() -> dict:
+ user = pwd.getpwuid(os.getuid())
+ public_keys = {}
+ for algorithm in ("ed25519", "ecdsa", "rsa"):
+ path = Path(f"/etc/ssh/ssh_host_{algorithm}_key.pub")
+ if path.is_file():
+ public_keys[algorithm] = path.read_text().strip()
+ known_jumpbox = subprocess.run(
+ ["ssh-keygen", "-F", "64.139.223.123"], capture_output=True, text=True, timeout=5,
+ ) if shutil.which("ssh-keygen") else None
+ associations = subprocess.run(
+ ["sacctmgr", "-nP", "show", "assoc", "where", f"user={user.pw_name}",
+ "format=Account,Partition,QOS,DefaultQOS"], capture_output=True, text=True, timeout=10,
+ ) if shutil.which("sacctmgr") else None
+ defaults = subprocess.run(
+ ["sacctmgr", "-nP", "show", "user", "where", f"name={user.pw_name}",
+ "format=User,DefaultAccount"], capture_output=True, text=True, timeout=10,
+ ) if shutil.which("sacctmgr") else None
+ active_accounts = subprocess.run(
+ ["squeue", "--noheader", "--user=" + user.pw_name, "--format=%a"],
+ capture_output=True, text=True, timeout=10,
+ ) if shutil.which("squeue") else None
+ # Shared runtime/model cache candidates are metadata, not proof of compatibility.
+ candidates = {}
+ for name in ("/var/lib/squash", "/it-share/data", "/it-share/wenyao-minimax-h3",
+ "/data/home/sa-shared/wenyao-minimax-h3"):
+ path = Path(name)
+ candidates[name] = sorted(item.name for item in path.iterdir()
+ if any(word in item.name.lower() for word in ("sglang", "rocm", "h3", "minimax"))) if path.is_dir() else None
+ enroot_config = Path("/etc/enroot/enroot.conf")
+ enroot_paths = [line.strip() for line in enroot_config.read_text().splitlines()
+ if line.strip().startswith(("ENROOT_DATA_PATH", "ENROOT_CACHE_PATH", "ENROOT_RUNTIME_PATH"))] if enroot_config.is_file() else None
+ storage = {}
+ for name in ("/it-share", "/it-share/data", "/it-share/hf-hub-cache", "/it-share/gharunners2", user.pw_dir):
+ path = Path(name)
+ if path.is_dir():
+ info = path.stat()
+ mount = subprocess.run(["findmnt", "--target", name, "--noheadings", "--output", "TARGET,SOURCE,FSTYPE"],
+ capture_output=True, text=True, timeout=5) if shutil.which("findmnt") else None
+ storage[name] = {"uid": info.st_uid, "gid": info.st_gid, "mode": oct(info.st_mode & 0o777),
+ "writable": os.access(path, os.W_OK), "free_bytes": shutil.disk_usage(path).free,
+ "mount": mount.stdout.strip() if mount and mount.returncode == 0 else None}
+ else:
+ storage[name] = None
+ return {
+ "schema_version": 1, "bundle_type": "h3_site_preflight_no_gpu",
+ "hostname": platform.node(), "uid": os.getuid(), "username": user.pw_name,
+ "user_home": user.pw_dir,
+ "cluster": os.environ.get("H3_CLUSTER"), "runner": os.environ.get("RUNNER_NAME"),
+ "ci": {key: os.environ.get(key) for key in ("GITHUB_REPOSITORY", "GITHUB_SHA", "GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT")},
+ "commands": {name: shutil.which(name) for name in ("salloc", "srun", "enroot", "amd-smi", "nvidia-smi")},
+ "ssh_host_public_keys": public_keys,
+ "previously_known_amd_jumpbox": known_jumpbox.stdout if known_jumpbox and known_jumpbox.returncode == 0 else None,
+ "scheduler_associations": associations.stdout if associations and associations.returncode == 0 else None,
+ "scheduler_default_account": defaults.stdout if defaults and defaults.returncode == 0 else None,
+ "scheduler_active_accounts": sorted(set(active_accounts.stdout.split())) if active_accounts and active_accounts.returncode == 0 else None,
+ "runtime_candidates": candidates, "enroot_paths": enroot_paths, "persistent_storage": storage,
+ "saved_allocations": allocation_observations(Path("/it-share/data/wenyao-minimax-h3/work/results/h3-cross-hardware"))
+ if os.environ.get("H3_CLUSTER") == "mi355x-amds" and shutil.which("scontrol") else [],
+ "amd_runtime": amd_runtime_observations() if os.environ.get("H3_CLUSTER") == "mi355x-amds" else None,
+ "gpu_execution": False, "runtime_compatibility": "not_tested",
+ }
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ (args.output / "site-preflight.json").write_text(json.dumps(inspect_site(), indent=2) + "\n")
diff --git a/experimental/video-generation/stage_amd_site.py b/experimental/video-generation/stage_amd_site.py
new file mode 100644
index 0000000000..f93f8bc215
--- /dev/null
+++ b/experimental/video-generation/stage_amd_site.py
@@ -0,0 +1,148 @@
+"""Seal AMD C1 inputs after inspection, without allocating or installing."""
+from __future__ import annotations
+
+import argparse
+import copy
+import hashlib
+import json
+import os
+from pathlib import Path
+import subprocess
+
+import ci
+from evaluator.mvp_gpu_job import source_file_manifest, validate_gpu_job
+from prepare_amd_runtime import CONTAINER, REVISION
+from stage_model_ci import source_spec
+
+WORKSPACE = Path("/it-share/data/wenyao-minimax-h3/work")
+INPUTS = Path(__file__).parent / "campaigns/h3-cross-hardware"
+
+
+def runtime_probe(record: dict) -> dict:
+ ci.need(record.get("status") in {"inspected", "recovered"} and record.get("source_revision") == REVISION,
+ "AMD runtime inspection is missing or has a different source")
+ probe = record["probe"]
+ required = ("torch", "torchvision", "av", "numpy", "diffusers", "transformers", "sglang", "aiter", "triton", "amdsmi")
+ missing = [name for name in required if not probe.get("imports", {}).get(name, {}).get("path")]
+ ci.need(not missing, "AMD runtime imports require preparation: " + ", ".join(missing))
+ if record["status"] == "inspected":
+ ci.need(probe.get("torch_hip") and not probe.get("device_error")
+ and len(probe.get("hip_devices", [])) == 8, "AMD HIP device enumeration is not verified")
+ devices = probe.get("torch_devices", [])
+ ci.need(len(devices) == 8 and all("MI355X" in item["name"] for item in devices),
+ "Runtime is not the inspected eight-MI355X node")
+ ci.need(Path(probe["python"]).is_absolute(), "Inspected Python path must be absolute")
+ return probe
+
+
+def timing_source(source: Path) -> tuple[Path, dict]:
+ from evaluator import mvp_runtime_timing as timing
+ destination = source.with_name(source.name + "-timing")
+ if not destination.exists():
+ subprocess.run(["git", "-C", str(source), "worktree", "add", "-b", "feat/h3-amd-serving-timing",
+ str(destination), REVISION], check=True)
+ subprocess.run(["git", "-C", str(destination), "apply", "--check", str(timing.PATCH)], check=True)
+ subprocess.run(["git", "-C", str(destination), "apply", str(timing.PATCH)], check=True)
+ subprocess.run(["git", "-C", str(destination), "add", *timing.PATCHED_FILES], check=True)
+ subprocess.run(["git", "-C", str(destination), "-c", "user.name=H3 Benchmark", "-c", "user.email=h3-benchmark@localhost",
+ "commit", "-m", "feat: record request-correlated H3 serving stages",
+ "-m", "记录 H3 请求的服务端阶段时间,保留原始运行时作为基线。"], check=True)
+ timing.validate_source(destination)
+ return destination, timing.identity()
+
+
+def stage(spec: dict, output: Path, *, server_timing: bool = False, allocation_minutes: int = 110, destination: Path | None = None) -> dict:
+ workspace = WORKSPACE
+ control = workspace / "campaigns/h3-cross-hardware"
+ readiness = control / "runtime-inspected.json"
+ if not readiness.is_file():
+ readiness = control / "rootfs-recovered.json"
+ runtime = ci.read(readiness)
+ probe = runtime_probe(runtime)
+ model = ci.read(control / "model-ready.json")
+ entries = spec["model"]["files"]
+ manifest = hashlib.sha256(json.dumps(entries, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
+ ci.need(model.get("status") == "complete" and model.get("manifest_sha256") == manifest
+ and model.get("model_revision") == spec["model"]["revision"],
+ "Prepared AMD weights differ from the frozen source")
+ rootfs = workspace.parent / "enroot-data" / CONTAINER
+ ci.need(runtime["rootfs"] == str(rootfs) and rootfs.is_dir(), "Prepared AMD rootfs is missing or changed")
+ source = workspace / ("runtime-sglang-" + REVISION)
+ identity = source_file_manifest(source)
+ ci.need(identity["revision"] == REVISION, "AMD source revision changed after inspection")
+ instrumentation = None
+ if server_timing:
+ source, instrumentation = timing_source(source)
+ identity = source_file_manifest(source)
+ destination = destination or control / ("formal-c1-timing-v1" if server_timing else "formal-c1-v1")
+ ci.need(not destination.exists(), "AMD formal inputs already exist; inspect and reuse the sealed configuration")
+ source_server = copy.deepcopy(spec["server"])
+ spec = copy.deepcopy(spec)
+ if server_timing:
+ spec["server_timing"] = True
+ plan = ci.read(INPUTS / "formal-8s-plan.json")
+ spec.update(gpu_vendor="amd", job_id=plan["plan_id"], plan=plan,
+ gpu_uuids=[f"00000000-0000-0000-0000-{i:012d}" for i in range(4)],
+ lock_directory="/work/campaigns/h3-cross-hardware/control/gpu-locks", port=30317,
+ serving={"mode": "closed_loop", "concurrency": 1, "delivery_deadline_seconds": None})
+ spec["server"] = {"tp_size": 1, "ulysses_degree": 4, "encoder_parallel": "auto",
+ "performance_mode": "speed", "dit_cpu_offload": False, "attention_backend": "aiter"}
+ ci.need(30 <= allocation_minutes <= 110, "AMD serving requires 30–110 remaining allocation minutes")
+ spec["limits"].update(job_seconds=(allocation_minutes - 10) * 60, startup_seconds=900, request_seconds=900,
+ cleanup_seconds=60, command_seconds=30, telemetry_interval_seconds=1)
+ spec["authorization"]["approval_reference"] = (
+ "User authorized MI355X recovery, one warmup and exactly twenty measured attempts on 2026-09-09. "
+ "Retain source model-license approval. AMD initial C1 uses 20 measured requests and one separate warmup, "
+ "8 allocated GPUs and 4 participating GPUs, one allocation capped at 120 minutes, "
+ f"with {allocation_minutes} minutes remaining for serving and ten minutes reserved for cleanup. "
+ "Generation compatibility is unverified; retain failures and release the owned allocation.")
+ spec["model"]["path"] = str(Path("/work") / Path(model["model_path"]).relative_to(workspace))
+ for role in ("baseline", "candidate"):
+ spec[role] = {"source": "/work/" + source.name, "source_sha256": identity["source_sha256"],
+ "revision": identity["revision"], "python": probe["python"]}
+ spec = validate_gpu_job(spec)
+ destination.mkdir()
+ entry = destination / "entry-only.sh"
+ command = '-c \'exec "$@"\' h3-entry "$@"' if 'exec bash "$@"' in (runtime.get("entrypoint") or "") else '"$@"'
+ entry.write_text((INPUTS / "entry-only-amd.sh").read_text().replace("@ENTRY@", command)
+ .replace("runtime-inspected.json", readiness.name))
+ ci.write(destination / "gpu-spec.json", spec)
+ config = {"schema_version": 1, "task_id": "h3-cross-hardware", "site": ci.AMD_SITE,
+ "workspace": {"host": str(workspace), "container": "/work"},
+ "runtime": {"entry": str(entry), "entry_sha256": ci.digest(entry), "rootfs": str(rootfs),
+ "ready_marker": str(readiness), "python": probe["python"]},
+ "spec": {"path": str(destination / "gpu-spec.json"), "sha256": ci.digest(destination / "gpu-spec.json")},
+ "resources": {"gpus": 4, "allocated_gpus": 8, "cpus": 32, "memory_gb": 1024, "minutes": allocation_minutes},
+ "allocation_receipts": [], "mode": "serving-smoke", "concurrencies": [1]}
+ config = ci.validate_config(config)
+ ci.prepared_spec(config)
+ ci.write(destination / "site.json", config)
+ for name in ("entry-only.sh", "gpu-spec.json", "site.json"):
+ (output / name).write_bytes((destination / name).read_bytes())
+ return {"site_config": str(destination / "site.json"), "source": identity,
+ "runtime_inspection": runtime, "instrumentation": instrumentation,
+ "server_configuration_deviation": {"source_server": source_server, "executed_server": spec["server"],
+ "reason": "AMD uses TP1/Ulysses4 with AITER; NVIDIA source settings are retained for explicit comparison."},
+ "model_receipt": model, "generation_executed": False,
+ "status": "prepared", "compatibility": "Imports checked; allocated HIP identity and full video/audio warmup remain mandatory before measurement"}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--source-run-id", required=True)
+ parser.add_argument("--output", required=True, type=Path)
+ parser.add_argument("--server-timing", action="store_true")
+ args = parser.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ record = {"status": "preparing", "generation_executed": False,
+ "ci": {key: os.environ.get(key) for key in ("H3_RUN_ID", "H3_SOURCE_SHA")}}
+ try:
+ spec, provenance = source_spec(args.source_run_id, args.output)
+ record.update(provenance)
+ with ci.task_lock(WORKSPACE / "campaigns/h3-cross-hardware/.site-preparation.lock"):
+ record.update(stage(spec, args.output, server_timing=args.server_timing))
+ except Exception as error:
+ record.update(status="failed", error=str(error))
+ raise
+ finally:
+ ci.write(args.output / "site-preparation.json", record)
diff --git a/experimental/video-generation/stage_model_ci.py b/experimental/video-generation/stage_model_ci.py
new file mode 100644
index 0000000000..154720e553
--- /dev/null
+++ b/experimental/video-generation/stage_model_ci.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""Stage the frozen model from an accepted H3 run, without allocating GPUs."""
+from __future__ import annotations
+
+import argparse
+from concurrent.futures import ThreadPoolExecutor
+import hashlib
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+from urllib.parse import quote
+from urllib.request import urlopen
+
+import ci
+from evaluator.mvp_gpu_job import validate_gpu_job
+from export_ci import REPOSITORY, source_ids, verified_execution
+
+
+def matches(path: Path, entry: dict) -> bool:
+ return (path.is_file() and path.stat().st_size == entry["size_bytes"]
+ and ci.digest(path) == entry["sha256"])
+
+
+def fetch_weight(root: Path, revision: str, entry: dict) -> None:
+ target = root / entry["path"]
+ if matches(target, entry):
+ return
+ ci.need(not target.exists(), "Existing model file differs from the frozen manifest: " + entry["path"])
+ target.parent.mkdir(parents=True, exist_ok=True)
+ partial = target.with_name(target.name + ".partial")
+ url = "https://huggingface.co/MiniMaxAI/MiniMax-H3/resolve/" + revision + "/" + quote(entry["path"], safe="/")
+ digest, size = hashlib.sha256(), 0
+ # No GitHub or implicit Hugging Face credentials are sent to the model host.
+ with urlopen(url, timeout=60) as response, partial.open("wb") as stream:
+ while chunk := response.read(8 * 1024 * 1024):
+ size += len(chunk)
+ ci.need(size <= entry["size_bytes"], "Model download exceeds frozen size")
+ digest.update(chunk)
+ stream.write(chunk)
+ ci.need(size == entry["size_bytes"] and digest.hexdigest() == entry["sha256"],
+ "Downloaded model differs from frozen size or SHA256: " + entry["path"])
+ partial.replace(target)
+
+
+def stage_model(spec: dict, workspace: Path, candidates: list[Path]) -> dict:
+ spec = validate_gpu_job(spec)
+ ci.need(spec["plan"]["model_id"] == "MiniMaxAI/MiniMax-H3", "Only the approved H3 model is supported")
+ approval = spec["authorization"]
+ ci.need(approval["compute_approved"] and approval["model_license_reviewed"]
+ and approval["approval_reference"].strip(), "Source lacks recorded model approval")
+ model = spec["model"]
+ entries = model["files"]
+ observations = []
+ root = workspace / "models" / "MiniMax-H3" / model["revision"]
+ for candidate in [*candidates, root]:
+ present = candidate.is_dir()
+ valid = present and all(matches(candidate / entry["path"], entry) for entry in entries)
+ observations.append({"path": str(candidate), "present": present, "verified": valid})
+ if valid:
+ root = candidate
+ break
+ else:
+ root.mkdir(parents=True, exist_ok=True)
+ missing_bytes = sum(entry["size_bytes"] for entry in entries if not (root / entry["path"]).exists())
+ ci.need(shutil.disk_usage(root).free >= missing_bytes, "Insufficient persistent space for frozen model")
+ with ThreadPoolExecutor(max_workers=4) as pool:
+ list(pool.map(lambda entry: fetch_weight(root, model["revision"], entry), entries))
+ return {"model_path": str(root), "model_revision": model["revision"],
+ "manifest_sha256": hashlib.sha256(json.dumps(entries, sort_keys=True, separators=(",", ":")).encode()).hexdigest(),
+ "verified_files": len(entries), "total_bytes": sum(entry["size_bytes"] for entry in entries),
+ "reuse_candidates": observations, "verification": "complete frozen file sizes and SHA256"}
+
+
+def source_spec(run_id: str, output: Path) -> tuple[dict, dict]:
+ """Read the accepted artifact contract without re-downloading model weights."""
+ ci.need(source_ids(run_id) == [run_id], "One accepted source run is required")
+ source, artifact = verified_execution(run_id)
+ ci.write(output / "source-provenance.json", {"source_ci": source, "source_artifact": artifact})
+ original = output / "source"
+ subprocess.run(["gh", "run", "download", run_id, "--repo", REPOSITORY,
+ "--name", artifact["name"], "--dir", str(original)], check=True, timeout=180)
+ sums = dict(line.split(" ", 1)[::-1] for line in (original / "SHA256SUMS").read_text().splitlines())
+ path = "gpu/c1/spec.json"
+ ci.need(sums.get(path) == ci.digest(original / path), "Frozen source specification checksum differs")
+ return validate_gpu_job(ci.read(original / path)), {"source_ci": source, "source_artifact": artifact}
+
+
+def prepare(run_id: str, workspace: Path, output: Path) -> None:
+ ci.need(source_ids(run_id) == [run_id], "One accepted source run is required")
+ ci.need(workspace.is_absolute(), "Persistent workspace must be absolute")
+ output.mkdir(parents=True, exist_ok=True)
+ record = {"schema_version": 1, "bundle_type": "h3_model_preparation_no_gpu",
+ "gpu_allocation": False, "gpu_execution": False, "status": "preparing",
+ "workspace": {"host": str(workspace), "container": "/work"},
+ "ci": {key: os.environ.get(key) for key in ("GITHUB_RUN_ID", "GITHUB_RUN_ATTEMPT", "GITHUB_SHA")}}
+ ci.write(output / "model-preparation.json", record)
+ try:
+ spec, provenance = source_spec(run_id, output)
+ record.update(provenance)
+ workspace.mkdir(parents=True, exist_ok=True)
+ with ci.task_lock(workspace / ".model-preparation.lock"):
+ revision = spec["model"]["revision"]
+ candidates = [base / "models--MiniMaxAI--MiniMax-H3" / "snapshots" / revision
+ for base in (Path("/it-share/hf-hub-cache"), Path.home() / ".cache/huggingface/hub")]
+ record.update(stage_model(spec, workspace, candidates), status="complete")
+ receipt = workspace / "campaigns/h3-cross-hardware/model-ready.json"
+ receipt.parent.mkdir(parents=True, exist_ok=True)
+ ci.write(receipt, record)
+ except Exception as error:
+ record.update(status="failed", error=str(error))
+ raise
+ finally:
+ ci.write(output / "model-preparation.json", record)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--source-run-id", required=True)
+ parser.add_argument("--workspace", required=True, type=Path)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ prepare(args.source_run_id, args.workspace, args.output)
diff --git a/experimental/video-generation/tests/test_ci.py b/experimental/video-generation/tests/test_ci.py
index c8420d13b3..38453b451f 100644
--- a/experimental/video-generation/tests/test_ci.py
+++ b/experimental/video-generation/tests/test_ci.py
@@ -38,6 +38,73 @@ 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"])
+
+
+def test_amd_granted_full_node_requires_explicit_gpu_evidence(tmp_path):
+ cfg = config(tmp_path)
+ cfg.update(mode="serving-smoke", site=dict(ci.AMD_SITE))
+ cfg["resources"].update(gpus=4, allocated_gpus=8)
+ ci.validate_config(cfg)
+ receipt, record = allocation(tmp_path)
+ record.update(Account=ci.AMD_SITE["account"], Partition="compute", AllocTRES="cpu=128,mem=512G,node=1,billing=128", TresPerNode="gres/gpu:8")
+ assert ci.allocated_gpu_count(record) == 8
+ assert ci.capacity(record, cfg["resources"]) is None
+ assert "--gres=gpu:8" in ci.step_argv(cfg, receipt, record, tmp_path, tmp_path)
+ record["OverSubscribe"] = "OK"
+ assert ci.allocated_gpu_count(record) is None
+ assert ci.capacity(record, cfg["resources"]) == "insufficient allocated GPU/CPU/memory capacity"
+ record.update(OverSubscribe="NO", AllocTRES="cpu=128,mem=512G,node=1,gres/gpu=4")
+ assert ci.allocated_gpu_count(record) == 4
+
+
+@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)
+
+
+def test_explicit_concurrency_selection_requires_serving_mode(tmp_path):
+ cfg = config(tmp_path)
+ cfg["concurrencies"] = [4]
+ with pytest.raises(ValueError, match="requires serving-smoke"):
+ ci.validate_config(cfg)
+ cfg["mode"] = "serving-smoke"
+ ci.validate_config(cfg)
+ cfg["concurrencies"] = [1, 1]
+ with pytest.raises(ValueError, match="unique concurrency"):
+ 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"
@@ -209,10 +276,13 @@ def test_staging_reuses_identical_source_and_refuses_drift(tmp_path):
(source / "evaluator").mkdir(parents=True)
(source / "ci.py").write_text("entry")
(source / "evaluator" / "__init__.py").write_text("")
+ (source / "runtime-patches").mkdir()
+ (source / "runtime-patches" / "timing.patch").write_text("CPU patch fixture")
dest = tmp_path / "package"
original = ci.stage_package(source, dest)
assert ci.stage_package(source, dest) == original
- (dest / "ci.py").write_text("tampered")
+ assert (dest / "runtime-patches" / "timing.patch").read_text() == "CPU patch fixture"
+ (dest / "runtime-patches" / "timing.patch").write_text("tampered")
with pytest.raises(ValueError, match="source differs"):
ci.stage_package(source, dest)
@@ -338,3 +408,25 @@ def test_entry_resolves_device_minors_instead_of_nvml_indices(tmp_path, assignme
else:
assert result.returncode != 0
assert 'lack NVIDIA UUIDs' in result.stderr
+
+
+@pytest.mark.parametrize("decision", [{"action": "allocate"}, {"action": "reuse", "receipt": {"identity": {"JobId": "999"}}}])
+def test_required_lease_never_replaces_or_borrows_another_allocation(tmp_path, monkeypatch, decision):
+ cfg = config(tmp_path)
+ entry = Path(cfg["runtime"]["entry"])
+ entry.write_text("entry")
+ Path(cfg["runtime"]["ready_marker"]).write_text("synthetic readiness")
+ cfg["runtime"]["entry_sha256"] = ci.digest(entry)
+ monkeypatch.setenv("GITHUB_RUN_ID", "456")
+ monkeypatch.setenv("GITHUB_RUN_ATTEMPT", "1")
+ monkeypatch.setenv("H3_SOURCE_SHA", "a" * 40)
+ monkeypatch.setattr(ci, "prepared_spec", lambda cfg: {})
+ monkeypatch.setattr(ci, "command", lambda argv, **kw: "a" * 40 if "rev-parse" in argv else "")
+ monkeypatch.setattr(ci, "stage_package", lambda *args: {})
+ monkeypatch.setattr(ci, "recover", lambda *args: decision)
+ monkeypatch.setattr(ci, "allocate", lambda *args: pytest.fail("must not replace the required allocation"))
+ monkeypatch.setattr(ci, "run_step", lambda *args: pytest.fail("must not enter a different allocation"))
+ monkeypatch.setattr(ci, "stop_allocation", lambda *args: pytest.fail("outer owner releases its allocation"))
+ output = tmp_path / "output"
+ assert ci.launch(cfg, output, required_allocation="123") == 2
+ assert "must reuse its original allocation" in ci.read(output / "ci.json")["error"]
diff --git a/experimental/video-generation/tests/test_compare_serving_ci.py b/experimental/video-generation/tests/test_compare_serving_ci.py
new file mode 100644
index 0000000000..2905713652
--- /dev/null
+++ b/experimental/video-generation/tests/test_compare_serving_ci.py
@@ -0,0 +1,96 @@
+import json
+from pathlib import Path
+import shutil
+
+import pytest
+
+import ci
+import compare_serving_ci as fidelity
+
+
+SHA = "a" * 40
+
+
+def source(root, run_id="123"):
+ run = root / "gpu/c1/baseline/run.json"
+ run.parent.mkdir(parents=True)
+ ci.write(run, {"configuration": {"serving": {"concurrency": 1}}})
+ ci.write(root / "ci.json", {"run_id": run_id, "run_attempt": "1", "source_sha": SHA})
+ ci.write(root / "manifest.json", {"run_id": run_id, "run_attempt": "1", "git_commit": SHA,
+ "mode": "serving-smoke"})
+ ci.write(root / "serving-smoke.json", {"bundle_type": "h3_serving_smoke_matrix", "schema_version": "1.0.0",
+ "cells": [{"concurrency": 1, "run": {"path": "gpu/c1/baseline/run.json", "sha256": ci.digest(run)}}]})
+ seal(root)
+ return {"databaseId": int(run_id), "runAttempt": 1, "headSha": SHA}
+
+
+def seal(root):
+ (root / "SHA256SUMS").write_text("".join(f"{sha} {path}\n" for path, sha in ci.inventory(root).items()))
+
+
+def test_selects_sealed_c1_and_rejects_tampered_bytes_or_ci_identity(tmp_path):
+ metadata = source(tmp_path)
+ selected = fidelity.selected_run(tmp_path, metadata)
+ assert selected == tmp_path / "gpu/c1/baseline"
+ with pytest.raises(ValueError, match="CI identity"):
+ fidelity.selected_run(tmp_path, {**metadata, "headSha": "b" * 40})
+ (selected / "run.json").write_text("{}")
+ with pytest.raises(ValueError, match="checksum"):
+ fidelity.selected_run(tmp_path, metadata)
+
+
+@pytest.mark.parametrize("mutation", ["non_c1", "duplicate_c1", "escape"])
+def test_rejects_wrong_or_ambiguous_cell_even_when_outer_seal_is_updated(tmp_path, mutation):
+ metadata = source(tmp_path)
+ path = tmp_path / "serving-smoke.json"
+ matrix = ci.read(path)
+ if mutation == "non_c1":
+ run = tmp_path / "gpu/c1/baseline/run.json"
+ ci.write(run, {"configuration": {"serving": {"concurrency": 2}}})
+ matrix["cells"][0]["run"]["sha256"] = ci.digest(run)
+ elif mutation == "duplicate_c1":
+ matrix["cells"].append(matrix["cells"][0])
+ else:
+ matrix["cells"][0]["run"]["path"] = "../outside/run.json"
+ ci.write(path, matrix)
+ seal(tmp_path)
+ with pytest.raises(ValueError):
+ fidelity.selected_run(tmp_path, metadata)
+
+
+def test_publication_retains_uncalibrated_failure_and_original_seals(tmp_path, monkeypatch):
+ originals = tmp_path / "originals"
+ metadata = {run: source(originals / run, run) for run in ("123", "456")}
+ monkeypatch.setenv("GITHUB_SHA", SHA)
+ monkeypatch.setenv("GITHUB_RUN_ID", "789")
+ monkeypatch.setenv("GITHUB_RUN_ATTEMPT", "1")
+ monkeypatch.setenv("GITHUB_REPOSITORY", "SemiAnalysisAI/InferenceX")
+ monkeypatch.setattr(ci, "command", lambda *args: SHA)
+ monkeypatch.setattr(fidelity.export_ci, "verified_execution", lambda run: (metadata[run], {"name": "original"}))
+ def download(argv, **kwargs):
+ assert argv[:3] == ["gh", "run", "download"]
+ shutil.copytree(originals / argv[3], Path(argv[argv.index("--dir") + 1]))
+ monkeypatch.setattr(fidelity.subprocess, "run", download)
+ def compare(left, right, *, policy):
+ assert left.parts[-4:] == ("source-123", "gpu", "c1", "baseline")
+ assert right.parts[-4:] == ("source-456", "gpu", "c1", "baseline")
+ assert policy["calibration_status"] == "uncalibrated"
+ return {"overall_status": "fail", "release_qualified": False, "policy": policy,
+ "summary": {"matched_valid_pairs": 20}}
+ monkeypatch.setattr(fidelity, "compare_runs", compare)
+ def report(result, path):
+ assert path.suffix == ".html" and result["overall_status"] == "fail"
+ path.parent.mkdir()
+ path.write_text("fixture report")
+ monkeypatch.setattr(fidelity, "write_report", report)
+ output = tmp_path / "published"
+ fidelity.publish(["123", "456"], output)
+ assert (output / "sources/123/original-SHA256SUMS").read_bytes() == (originals / "123/SHA256SUMS").read_bytes()
+ assert ci.read(output / "sources/123/manifest.json")["git_commit"] == SHA
+ assert not (output / "source-123").exists()
+ assert not (output / "sources/123/gpu").exists()
+ result = json.loads((output / "comparison.json").read_text())
+ assert result["source_artifacts"][1]["ci"]["databaseId"] == 456
+ receipt = ci.read(output / "reprocessing.json")
+ assert receipt["status"] == "complete" and receipt["threshold_outcome"] == "fail"
+ assert receipt["release_qualified"] is False and receipt["generation_executed"] is False
diff --git a/experimental/video-generation/tests/test_export_ci.py b/experimental/video-generation/tests/test_export_ci.py
index 54922b6808..27ef23700c 100644
--- a/experimental/video-generation/tests/test_export_ci.py
+++ b/experimental/video-generation/tests/test_export_ci.py
@@ -46,6 +46,13 @@ def test_source_ids_preserve_order_without_shell_interpretation():
assert export_ci.source_ids("34293342829,34291306687") == ["34293342829", "34291306687"]
+@pytest.mark.parametrize("hardware", ["H100", "B200", "MI355X"])
+def test_serving_sources_accept_verified_cross_hardware_generation(github, hardware):
+ github[1]["jobs"][0]["name"] = f"h3-video / p1.5 | H3 video {hardware} smoke"
+ source, artifact = export_ci.verified_execution("123")
+ assert source["headSha"] == SHA and artifact["id"] == 789
+
+
def test_inventory_reuse_requires_the_hardware_job_and_artifact(github):
with pytest.raises(ValueError):
export_ci.verified_execution("123", inventory=True)
diff --git a/experimental/video-generation/tests/test_inspect_amd_node.py b/experimental/video-generation/tests/test_inspect_amd_node.py
new file mode 100644
index 0000000000..c7a4821435
--- /dev/null
+++ b/experimental/video-generation/tests/test_inspect_amd_node.py
@@ -0,0 +1,96 @@
+"""Fake scheduler checks for ownership and cleanup, not AMD hardware evidence."""
+from types import SimpleNamespace
+
+import pytest
+
+import ci
+import inspect_amd_node as amd
+from test_ci import allocation
+
+
+def test_wrong_node_stops_before_device_queries(tmp_path, monkeypatch):
+ ci.write(tmp_path / "context.json", {"allocation": {"identity": {"JobId": "123"}}, "node": "amd-node"})
+ monkeypatch.setenv("SLURM_JOB_ID", "123")
+ monkeypatch.setenv("SLURMD_NODENAME", "another-node")
+ monkeypatch.setattr(amd, "observation", lambda argv: pytest.fail("foreign node must not be queried"))
+ with pytest.raises(ValueError, match="Wrong AMD inventory allocation"):
+ amd.inspect_node(tmp_path)
+ assert not (tmp_path / "binding.json").exists()
+
+
+@pytest.mark.parametrize("value", ["0-7", "0,1,2,3,4,5,6,7"])
+def test_full_amd_step_assignment(value):
+ assert amd.step_gpu_indices(value) == set(range(8))
+
+
+@pytest.mark.parametrize("value", ["", "0-99", "7-0", "0;id"])
+def test_invalid_amd_step_assignment(value):
+ with pytest.raises(ValueError, match="AMD step GPU assignment"):
+ amd.step_gpu_indices(value)
+
+
+@pytest.mark.parametrize("reused", [False, True])
+@pytest.mark.parametrize("prepare_runtime", [False, True])
+def test_failed_inventory_drains_only_owned_step_and_preserves_borrowed_allocation(tmp_path, monkeypatch, reused, prepare_runtime):
+ monkeypatch.setenv("H3_RUN_ID", "456")
+ monkeypatch.setenv("H3_RUN_ATTEMPT", "1")
+ account = "cameronamd@semianalysis.com"
+ monkeypatch.setattr(amd.pwd, "getpwuid", lambda uid: SimpleNamespace(pw_name=account))
+ receipt, record = allocation(tmp_path)
+ site = {"cluster": "mi355x-amds", "partition": "compute", "account": account, "gpu_model": "MI355X"}
+ receipt.update(task_id="h3-cross-hardware", site=site)
+ receipt["identity"].update(Account=account, Partition="compute")
+ record.update(receipt["identity"])
+ monkeypatch.setattr(ci, "recover", lambda *args: {"action": "reuse" if reused else "allocate", "receipt": receipt, "active_steps": ""})
+ def allocate(config, root):
+ assert not reused
+ ci.write(root / "allocation.json", receipt)
+ return receipt
+ monkeypatch.setattr(ci, "allocate", allocate)
+ monkeypatch.setattr(ci, "job_record", lambda job: record)
+ import prepare_amd_runtime
+ monkeypatch.setattr(prepare_amd_runtime, "prepare_source", lambda workspace: workspace)
+ def fail_step(argv, log, timeout):
+ assert "--gres=gpu:8" in argv
+ assert "--time=" + ("55" if prepare_runtime else "10") in argv
+ assert timeout == (3360 if prepare_runtime else 660)
+ return 1
+ monkeypatch.setattr(ci, "run_step", fail_step)
+ cleanup = []
+ monkeypatch.setattr(ci, "drain_step", lambda owned, task, root: cleanup.append(("step", owned["identity"]["JobId"])) or {"status": "ended"})
+ monkeypatch.setattr(ci, "stop_allocation", lambda owned, task: cleanup.append(("allocation", owned["identity"]["JobId"])) or {"status": "released"})
+ output = tmp_path / "output"
+ assert amd.inspect(tmp_path / "work", output, prepare_runtime=prepare_runtime) == 2
+ assert cleanup == [("step", "123")] + ([] if reused else [("allocation", "123")])
+ assert ci.read(output / "inventory-status.json")["generation_executed"] is False
+ assert (output / "SHA256SUMS").is_file()
+
+
+def test_failed_cpu_entry_does_not_approve_partial_rootfs(tmp_path, monkeypatch):
+ import subprocess
+ import prepare_amd_runtime as runtime
+ workspace = tmp_path / "work"
+ (workspace / "campaigns/h3-cross-hardware").mkdir(parents=True)
+ rootfs = tmp_path / "enroot-data" / runtime.CONTAINER
+ (rootfs / "etc").mkdir(parents=True)
+ (rootfs / "etc/rc").write_text('exec "$@"\n')
+ origin = rootfs.with_suffix(".image.json")
+ ci.write(origin, {"image": str(runtime.IMAGE), "status": "creating"})
+ previous = workspace / "results/h3-cross-hardware/github-34344130223-1"
+ previous.mkdir(parents=True)
+ ci.write(previous / "inventory-status.json", {"allocation_cleanup": {"status": "released"}})
+ (previous / "srun.log").write_text("Ignoring xattrs in filesystem\ncreated 464452 files\ncreated 11757 symlinks\n")
+ monkeypatch.setenv("H3_RUN_ID", "987")
+ monkeypatch.setenv("H3_RUN_ATTEMPT", "1")
+ monkeypatch.setattr(runtime, "prepare_source", lambda path: path)
+ def fail_entry(argv, **kwargs):
+ if argv[0].endswith("enroot"):
+ assert not any("/dev/kfd" in arg or "/dev/dri" in arg for arg in argv)
+ raise subprocess.CalledProcessError(1, argv)
+ monkeypatch.setattr(runtime.subprocess, "run", fail_entry)
+ output = tmp_path / "output"
+ with pytest.raises(subprocess.CalledProcessError):
+ runtime.recover_rootfs(workspace, output)
+ assert ci.read(origin)["status"] == "creating"
+ assert ci.read(output / "rootfs-recovery.json")["status"] == "failed"
+ assert not (workspace / "campaigns/h3-cross-hardware/rootfs-recovered.json").exists()
diff --git a/experimental/video-generation/tests/test_mvp_amd_gpu.py b/experimental/video-generation/tests/test_mvp_amd_gpu.py
new file mode 100644
index 0000000000..32adbffc6e
--- /dev/null
+++ b/experimental/video-generation/tests/test_mvp_amd_gpu.py
@@ -0,0 +1,117 @@
+"""CPU checks for observed AMD SMI shapes; these are not GPU measurements."""
+import copy
+from types import SimpleNamespace
+
+import pytest
+
+from evaluator import mvp_amd_gpu as amd
+
+
+GPU = "75ff75a3-0000-1000-80e3-fd74aab3f72c"
+OTHER = "68ff75a3-0000-1000-8089-743843afe909"
+
+
+def observations():
+ # Reduced shape from CI 34341458378 on MI355X / AMD SMI 26.2.0.
+ return {
+ "list": [{"gpu": 0, "bdf": "0000:05:00.0", "uuid": GPU, "partition_id": 0}],
+ "static": [{"gpu": 0, "bus": {"bdf": "0000:05:00.0"},
+ "asic": {"market_name": "AMD Instinct MI355X"}, "driver": {"version": "6.16.6"},
+ "limit": {"socket_power": {"value": 1400, "unit": "W"}, "max_power": {"value": 1400, "unit": "W"}}}],
+ "metric": {"gpu_data": [{"gpu": 0,
+ "mem_usage": {"total_vram": {"value": 294896, "unit": "MB"}, "used_vram": {"value": 283, "unit": "MB"}},
+ "usage": {"gfx_activity": {"value": 0, "unit": "%"}},
+ "power": {"socket_power": {"value": 239, "unit": "W"}},
+ "temperature": {"hotspot": {"value": 36, "unit": "C"}}}]},
+ "process": [{"gpu": 0, "process_list": [{"process_info": {"pid": 15910, "memory_usage": {"vram_mem": {"value": 0, "unit": "B"}}}}]}],
+ }
+
+
+def test_snapshot_preserves_zero_memory_process_and_measured_power(monkeypatch):
+ data = observations()
+ monkeypatch.setattr(amd, "smi", lambda option, timeout: copy.deepcopy(data[option]))
+ probe = amd.AmdGpuProbe([GPU], 2)
+ result = probe.snapshot()
+ assert result["compute_apps"] == [{"gpu_uuid": GPU, "pid": 15910, "memory_used_mib": 0.0}]
+ assert result["gpus"][0]["power_watts"] == 239
+ assert result["gpus"][0]["memory_used_mib"] == 283
+ assert probe.power_configuration()["gpus"][0]["configured_limit_w"] == 1400
+ data["metric"]["gpu_data"][0]["power"]["socket_power"] = "N/A"
+ assert probe.snapshot()["gpus"][0]["power_watts"] is None
+
+
+def test_hip_order_joins_physical_identity_instead_of_smi_ordinal(monkeypatch):
+ rows = observations()["list"] + [{"gpu": 1, "bdf": "0000:15:00.0", "uuid": OTHER, "partition_id": 0}]
+ monkeypatch.setattr(amd, "smi", lambda *args: rows)
+ def count(pointer):
+ pointer._obj.value = 2
+ return 0
+ def bdf(buffer, size, ordinal):
+ buffer.value = [b"0000:15:00.0", b"0000:05:00.0"][ordinal]
+ return 0
+ monkeypatch.setattr(amd.ctypes, "CDLL", lambda _: SimpleNamespace(hipGetDeviceCount=count, hipDeviceGetPCIBusId=bdf))
+ assert amd.hip_devices() == [OTHER, GPU]
+
+
+@pytest.mark.parametrize("mutation", [
+ lambda rows: rows.append(dict(rows[0])),
+ lambda rows: rows[0].update(partition_id=1),
+ lambda rows: rows[0].update(bdf="../../other"),
+])
+def test_ambiguous_or_partitioned_device_inventory_is_rejected(mutation):
+ rows = observations()["list"]
+ mutation(rows)
+ with pytest.raises(RuntimeError):
+ amd.inventory(rows)
+
+
+@pytest.mark.parametrize("value", [{"value": 239, "unit": "mW"}, {"value": True, "unit": "W"}, {"value": float("nan"), "unit": "W"}])
+def test_invalid_power_unit_or_value_is_not_a_measurement(value):
+ assert amd.number(value, "W", optional=True) is None
+ with pytest.raises(RuntimeError):
+ amd.number(value, "W")
+
+
+@pytest.mark.parametrize("change", ["pid", "start_ticks", "executable", "uid", "memory", "unverified"])
+def test_monitor_exception_rejects_changed_or_active_context(monkeypatch, change):
+ identity = {"pid": 15910, "start_ticks": 50, "executable": "/opt/gpuagent/gpuagent", "uid": 0}
+ receipt = {"status": "verified", "process": identity}
+ app = {"pid": 15910, "memory_used_mib": 0}
+ observed = dict(identity)
+ if change == "memory":
+ app["memory_used_mib"] = 1
+ elif change == "unverified":
+ receipt["status"] = "unverified"
+ elif change == "executable":
+ observed[change] = "/tmp/other"
+ else:
+ observed[change] += 1
+ monkeypatch.setattr(amd, "monitor_process", lambda pid: observed)
+ assert not amd.is_system_monitor(app, receipt)
+
+
+def test_verified_monitor_is_retained_separately_from_workload_contexts(monkeypatch):
+ data = observations()
+ monkeypatch.setattr(amd, "smi", lambda option, timeout: copy.deepcopy(data[option]))
+ probe = amd.AmdGpuProbe([GPU], 2)
+ identity = {"pid": 15910, "start_ticks": 50, "executable": "/opt/gpuagent/gpuagent", "uid": 0}
+ probe.monitor = {"status": "verified", "service": "gpuagent.service", "process": identity}
+ monkeypatch.setattr(amd, "monitor_process", lambda pid: dict(identity))
+ result = probe.snapshot()
+ assert result["compute_apps"] == []
+ excluded = result["excluded_system_monitor_contexts"]
+ assert len(excluded) == 1 and excluded[0]["pid"] == 15910 and excluded[0]["gpu_uuid"] == GPU
+ assert excluded[0]["identity"]["service"] == "gpuagent.service"
+
+
+def test_service_pid_mismatch_never_approves_monitor(monkeypatch):
+ monkeypatch.setattr(amd, "_command", lambda *a, **k: "MainPID=42\nExecMainPID=43\nExecStart={ path=/opt/gpuagent/gpuagent ; }\nActiveState=active\nSubState=running\nControlGroup=/system.slice/gpuagent.service\nType=simple\n")
+ monkeypatch.setattr(amd, "monitor_process", lambda pid: pytest.fail("mismatched service PID must be rejected"))
+ assert amd.observe_system_monitor(1)["status"] == "unverified"
+
+
+def test_unreadable_monitor_identity_remains_a_foreign_process(monkeypatch):
+ def unreadable(pid):
+ raise PermissionError("process identity unavailable")
+ monkeypatch.setattr(amd, "monitor_process", unreadable)
+ assert not amd.is_system_monitor({"pid": 42, "memory_used_mib": 0}, {"status": "verified", "pid": 42})
diff --git a/experimental/video-generation/tests/test_mvp_gpu_job.py b/experimental/video-generation/tests/test_mvp_gpu_job.py
index a891115902..aa6779f393 100644
--- a/experimental/video-generation/tests/test_mvp_gpu_job.py
+++ b/experimental/video-generation/tests/test_mvp_gpu_job.py
@@ -57,6 +57,25 @@ def snapshot(*, used=50, pid=None):
"compute_apps": [{"gpu_uuid": GPU, "pid": pid, "memory_used_mib": used}] if pid else []}
+def test_amd_spec_binds_vendor_and_documented_attention_layout(spec, tmp_path, monkeypatch):
+ spec["gpu_vendor"] = "amd"
+ spec["gpu_uuids"] = [GPU.removeprefix("GPU-")]
+ spec["server"]["attention_backend"] = "aiter"
+ frozen = gpu.validate_gpu_job(spec)
+ assert gpu._server_argv(frozen, "baseline")[-2:] == ["--attention-backend", "aiter"]
+ monkeypatch.setenv("ROCR_VISIBLE_DEVICES", "3")
+ env = gpu._runtime_env("/pinned/source", frozen["gpu_uuids"], "nonce", tmp_path, "amd")
+ assert env["ROCR_VISIBLE_DEVICES"] == "3"
+ assert env["HIP_VISIBLE_DEVICES"] == env["CUDA_VISIBLE_DEVICES"] == "0"
+ assert env["SGLANG_USE_AITER"] == "1"
+ monkeypatch.delenv("ROCR_VISIBLE_DEVICES")
+ with pytest.raises(ValueError, match="bound ROCR"):
+ gpu._runtime_env("/pinned/source", frozen["gpu_uuids"], "nonce", tmp_path, "amd")
+ del spec["gpu_vendor"]
+ with pytest.raises(ValueError, match="declared vendor"):
+ gpu.validate_gpu_job(spec)
+
+
def controlled_receipt(spec):
roles = {}
for label in ("baseline", "candidate"):
diff --git a/experimental/video-generation/tests/test_mvp_power.py b/experimental/video-generation/tests/test_mvp_power.py
index 2642807570..ff2a44328f 100644
--- a/experimental/video-generation/tests/test_mvp_power.py
+++ b/experimental/video-generation/tests/test_mvp_power.py
@@ -55,6 +55,22 @@ def test_ramp_clipping_separates_phases_and_excludes_client_decode():
assert result["sample_series"][8]["aggregate_watts"] == 135
+def test_amd_energy_retains_sensor_source_and_rejects_mixed_sources():
+ role, run, samples = data()
+ for sample in samples:
+ sample["power_query"] = {"field": "amd-smi power.socket_power", "start_utc": sample["at"],
+ "start_monotonic_seconds": sample["monotonic_seconds"],
+ "end_monotonic_seconds": sample["monotonic_seconds"]}
+ result = analyze(role, run, samples)
+ assert result["valid"] is True
+ assert result["semantics"]["sensor"].startswith("amd-smi power.socket_power;")
+ assert result["phases"]["measurement"]["aggregate"]["energy_j"] == 450
+ samples[9]["power_query"]["field"] = "power.draw"
+ result = analyze(role, run, samples)
+ assert result["phases"]["measurement"]["aggregate"] is None
+ assert "power_sensor_source_inconsistent" in result["invalid_reasons"]
+
+
@pytest.mark.parametrize("defect", ["missing_power", "nan", "negative", "missing_gpu", "duplicate_gpu", "foreign", "identity", "partition", "nonmonotonic", "clock_jump"])
def test_invalid_telemetry_withholds_measurement_metrics(defect):
role, run, samples = data()
diff --git a/experimental/video-generation/tests/test_mvp_runtime_timing.py b/experimental/video-generation/tests/test_mvp_runtime_timing.py
new file mode 100644
index 0000000000..e2a6e42692
--- /dev/null
+++ b/experimental/video-generation/tests/test_mvp_runtime_timing.py
@@ -0,0 +1,148 @@
+"""CPU event/transport checks; these do not establish H3 timing overhead."""
+
+from copy import deepcopy
+import hashlib
+import importlib.util
+import json
+from types import SimpleNamespace
+
+import pytest
+
+from evaluator import mvp_runtime_timing as timing
+from evaluator.mvp_serving import summarize
+from test_mvp_serving import sample_run
+from test_mvp_runner import execute, fixture_server, mocked_media, plan # noqa: F401
+
+
+@pytest.fixture
+def ledger(tmp_path, monkeypatch):
+ path = tmp_path / "server-timings.jsonl"
+ path.touch()
+ monkeypatch.setenv("VGBENCH_SERVER_TIMING_PATH", str(path))
+ monkeypatch.setenv("VGBENCH_SERVER_TIMING_INSTANCE", "cpu-fixture-instance")
+ monkeypatch.setattr(timing, "_clock_id", lambda: "linux:cpu-fixture:time-namespace:CLOCK_MONOTONIC")
+ return path
+
+
+def complete_request(request_id):
+ timing.emit(request_id, "http_received")
+ timing.emit(request_id, "http_accepted")
+ with timing.forward([SimpleNamespace(request_id=request_id, num_outputs_per_prompt=1)], replica_id=0, leader=True):
+ pass
+ timing.emit(request_id, "media_ready")
+
+
+def test_real_event_writer_correlates_request_and_derives_stage_windows(ledger, monkeypatch):
+ stamps = iter([1, 2, 7, 17, 19])
+ monkeypatch.setattr(timing.time, "monotonic_ns", lambda: next(stamps) * 1_000_000_000)
+ complete_request("video-1")
+ result = timing.collect("video-1")
+ assert result["status"] == "complete"
+ assert [result[name] for name in timing.DURATIONS] == [1, 5, 10, 2, 18]
+ assert result["observed_batch_size"] == 1 and result["replica_id"] == 0
+ assert timing.collect("different-video") is None
+ assert len(timing.read_events(ledger)) == 5
+
+
+def test_interleaved_requests_and_missing_stage_do_not_create_queue_time(ledger):
+ timing.emit("a", "http_received")
+ complete_request("b")
+ timing.emit("a", "http_accepted")
+ partial = timing.collect("a")
+ assert partial["status"] == "partial"
+ assert partial["queue_delay_seconds"] is None
+ assert partial["server_ready_latency_seconds"] is None
+ assert partial["observed_batch_size"] is None
+ assert timing.collect("b")["status"] == "complete"
+
+
+@pytest.mark.parametrize("defect", ["duplicate", "clock", "reverse", "instance", "batch"])
+def test_corrupt_or_incompatible_events_fail_closed(ledger, defect):
+ complete_request("a")
+ rows = timing.read_events(ledger)
+ if defect == "duplicate":
+ rows.append(rows[0])
+ elif defect == "clock":
+ rows[2]["clock_id"] = "another-host"
+ elif defect == "reverse":
+ rows[2]["monotonic_ns"] = rows[0]["monotonic_ns"] - 1
+ elif defect == "instance":
+ rows[2]["instance_id"] = "another-server"
+ else:
+ rows[2]["observed_batch_size"] = 2
+ with pytest.raises(ValueError):
+ timing.derive(rows, "a", "cpu-fixture-instance")
+
+
+def test_worker_followers_do_not_duplicate_events_and_failure_has_no_ready_time(ledger):
+ req = SimpleNamespace(request_id="a", num_outputs_per_prompt=1)
+ with timing.forward([req], replica_id=0, leader=False):
+ pass
+ assert timing.read_events(ledger) == []
+ with pytest.raises(RuntimeError, match="generation failed"):
+ with timing.forward([req], replica_id=0, leader=True):
+ raise RuntimeError("generation failed")
+ partial = timing.collect("a")
+ assert partial["execution_seconds"] >= 0
+ assert partial["server_ready_latency_seconds"] is None
+
+
+def test_http_client_retains_timings_in_original_record_and_journal(plan, mocked_media, fixture_server, ledger, tmp_path):
+ ordinal = 0
+ def record_fixture_timing():
+ nonlocal ordinal
+ ordinal += 1
+ complete_request(f"fixture-{ordinal}")
+ endpoint, _ = fixture_server(before_submit=record_fixture_timing)
+ output = tmp_path / "client"
+ result = execute(plan, output, endpoint)
+ assert all(record["server_timings"]["request_id"] == record["job_id"] for record in result["records"])
+ records = [row["record"] for row in map(json.loads, (output / "events.jsonl").read_text().splitlines())
+ if row["event"] == "attempt_finished"]
+ assert [row["server_timings"] for row in records] == [row["server_timings"] for row in result["records"]]
+
+
+def test_serving_summary_requires_full_valid_population_for_percentiles():
+ run = sample_run()
+ historical = summarize(run)
+ assert historical["queue_delay_seconds"] is None
+ assert "server_execution_seconds" not in historical
+ for record in run["records"][:10]:
+ record["server_timings"] = {"status": "complete", "queue_delay_seconds": 2,
+ "execution_seconds": 5, "server_ready_latency_seconds": 8,
+ "prequeue_seconds": .5, "postprocess_seconds": .5, "observed_batch_size": 1, "replica_id": 0}
+ observed = summarize(run)
+ assert observed["queue_delay_seconds"]["p90"] == 2
+ assert observed["server_execution_seconds"]["sample_count"] == 10
+ assert observed["observed_batch_sizes"] == [1] * 10
+ run["records"][0]["server_timings"] = None
+ incomplete = summarize(run)
+ assert incomplete["queue_delay_seconds"]["missing_count"] == 1
+ assert incomplete["queue_delay_seconds"]["p90"] is None
+ assert incomplete["observed_batch_sizes"] is None
+
+
+def test_offline_verification_binds_ledger_hash_instance_and_request(ledger, tmp_path):
+ complete_request("a")
+ run = {"records": [{"job_id": "a", "status": "succeeded", "server_timings": timing.collect("a")}]}
+ role = {"process_identity": {"launch_nonce": "cpu-fixture-instance"}, "server_timing_evidence": {
+ **timing.identity(), "path": ledger.name, "instance_id": "cpu-fixture-instance",
+ "sha256": hashlib.sha256(ledger.read_bytes()).hexdigest()}}
+ timing.verify_evidence(tmp_path, role, run, required=True)
+ changed = deepcopy(run)
+ changed["records"][0]["server_timings"]["queue_delay_seconds"] = 99
+ with pytest.raises(ValueError, match="raw events"):
+ timing.verify_evidence(tmp_path, role, changed, required=True)
+ ledger.write_text(ledger.read_text() + "{}\n")
+ with pytest.raises(ValueError, match="hash mismatch"):
+ timing.verify_evidence(tmp_path, role, run, required=True)
+
+
+def test_staged_runtime_helper_can_resolve_its_pinned_patch(tmp_path):
+ import ci
+ destination = tmp_path / "staged"
+ ci.stage_package(timing.PATCH.parents[1], destination)
+ spec = importlib.util.spec_from_file_location("staged_timing", destination / "evaluator/mvp_runtime_timing.py")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ assert module.identity() == timing.identity()
diff --git a/experimental/video-generation/tests/test_mvp_serving_smoke.py b/experimental/video-generation/tests/test_mvp_serving_smoke.py
index 2cefef538f..24929ba138 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("
|---|