From a1a8d4e18c52133ee72be0d6d5d5aaca71ed0ccd Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:29 -0700 Subject: [PATCH 01/12] test: add the PR #2916 H3 serving spec as a fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将 PR #2916 的 H3 serving spec 原样复制为测试 fixture。 --- .../video-generation/tests/fixtures/README.md | 3 + .../tests/fixtures/h3-serving-2gpu.json | 521 ++++++++++++++++++ 2 files changed, 524 insertions(+) create mode 100644 experimental/video-generation/tests/fixtures/README.md create mode 100644 experimental/video-generation/tests/fixtures/h3-serving-2gpu.json diff --git a/experimental/video-generation/tests/fixtures/README.md b/experimental/video-generation/tests/fixtures/README.md new file mode 100644 index 0000000000..0a87e1025d --- /dev/null +++ b/experimental/video-generation/tests/fixtures/README.md @@ -0,0 +1,3 @@ +`h3-serving-2gpu.json` is a verbatim copy of `experimental/video-generation/specs/h3-serving-2gpu.json` at commit 0cb4c847d8c8089cc9f25a8a8c826d781f9b9f22 of PR #2916 (git blob 404bc4f02). +Its SHA-256 is `a60583798a23f259a4d0185315d97b8bd3a1cca94e623068a2e7c95be5921b1d`. Do not edit it. +Recreate it with `git show 0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json > h3-serving-2gpu.json`. diff --git a/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json b/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json new file mode 100644 index 0000000000..404bc4f02b --- /dev/null +++ b/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json @@ -0,0 +1,521 @@ +{ + "allocation": { + "label": "srt-slurm job assigned at launch", + "mode": "dedicated_ci" + }, + "authorization": { + "approval_reference": "InferenceX #2916 native srt-slurm H3 serving run, authorized by the repository owner on 2026-10-17 (SemiAnalysis clusters only); model use per the MiniMax-H3 license review recorded for the #2894 baseline (user-confirmation-2026-09-02).", + "compute_approved": true, + "model_license_reviewed": true + }, + "baseline": { + "python": "/usr/bin/python3", + "revision": "71de97b264b04dcd514cf904003028aefe9775c8", + "source": "/sgl-workspace/sglang", + "source_sha256": "e6069f8b948234075fdcfba23e459e2f56a45b11d0ba3ee5ecbb754e338deb13" + }, + "candidate": { + "python": "/usr/bin/python3", + "revision": "71de97b264b04dcd514cf904003028aefe9775c8", + "source": "/sgl-workspace/sglang", + "source_sha256": "e6069f8b948234075fdcfba23e459e2f56a45b11d0ba3ee5ecbb754e338deb13" + }, + "gpu_uuids": [ + "GPU-00000000-0000-0000-0000-000000000000", + "GPU-00000000-0000-0000-0000-000000000001" + ], + "job_id": "h3-serving-2gpu", + "limits": { + "cleanup_seconds": 60, + "command_seconds": 30, + "job_seconds": 3600, + "max_idle_memory_mib": 64, + "request_seconds": 900, + "startup_seconds": 900, + "telemetry_interval_seconds": 1 + }, + "lock_directory": "/logs/h3/locks", + "model": { + "files": [ + { + "path": "FL2VA/audio_vae/config.json", + "sha256": "d8f3bcc62e23c7e9806970fa63cca6139c06faa3797cf9c94034f60db8512771", + "size_bytes": 1973 + }, + { + "path": "FL2VA/audio_vae/config.yaml", + "sha256": "259f675c9f71eedc24ca4f23965cd32a1b3878fc894f56664c40328d99831a5e", + "size_bytes": 91 + }, + { + "path": "FL2VA/audio_vae/dac_activations.py", + "sha256": "74ca8bb9e8039f1ff362ca4219eee5240a7df9502f3dbdc3161d4e86fbd6dc04", + "size_bytes": 2189 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_act.py", + "sha256": "db8761e66c0eaf9fce2dcccb59162d52b22ca0de9f699cf7d21ec7cd507e86fd", + "size_bytes": 835 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_filter.py", + "sha256": "a369523cd7f14f4f299d8e2e47f3c473da76ed1910e6e9134fd7a9fce02537e8", + "size_bytes": 3300 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_resample.py", + "sha256": "fe35055893833c563e42adc5e5882cbe7628c82d48d33c300fcf0a9576997910", + "size_bytes": 1740 + }, + { + "path": "FL2VA/audio_vae/dac_attn_proj.py", + "sha256": "0e04556fa5fd38d4a71e62fcf4d3242fd9a2464fd72ed62baef2ec0980d910c6", + "size_bytes": 3317 + }, + { + "path": "FL2VA/audio_vae/dac_audio_vae.py", + "sha256": "ec04939602d4710d039e83f558a7572547669652f7fa967e3de0a3b6c48cfd48", + "size_bytes": 7266 + }, + { + "path": "FL2VA/audio_vae/dac_bigvgan.py", + "sha256": "f5df4e6f633e74da479549ef7a6d2684964e4b65ac42c6765be42f9c7fcb7bd2", + "size_bytes": 7102 + }, + { + "path": "FL2VA/audio_vae/dac_utils.py", + "sha256": "74ef0ba2c2a11ca35a7c0cc166fe88e564de01230000a0bcc8429103312ad9f0", + "size_bytes": 362 + }, + { + "path": "FL2VA/audio_vae/metadata.json", + "sha256": "755d0529d43b2b5c83590f6f44ca659bc68e6a21b01d5669c93e8b2965749bff", + "size_bytes": 440 + }, + { + "path": "FL2VA/audio_vae/minimax_h3_audio_vae.py", + "sha256": "63bc0dab6def69480cfd77ea820f9a9a84fc4bd6e56868e4f45ec1e1e200578f", + "size_bytes": 3532 + }, + { + "path": "FL2VA/audio_vae/model.safetensors", + "sha256": "37dddc2f3e6d5d5139d823d5ea283bbf304dadcb885b1ccda818aa13dade5ea2", + "size_bytes": 605429308 + }, + { + "path": "FL2VA/model_index.json", + "sha256": "d1113e0f123c69f79cd0de35ca1771606ebc3ec924270d257b771f96f584aa6b", + "size_bytes": 719 + }, + { + "path": "FL2VA/processor/chat_template.json", + "sha256": "5c72a170d2a4a1a3bc5adad2e689ae28138a9700e5b8c96c0266331e86c0acce", + "size_bytes": 5499 + }, + { + "path": "FL2VA/processor/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/processor/preprocessor_config.json", + "sha256": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516", + "size_bytes": 390 + }, + { + "path": "FL2VA/processor/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/processor/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/processor/video_preprocessor_config.json", + "sha256": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13", + "size_bytes": 385 + }, + { + "path": "FL2VA/processor/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/text_encoder/chat_template.json", + "sha256": "5c72a170d2a4a1a3bc5adad2e689ae28138a9700e5b8c96c0266331e86c0acce", + "size_bytes": 5499 + }, + { + "path": "FL2VA/text_encoder/config.json", + "sha256": "d2dd0c60d01b9e195d9447c52da61c7302d28828524914c044d9c6e1b81d0427", + "size_bytes": 1474 + }, + { + "path": "FL2VA/text_encoder/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/text_encoder/model-00001-of-00014.safetensors", + "sha256": "6b9dfbc930e505402ae9d7e5091a9d7d656cda5f34614f01cfe70bfb0cca27cb", + "size_bytes": 4932328944 + }, + { + "path": "FL2VA/text_encoder/model-00002-of-00014.safetensors", + "sha256": "d8bb44b4ff303fe76fe9e894022fb3dc71b15a2e716592790fe0e3c3e60478fa", + "size_bytes": 4875990528 + }, + { + "path": "FL2VA/text_encoder/model-00003-of-00014.safetensors", + "sha256": "54f22e8b3168f8dc962fac0d313607ebf52a12b433d4cf3098a0d82d9f042940", + "size_bytes": 4875990552 + }, + { + "path": "FL2VA/text_encoder/model-00004-of-00014.safetensors", + "sha256": "ad09c74d3c13ee29b5d0d84548fd8a3424a651564eaccd519946c296e59c557f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00005-of-00014.safetensors", + "sha256": "fc993c8a0e2a5b0570f383e1a95dc3a1281d1b224b6f3ee908f4827941e1dfc2", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00006-of-00014.safetensors", + "sha256": "82f05620d1f718a90c362b221d6a184ff1a0f53301d706882d3df49695fa1974", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00007-of-00014.safetensors", + "sha256": "fb91da8cb01ff4de3eef0eab1c3e769a734b3a1aafc61734068638a0d6c86934", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00008-of-00014.safetensors", + "sha256": "431ca56535c8781944ce3801f5eb61c45531e853ecc5846d936ebaf4761b764f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00009-of-00014.safetensors", + "sha256": "3825e3f4302f4d2f7d76aa7430d2ce0864fde6b9e540a5806bf0d8e38e4d9f47", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00010-of-00014.safetensors", + "sha256": "aded5a4d1d5e22dbd8b6f79266b6eb88c840411b09527c53917a1419ace22e2f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00011-of-00014.safetensors", + "sha256": "3820ffe8d8d6477f6fe8d614ef3c87abb264ee39accebf43a1507b970d80946f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00012-of-00014.safetensors", + "sha256": "05ad2d08ce71963121c9b03f1d9ec5d7641052f4b23c6c12b80d71065eb8e98e", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00013-of-00014.safetensors", + "sha256": "b64f2289871261fdd1abbd3b78bcd66011b341de3dc8eeb2ed1a473ee7c8d95c", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00014-of-00014.safetensors", + "sha256": "e45b6c9998c77ee5a6577f9f47bc76416c1d4d387169e50c4c9d3134ea51b13b", + "size_bytes": 3270697008 + }, + { + "path": "FL2VA/text_encoder/model.safetensors.index.json", + "sha256": "06c952c569285870b811989b794b9766493e280fb77fbcb957fc4e5fcf25403a", + "size_bytes": 97831 + }, + { + "path": "FL2VA/text_encoder/preprocessor_config.json", + "sha256": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516", + "size_bytes": 390 + }, + { + "path": "FL2VA/text_encoder/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/text_encoder/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/text_encoder/video_preprocessor_config.json", + "sha256": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13", + "size_bytes": 385 + }, + { + "path": "FL2VA/text_encoder/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/tokenizer/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/tokenizer/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/tokenizer/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/tokenizer/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/transformer/config.json", + "sha256": "f619093a231fcfbcc3d035bec26c50ad864e7331a500d5c519f5045dc1e50458", + "size_bytes": 604 + }, + { + "path": "FL2VA/transformer/model-00001-of-00013.safetensors", + "sha256": "0b3386565e476bfdea287e9ea9f269d036e5c649ed14bb9b4afac1dc4661bd2a", + "size_bytes": 5227812968 + }, + { + "path": "FL2VA/transformer/model-00002-of-00013.safetensors", + "sha256": "9c98fd4579c9bc96d1b1bbf65c1243f9256df60bccd229570cecc61897773003", + "size_bytes": 5164578856 + }, + { + "path": "FL2VA/transformer/model-00003-of-00013.safetensors", + "sha256": "fa484c940d4170199fb69f5016739df2daf6ecd1150631b12a297e06e3964730", + "size_bytes": 5164578872 + }, + { + "path": "FL2VA/transformer/model-00004-of-00013.safetensors", + "sha256": "4a851df37cce2d14b39cb58df41def7b591b1f11b8520a32b421a5fa3e997c9f", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00005-of-00013.safetensors", + "sha256": "3fe6ff94dc9d3107c6776a25277d2b72b5443f88cf0f4ed835fd5b65a94ea5d4", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00006-of-00013.safetensors", + "sha256": "79b47e1b9ff03a2c6f06dd15a28972acf32c0867172ee34e5aa6e96b99494127", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00007-of-00013.safetensors", + "sha256": "6ac4d6ce639786b722a8dd99843ed56dcab48a0d174a4b0f9b8cdcbe5160fb0f", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00008-of-00013.safetensors", + "sha256": "03531812243fb0a333e01efa7e3d39750580bca8cbf2ba0529da419278eab736", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00009-of-00013.safetensors", + "sha256": "7e82a80b0d0d267026eb841b81523069cc7b956c1ebd8a18c07e462ffb262487", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00010-of-00013.safetensors", + "sha256": "ef0a1f6b65145232543dd2232d58d91a4d31f44b0da5084acf22cea123255481", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00011-of-00013.safetensors", + "sha256": "bf885b8f2f078c5499b75fded21094a26bfe0e6e00bee276e499443ecd2f298d", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00012-of-00013.safetensors", + "sha256": "dc717572d014ebf8527e35af815d10eb2f5a6bc87253b8a7d2f456965192db69", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00013-of-00013.safetensors", + "sha256": "8bfd852d5817e9836de1d3ec8dbac1c5446b167568b717371f08282b22291aa2", + "size_bytes": 4242305176 + }, + { + "path": "FL2VA/transformer/model.safetensors.index.json", + "sha256": "fb457a26ffa6294660e249b0ddd03a337f2e5393f770b5c34c8b8f90a29a7efb", + "size_bytes": 38323 + }, + { + "path": "FL2VA/video_vae/attention.py", + "sha256": "c9db8465c57f0bfb40c0194227be6e34b2c1ff7c5b5d63abca9964eed6283767", + "size_bytes": 5785 + }, + { + "path": "FL2VA/video_vae/base_module.py", + "sha256": "0e7ddf5086179a306298693ac461ffcbc27ccb28dbc786b1a05060de7a3357c0", + "size_bytes": 9520 + }, + { + "path": "FL2VA/video_vae/config.json", + "sha256": "3edd2cdd1ebc823c868be55ef917e1b3b8a398fde4d3150dae44a3bf05d9f627", + "size_bytes": 1807 + }, + { + "path": "FL2VA/video_vae/conv.py", + "sha256": "b3adee35f27e5d372543242aa86ce5e0138f13b794b6c81f919001e3e2346723", + "size_bytes": 4487 + }, + { + "path": "FL2VA/video_vae/flash.py", + "sha256": "c1918463d303a16f278a670bb4f33d3f8d6f7653b0d473313ff5835699b86d04", + "size_bytes": 5763 + }, + { + "path": "FL2VA/video_vae/func.py", + "sha256": "0612c26a65f095699cc7acd51937cd153584fa5a0476956e46a4679aa575c22d", + "size_bytes": 5840 + }, + { + "path": "FL2VA/video_vae/klvae.py", + "sha256": "d05a22b4918ad303f147d6b0d0ae9038bd1af1f810a5553b04f3542ab9bcff96", + "size_bytes": 48594 + }, + { + "path": "FL2VA/video_vae/minimax_h3_video_vae.py", + "sha256": "77fdea69a48485b5434f65d08096657b5d78b78b7cdf24e1241038f237720138", + "size_bytes": 5123 + }, + { + "path": "FL2VA/video_vae/norm.py", + "sha256": "258b120ca8d9a16366d7b811fe93eebf98fe1564db0995e8b7747229c651919a", + "size_bytes": 10686 + }, + { + "path": "FL2VA/video_vae/normalize.py", + "sha256": "8db651acb9eb551c906a2850bbca8e9d851f00d07b30ccf66ceb6057da687a13", + "size_bytes": 1200 + }, + { + "path": "FL2VA/video_vae/parallel.py", + "sha256": "7193522beafd65bc3dbe40b9843e360ab8da3a0bb211e9bfb67bb0d5d0e66919", + "size_bytes": 12997 + }, + { + "path": "FL2VA/video_vae/source/config.json", + "sha256": "66c68f541e6578ce613ce7a0fc985eb59097038829e49f7535e6d08e6d95ab12", + "size_bytes": 1164 + }, + { + "path": "FL2VA/video_vae/source/model.safetensors", + "sha256": "5f0c2e161d895a9fee7645ca32d4a7e3a22b90cacfcbeba62ec999cdbbefe0d3", + "size_bytes": 10415548320 + }, + { + "path": "FL2VA/video_vae/utils.py", + "sha256": "309e45e9d9e33cd5516a10eac2d207eb9f940079a6d9bbe0ee80aaaf7eb42dcd", + "size_bytes": 764 + }, + { + "path": "FL2VA/video_vae/vae_cnn.py", + "sha256": "3949b701e6893bd2ea070cd5b18ed8135a7c21d884290df449b1997212d65b11", + "size_bytes": 8836 + }, + { + "path": "FL2VA/video_vae/vae_module.py", + "sha256": "9558fb302e10a619f198e12e04701c68988e897d0b627f958f33d27fd2a30d94", + "size_bytes": 1884 + }, + { + "path": "FL2VA/video_vae/vae_processor.py", + "sha256": "32870580a1802d9163104a4d68170e35a4772106867181e1bd343dff11f87d2e", + "size_bytes": 8369 + }, + { + "path": "FL2VA/video_vae/vae_vit.py", + "sha256": "a372c28441b56ec2dbc6041dbd6ce2766c64c0e61b2c69a16594a97a11da3f58", + "size_bytes": 13490 + }, + { + "path": "LICENSE", + "sha256": "59b99642b95ea21630e311198ddbfffbfe05aadba0c2f5d884cbdf4efcc90f44", + "size_bytes": 17604 + }, + { + "path": "README.md", + "sha256": "f0116a90332496bdfcc827320c603a26b849c73bf804f2674d03682fbbd2334a", + "size_bytes": 38406 + }, + { + "path": "model_index.json", + "sha256": "5a587fe13b2371427415ac892463142683aefcd8d322e274a3a095eac37ac7d2", + "size_bytes": 2936 + }, + { + "path": "modular_model_index.json", + "sha256": "a2b6a210e482ffb78e613b553f570c44e101afce6741bd4ed91429d0559af031", + "size_bytes": 2935 + } + ], + "path": "/mnt/hf_hub_cache/models--MiniMaxAI--MiniMax-H3/snapshots/42ed227ee7df40d41602854ae760620d6eb651fe", + "revision": "42ed227ee7df40d41602854ae760620d6eb651fe" + }, + "plan": { + "cases": [ + { + "case_id": "drum-taps", + "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 + } + ], + "generation": { + "aspect_ratio": "16:9", + "audio_channels": 2, + "audio_flow_shift": 3.0, + "audio_sample_rate_hz": 32000, + "duration_seconds": 4, + "flow_shift": 12.0, + "fps": 24, + "frame_count": 107, + "height": 768, + "num_inference_steps": 50, + "width": 1344 + }, + "model_id": "MiniMaxAI/MiniMax-H3", + "model_revision": "42ed227ee7df40d41602854ae760620d6eb651fe", + "plan_id": "h3-serving-2gpu-c124-v1", + "repetitions": 4, + "warmup_runs": 1 + }, + "policy": { + "calibration_status": "uncalibrated", + "max_audio_rms_ratio_error": 0.1, + "max_latency_increase_fraction": 0.1, + "max_memory_increase_fraction": 0.1, + "min_audio_spectral_cosine": 0.95, + "min_video_psnr_db": 30, + "policy_id": "h3-example-uncalibrated-gpu-v1" + }, + "port": 30283, + "schema_version": "0.1.0", + "server": { + "dit_cpu_offload": false, + "encoder_parallel": "auto", + "performance_mode": "speed", + "tp_size": 1, + "ulysses_degree": 2 + }, + "serving": { + "concurrency": 1, + "delivery_deadline_seconds": null, + "mode": "closed_loop" + } +} From d2a5e52e5187b91c609752562800db57c95198d9 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:33 -0700 Subject: [PATCH 02/12] feat: add a shared MiniMax-H3 workload spec with plan hashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增 SGLang 与 vLLM-Omni 共用的 MiniMax-H3 workload spec 及 loader;生成的 plan 及其 plan_sha256 与 #2916 runner 一致。 --- experimental/video-generation/specs/README.md | 42 ++++ experimental/video-generation/specs/h3.yaml | 164 +++++++++++++ .../video-generation/specs/h3_spec.py | 231 ++++++++++++++++++ .../video-generation/tests/test_h3_spec.py | 209 ++++++++++++++++ 4 files changed, 646 insertions(+) create mode 100644 experimental/video-generation/specs/README.md create mode 100644 experimental/video-generation/specs/h3.yaml create mode 100644 experimental/video-generation/specs/h3_spec.py create mode 100644 experimental/video-generation/tests/test_h3_spec.py diff --git a/experimental/video-generation/specs/README.md b/experimental/video-generation/specs/README.md new file mode 100644 index 0000000000..4ae3dc4eec --- /dev/null +++ b/experimental/video-generation/specs/README.md @@ -0,0 +1,42 @@ +# H3 workload spec + +`h3.yaml` defines the MiniMax-H3 text-to-video-and-audio workload that SGLang and vLLM-Omni both run. +`h3_spec.py` loads and validates it, then builds the plan object that the runner from PR #2916 executes and hashes. + +## What the YAML records + +- `model` and every `generation` field hold a `value` and a `source` citation of the form `@::`. +- `sent.sglang` and `sent.vllm-omni` say whether that engine's request carries the field. + SGLang rejects caller-set fps and frame counts, runs at a fixed 24 fps, and derives the frame count and dimensions from its `target`. + Only vLLM-Omni receives fps, frame count, width, and height. +- `checked_on_clip` says whether the delivered clip is compared to the value. + The runner checks the clip's duration against `frame_count / fps` (107 / 24 = 4.458 s), so `duration_seconds` itself is not checked. +- `prompts` defines each prompt once with its motion and sound requirements. + Each entry in `sets` lists cases that pair a prompt with a seed, and its `seed_sources` cites every seed. +- `reference` records the SGLang reference run. It is informational, and no code reads or hashes it. + The run used workflow commit f7c7f58, whose spec, `mvp_runner.py`, and `mvp_media.py` match 0cb4c847d byte for byte. + +## Hash contract + +`canonical_json_bytes` is a byte-for-byte copy of the function in #2916's `evaluator/mvp_runner.py`. +It serializes with sorted keys, compact separators, UTF-8, and no NaN. + +- `plan_sha256(plan_for(spec, set_id))` equals the `plan_sha256` that #2916's runner records for the same plan. + The `frozen-c124` set reproduces #2916's `h3-serving-2gpu-c124-v1` plan, whose hash is `8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68`. +- `spec_sha256(spec)` covers the model, the generation values, and each set's resolved cases and counts. +- Citations, `sent`, `checked_on_clip`, and `reference` are outside both hashes, as are YAML key order, comments, and formatting. + Neither hash includes GPU UUIDs, job IDs, ports, or hosts. +- `plan_for(spec, set_id, repetitions=..., warmup_runs=...)` overrides the counts. + If either count differs from the set's own, the plan ID gains a `-r-w` suffix, so a shortened run never shares an ID or a hash with the full one. + +## Run the tests and print the hashes + +From the repository root: + +```bash +uv run --no-project --python 3.12 --with 'pyyaml>=6,<7' --with 'pytest>=8,<9' python -m pytest experimental/video-generation/tests/test_h3_spec.py -q +uv run --no-project --python 3.12 --with 'pyyaml>=6,<7' python experimental/video-generation/specs/h3_spec.py +``` + +The second command prints `spec_sha256` and each set's `plan_id` and `plan_sha256` as JSON. +To hash another spec file, pass its path as the argument. diff --git a/experimental/video-generation/specs/h3.yaml b/experimental/video-generation/specs/h3.yaml new file mode 100644 index 0000000000..f8283295ba --- /dev/null +++ b/experimental/video-generation/specs/h3.yaml @@ -0,0 +1,164 @@ +# `sent` follows the payload builders at +# feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/evaluator/mvp_runner.py:186-209 +# `checked_on_clip` follows the clip checks at +# feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/evaluator/mvp_media.py:384-425 +# against the expected media built at mvp_runner.py:613-615. + +schema_version: 1 + +model: + id: + value: MiniMaxAI/MiniMax-H3 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:492 + revision: + value: 42ed227ee7df40d41602854ae760620d6eb651fe + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:493 + +generation: + aspect_ratio: + value: "16:9" + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:480 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + audio_channels: + value: 2 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:481 + sent: {sglang: false, vllm-omni: false} + checked_on_clip: true + audio_flow_shift: + value: 3.0 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:482 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + audio_sample_rate_hz: + value: 32000 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:483 + sent: {sglang: false, vllm-omni: false} + checked_on_clip: true + duration_seconds: + value: 4 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:484 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + flow_shift: + value: 12.0 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:485 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + fps: + value: 24 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:486 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + frame_count: + # SGLang rejects caller-set fps and num_frames, runs at a fixed 24 fps, and + # snaps duration_seconds * fps = 96 up to its 17n+5 frame boundary, so the + # clip lasts 107 / 24 = 4.458 s rather than duration_seconds + # (sgl-project/sglang@71de97b26:python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/time_request.py:5). + value: 107 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:487 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + height: + value: 768 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:488 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + num_inference_steps: + value: 50 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:489 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + width: + value: 1344 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:490 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + +prompts: + drum-taps: + text: >- + 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 + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:21 + pouring-water: + text: >- + A single continuous close-up of a hand steadily pouring water from a + clear glass jug into an empty drinking glass on a wooden table. The water + level visibly rises and the pouring and splashing sounds are clearly + audible. Daylight, a still camera, no speech, no music, no subtitles, and + no scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:28 + bicycle-pass: + text: >- + A single continuous wide shot of a cyclist riding from the left side of + the frame to the right along a quiet park path. The moving wheels remain + visible and the bicycle bell rings once as the cyclist passes the camera. + Soft outdoor ambience is audible. The camera stays still, with no music, + speech, subtitles, or scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:35 + footsteps: + text: >- + A single continuous low-angle shot of a person in ordinary shoes walking + across a wooden floor from left to right. Each step visibly lands on the + floor and the corresponding footsteps are audible in the quiet room. The + camera stays still. No music, speech, subtitles, or scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:42 + +sets: + frozen-c124: + plan_id: h3-serving-2gpu-c124-v1 + repetitions: 4 + warmup_runs: 1 + seed_sources: + 11: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:476 + cases: + - {case_id: drum-taps, prompt: drum-taps, seed: 11} + engine-compare-v1: + plan_id: h3-engine-compare-v1 + repetitions: 1 + warmup_runs: 1 + seed_sources: + 11: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:8 + 29: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:22 + cases: + - {case_id: drum-taps-s11, prompt: drum-taps, seed: 11} + - {case_id: drum-taps-s29, prompt: drum-taps, seed: 29} + - {case_id: pouring-water-s11, prompt: pouring-water, seed: 11} + - {case_id: pouring-water-s29, prompt: pouring-water, seed: 29} + - {case_id: bicycle-pass-s11, prompt: bicycle-pass, seed: 11} + - {case_id: bicycle-pass-s29, prompt: bicycle-pass, seed: 29} + - {case_id: footsteps-s11, prompt: footsteps, seed: 11} + - {case_id: footsteps-s29, prompt: footsteps, seed: 29} + +# Informational only; not hashed. +reference: + sglang: + pull_request: https://github.com/SemiAnalysisAI/InferenceX/pull/2916 + run: https://github.com/SemiAnalysisAI/InferenceX/actions/runs/39002901777 + hardware: H200 + workflow_sha: f7c7f581088468e0c4074836603d2f90fcd5986b + plan_id: h3-serving-2gpu-c124-v1 + recorded_plan_sha256: 8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68 + outcome: >- + Concurrency cells 1, 2 and 4 each completed 5 clips (1 warmup, 4 + measured); all 15 decoded as valid 1344x768, 107 frames, 24 fps, 32000 Hz + stereo. + sglang_revision: 71de97b264b04dcd514cf904003028aefe9775c8 + server: {ulysses_degree: 2, tp_size: 1, port: 30283} + sources: + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:13 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:507 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:513 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:514 diff --git a/experimental/video-generation/specs/h3_spec.py b/experimental/video-generation/specs/h3_spec.py new file mode 100644 index 0000000000..370cb48324 --- /dev/null +++ b/experimental/video-generation/specs/h3_spec.py @@ -0,0 +1,231 @@ +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +import yaml + +ENGINES = ("sglang", "vllm-omni") +DEFAULT_SPEC = Path(__file__).with_name("h3.yaml") +_CITATION = re.compile(r"[\w./-]+@[0-9a-f]{7,40}:[\w./-]+:\d+") +_REVISION = re.compile(r"[0-9a-f]{40}") +_MAX_SEED = 2**63 + + +def canonical_json_bytes(value: Any) -> bytes: + """Stable hash representation, UTF-8, sorted keys, no trailing newline.""" + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False + ).encode("utf-8") + + +def _sha256(value: Any) -> str: + return hashlib.sha256(canonical_json_bytes(value)).hexdigest() + + +def _fields(value: Any, where: str, keys: set[str]) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != keys: + raise ValueError(f"{where} must be a mapping with exactly the keys {sorted(keys)}") + return value + + +def _text(value: Any, where: str) -> None: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{where} must be a nonempty string") + + +def _flag(value: Any, where: str) -> None: + if not isinstance(value, bool): + raise ValueError(f"{where} must be true or false") + + +def _integer(value: Any, where: str, *, minimum: int, below: int | None = None) -> None: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < minimum + or (below is not None and value >= below) + ): + bound = f"in [{minimum}, {below})" if below is not None else f">= {minimum}" + raise ValueError(f"{where} must be an integer {bound}") + + +def _citation(value: Any, where: str) -> None: + if not isinstance(value, str) or not _CITATION.fullmatch(value): + raise ValueError(f"{where} must cite @::") + + +def _validate_set(set_id: str, prompt_set: Any, prompts: dict[str, Any]) -> None: + where = f"sets.{set_id}" + _fields(prompt_set, where, {"plan_id", "repetitions", "warmup_runs", "seed_sources", "cases"}) + _text(prompt_set["plan_id"], f"{where}.plan_id") + _integer(prompt_set["repetitions"], f"{where}.repetitions", minimum=1) + _integer(prompt_set["warmup_runs"], f"{where}.warmup_runs", minimum=0) + cases = prompt_set["cases"] + if not isinstance(cases, list) or not cases: + raise ValueError(f"{where}.cases must be a nonempty list") + case_ids: set[str] = set() + for index, case in enumerate(cases): + case_where = f"{where}.cases[{index}]" + _fields(case, case_where, {"case_id", "prompt", "seed"}) + _text(case["case_id"], f"{case_where}.case_id") + if case["case_id"] in case_ids: + raise ValueError(f"{case_where}.case_id {case['case_id']!r} is not unique") + case_ids.add(case["case_id"]) + if case["prompt"] not in prompts: + raise ValueError(f"{case_where}.prompt {case['prompt']!r} is not a defined prompt") + _integer(case["seed"], f"{case_where}.seed", minimum=0, below=_MAX_SEED) + seed_sources = prompt_set["seed_sources"] + seeds = {case["seed"] for case in cases} + if not isinstance(seed_sources, dict) or set(seed_sources) != seeds: + raise ValueError(f"{where}.seed_sources must cite exactly the seeds {sorted(seeds)}") + for seed, source in seed_sources.items(): + _citation(source, f"{where}.seed_sources[{seed}]") + + +def load_spec(path: str | Path) -> dict[str, Any]: + """Parse and validate a spec file; raise ValueError naming the first bad field.""" + spec = yaml.safe_load(Path(path).read_text(encoding="utf-8")) + _fields(spec, "spec", {"schema_version", "model", "generation", "prompts", "sets", "reference"}) + if isinstance(spec["schema_version"], bool) or spec["schema_version"] != 1: + raise ValueError("schema_version must be 1") + model = _fields(spec["model"], "model", {"id", "revision"}) + for name, entry in model.items(): + _fields(entry, f"model.{name}", {"value", "source"}) + _text(entry["value"], f"model.{name}.value") + _citation(entry["source"], f"model.{name}.source") + if not _REVISION.fullmatch(model["revision"]["value"]): + raise ValueError("model.revision.value must be a 40-character commit") + + generation = spec["generation"] + if not isinstance(generation, dict) or not generation: + raise ValueError("generation must be a nonempty mapping") + for name, field in generation.items(): + where = f"generation.{name}" + _fields(field, where, {"value", "source", "sent", "checked_on_clip"}) + if isinstance(field["value"], bool) or not isinstance(field["value"], int | float | str): + raise ValueError(f"{where}.value must be a number or string") + _citation(field["source"], f"{where}.source") + sent = _fields(field["sent"], f"{where}.sent", set(ENGINES)) + for engine, carried in sent.items(): + _flag(carried, f"{where}.sent.{engine}") + _flag(field["checked_on_clip"], f"{where}.checked_on_clip") + + prompts = spec["prompts"] + if not isinstance(prompts, dict) or not prompts: + raise ValueError("prompts must be a nonempty mapping") + for name, prompt in prompts.items(): + where = f"prompts.{name}" + _fields(prompt, where, {"text", "requires_motion", "requires_sound", "source"}) + _text(prompt["text"], f"{where}.text") + _flag(prompt["requires_motion"], f"{where}.requires_motion") + _flag(prompt["requires_sound"], f"{where}.requires_sound") + _citation(prompt["source"], f"{where}.source") + + sets = spec["sets"] + if not isinstance(sets, dict) or not sets: + raise ValueError("sets must be a nonempty mapping") + for set_id, prompt_set in sets.items(): + _validate_set(set_id, prompt_set, prompts) + + if not isinstance(spec["reference"], dict): + raise ValueError("reference must be a mapping") + return spec + + +def generation_values(spec: dict[str, Any]) -> dict[str, Any]: + return {name: field["value"] for name, field in spec["generation"].items()} + + +def _set_plan_fields(spec: dict[str, Any], set_id: str) -> dict[str, Any]: + prompt_set = spec["sets"][set_id] + cases = [] + for case in prompt_set["cases"]: + prompt = spec["prompts"][case["prompt"]] + cases.append( + { + "case_id": case["case_id"], + "prompt": prompt["text"], + "seed": case["seed"], + "requires_motion": prompt["requires_motion"], + "requires_sound": prompt["requires_sound"], + } + ) + return { + "plan_id": prompt_set["plan_id"], + "cases": cases, + "repetitions": prompt_set["repetitions"], + "warmup_runs": prompt_set["warmup_runs"], + } + + +def plan_for( + spec: dict[str, Any], + set_id: str, + *, + repetitions: int | None = None, + warmup_runs: int | None = None, +) -> dict[str, Any]: + """Build the #2916 plan object for one prompt set. + + Count overrides that differ from the set's own counts rename the plan + (``-r-w``), so a shortened run never + shares an identity with the full one. + """ + plan = { + "model_id": spec["model"]["id"]["value"], + "model_revision": spec["model"]["revision"]["value"], + "generation": generation_values(spec), + **_set_plan_fields(spec, set_id), + } + if repetitions is not None: + _integer(repetitions, "repetitions", minimum=1) + plan["repetitions"] = repetitions + if warmup_runs is not None: + _integer(warmup_runs, "warmup_runs", minimum=0) + plan["warmup_runs"] = warmup_runs + prompt_set = spec["sets"][set_id] + if (plan["repetitions"], plan["warmup_runs"]) != ( + prompt_set["repetitions"], + prompt_set["warmup_runs"], + ): + plan["plan_id"] += f"-r{plan['repetitions']}-w{plan['warmup_runs']}" + return plan + + +def plan_sha256(plan: dict[str, Any]) -> str: + """Equal to the ``plan_sha256`` that PR #2916's runner records for the same plan.""" + return _sha256(plan) + + +def spec_sha256(spec: dict[str, Any]) -> str: + """Hash the model, the generation values, and each set's plan fields; nothing else counts.""" + return _sha256( + { + "model": { + "id": spec["model"]["id"]["value"], + "revision": spec["model"]["revision"]["value"], + }, + "generation": generation_values(spec), + "sets": {set_id: _set_plan_fields(spec, set_id) for set_id in spec["sets"]}, + } + ) + + +def main(argv: list[str]) -> None: + spec = load_spec(argv[1] if len(argv) > 1 else DEFAULT_SPEC) + plans = {set_id: plan_for(spec, set_id) for set_id in spec["sets"]} + report = { + "spec_sha256": spec_sha256(spec), + "sets": { + set_id: {"plan_id": plan["plan_id"], "plan_sha256": plan_sha256(plan)} + for set_id, plan in plans.items() + }, + } + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/experimental/video-generation/tests/test_h3_spec.py b/experimental/video-generation/tests/test_h3_spec.py new file mode 100644 index 0000000000..fb05888d35 --- /dev/null +++ b/experimental/video-generation/tests/test_h3_spec.py @@ -0,0 +1,209 @@ +import copy +import functools +import json +import operator +import re +import sys +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest +import yaml + +SPECS = Path(__file__).resolve().parents[1] / "specs" +sys.path.insert(0, str(SPECS)) +import h3_spec # noqa: E402 + +SPEC_PATH = SPECS / "h3.yaml" +FIXTURE_PATH = Path(__file__).with_name("fixtures") / "h3-serving-2gpu.json" +RUN_39002901777_RECORDED_PLAN_SHA256 = ( + "8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68" +) +FROZEN = "frozen-c124" +CITATION = "feat/example@0123456789abcdef:specs/source.json:1" + + +@pytest.fixture(scope="module") +def spec() -> dict[str, Any]: + return h3_spec.load_spec(SPEC_PATH) + + +@pytest.fixture(scope="module") +def reference_plan() -> dict[str, Any]: + return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))["plan"] + + +def _assign(tree: Any, path: tuple[Any, ...], value: Any) -> None: + *parents, last = path + functools.reduce(operator.getitem, parents, tree)[last] = value + + +def _leaves(value: Any, path: tuple[Any, ...] = ()) -> Iterator[tuple[tuple[Any, ...], Any]]: + if isinstance(value, dict): + for key, item in value.items(): + yield from _leaves(item, (*path, key)) + elif isinstance(value, list): + for index, item in enumerate(value): + yield from _leaves(item, (*path, index)) + else: + yield path, value + + +def _spec_path(spec: dict[str, Any], plan_path: tuple[Any, ...]) -> tuple[Any, ...]: + match plan_path: + case ("model_id",): + return ("model", "id", "value") + case ("model_revision",): + return ("model", "revision", "value") + case ("generation", name): + return ("generation", name, "value") + case ("cases", index, "prompt" | "requires_motion" | "requires_sound" as key): + prompt = spec["sets"][FROZEN]["cases"][index]["prompt"] + return ("prompts", prompt, "text" if key == "prompt" else key) + case ("cases", index, key): + return ("sets", FROZEN, "cases", index, key) + case (key,): + return ("sets", FROZEN, key) + raise AssertionError(plan_path) + + +def _mutated(value: Any) -> Any: + if isinstance(value, bool): + return not value + if isinstance(value, int | float): + return value + 1 + return f"{value}x" + + +def _reverse_keys(value: Any) -> Any: + if isinstance(value, dict): + return {key: _reverse_keys(value[key]) for key in reversed(value)} + if isinstance(value, list): + return [_reverse_keys(item) for item in value] + return value + + +def test_frozen_set_reproduces_the_2916_plan_and_its_recorded_hash( + spec: dict[str, Any], reference_plan: dict[str, Any] +) -> None: + plan = h3_spec.plan_for(spec, FROZEN) + assert plan == reference_plan + # Dict equality treats 3 and 3.0 alike; the hashes do not. + assert h3_spec.plan_sha256(plan) == RUN_39002901777_RECORDED_PLAN_SHA256 + assert h3_spec.plan_sha256(reference_plan) == RUN_39002901777_RECORDED_PLAN_SHA256 + + +def test_hashes_ignore_key_order_formatting_and_comments( + spec: dict[str, Any], tmp_path: Path +) -> None: + raw = yaml.safe_load(SPEC_PATH.read_text(encoding="utf-8")) + body = yaml.safe_dump(_reverse_keys(raw), sort_keys=False, indent=4, width=50) + rewritten = tmp_path / "h3.yaml" + rewritten.write_text(f"# rewritten copy\n{body}# end\n\n", encoding="utf-8") + reloaded = h3_spec.load_spec(rewritten) + for set_id in spec["sets"]: + assert h3_spec.plan_sha256(h3_spec.plan_for(reloaded, set_id)) == h3_spec.plan_sha256( + h3_spec.plan_for(spec, set_id) + ) + assert h3_spec.spec_sha256(reloaded) == h3_spec.spec_sha256(spec) + + +def test_each_spec_value_changes_only_its_plan_field_and_the_hash( + spec: dict[str, Any], reference_plan: dict[str, Any] +) -> None: + base = h3_spec.plan_for(spec, FROZEN) + base_leaves = dict(_leaves(base)) + owners = {h3_spec.plan_sha256(base): ("unmutated",)} + for plan_path, value in _leaves(reference_plan): + mutated = copy.deepcopy(spec) + _assign(mutated, _spec_path(mutated, plan_path), _mutated(value)) + plan = h3_spec.plan_for(mutated, FROZEN) + assert [path for path, item in _leaves(plan) if base_leaves[path] != item] == [plan_path] + digest = h3_spec.plan_sha256(plan) + assert digest not in owners, f"{plan_path} hashes like {owners[digest]}" + owners[digest] = plan_path + + +def test_count_overrides_rename_the_plan_and_change_its_hash(spec: dict[str, Any]) -> None: + full = h3_spec.plan_for(spec, FROZEN) + single = h3_spec.plan_for(spec, FROZEN, repetitions=1, warmup_runs=0) + assert single == { + **full, + "plan_id": f"{full['plan_id']}-r1-w0", + "repetitions": 1, + "warmup_runs": 0, + } + assert h3_spec.plan_sha256(single) != h3_spec.plan_sha256(full) + same_counts = h3_spec.plan_for( + spec, FROZEN, repetitions=full["repetitions"], warmup_runs=full["warmup_runs"] + ) + assert same_counts == full + with pytest.raises(ValueError, match=re.escape("repetitions must be an integer >= 1")): + h3_spec.plan_for(spec, FROZEN, repetitions=0) + + +def _minimal_spec() -> dict[str, Any]: + return { + "schema_version": 1, + "model": { + "id": {"value": "org/model", "source": CITATION}, + "revision": {"value": "a" * 40, "source": CITATION}, + }, + "generation": { + "fps": { + "value": 24, + "source": CITATION, + "sent": {"sglang": False, "vllm-omni": True}, + "checked_on_clip": True, + }, + }, + "prompts": { + "p": { + "text": "a prompt", + "requires_motion": True, + "requires_sound": False, + "source": CITATION, + }, + }, + "sets": { + "s": { + "plan_id": "plan", + "repetitions": 1, + "warmup_runs": 0, + "seed_sources": {7: CITATION}, + "cases": [{"case_id": "c", "prompt": "p", "seed": 7}], + }, + }, + "reference": {}, + } + + +@pytest.mark.parametrize( + ("path", "value", "message"), + [ + (("generation", "fps", "sent"), {"sglang": False}, "generation.fps.sent must be a mapping"), + (("generation", "fps", "source"), "mvp_runner.py:186", "generation.fps.source must cite"), + (("generation", "fps", "value"), True, "generation.fps.value must be a number or string"), + (("sets", "s", "cases", 0, "seed"), 2**63, "sets.s.cases[0].seed must be an integer"), + (("sets", "s", "cases", 0, "seed"), True, "sets.s.cases[0].seed must be an integer"), + (("sets", "s", "cases", 0, "prompt"), "q", "sets.s.cases[0].prompt 'q' is not a defined"), + ( + ("sets", "s", "cases"), + [{"case_id": "c", "prompt": "p", "seed": n} for n in (7, 7)], + "is not unique", + ), + (("sets", "s", "seed_sources"), {}, "sets.s.seed_sources must cite exactly the seeds [7]"), + (("sets",), {}, "sets must be a nonempty mapping"), + (("schema_version",), True, "schema_version must be 1"), + ], +) +def test_load_spec_rejects_malformed_specs( + tmp_path: Path, path: tuple[Any, ...], value: Any, message: str +) -> None: + spec = _minimal_spec() + _assign(spec, path, value) + target = tmp_path / "spec.yaml" + target.write_text(yaml.safe_dump(spec), encoding="utf-8") + with pytest.raises(ValueError, match=re.escape(message)): + h3_spec.load_spec(target) From f9c8154f2fafcf4e79c0fc1e2625fd0a01db4210 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:35 -0700 Subject: [PATCH 03/12] ci: test and lint the H3 workload spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增 CI,对 H3 workload spec 运行测试、lint 和格式检查。 --- .github/workflows/test-h3-spec.yml | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/test-h3-spec.yml diff --git a/.github/workflows/test-h3-spec.yml b/.github/workflows/test-h3-spec.yml new file mode 100644 index 0000000000..a23203f3f7 --- /dev/null +++ b/.github/workflows/test-h3-spec.yml @@ -0,0 +1,43 @@ +name: Test H3 Spec + +on: + pull_request: + paths: &h3-spec-paths + - 'experimental/video-generation/specs/**' + - 'experimental/video-generation/tests/**' + - '.github/workflows/test-h3-spec.yml' + push: + paths: *h3-spec-paths + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: H3 spec loader and hashes + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - name: Test spec loader and hashes + run: >- + uv run --no-project --python 3.12 + --with 'pyyaml>=6,<7' --with 'pytest>=8,<9' + python -m pytest experimental/video-generation/tests/test_h3_spec.py -q + - name: Lint and format + # The infx config never lints tests/, so assert (S101) is allowed there. + # These standalone modules have no package, so INP001 does not apply. + run: | + uvx ruff@0.14.0 check --config inferencex-e2e/infx/ruff.toml --ignore INP001 \ + experimental/video-generation/specs/h3_spec.py + uvx ruff@0.14.0 check --config inferencex-e2e/infx/ruff.toml --ignore INP001,S101 \ + experimental/video-generation/tests/test_h3_spec.py + uvx ruff@0.14.0 format --check --config inferencex-e2e/infx/ruff.toml \ + experimental/video-generation/specs/h3_spec.py experimental/video-generation/tests/test_h3_spec.py From 64a04725b9748b628e781bb19204e9372e6ed1d2 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:57:22 -0700 Subject: [PATCH 04/12] docs: add vLLM-Omni serving notes for MiniMax-H3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:记录在 2x H100 上服务 MiniMax-H3 所用的 vLLM-Omni 镜像 pin、server 命令、并行配置与 attention backend,供 Phase B 使用。 --- experimental/video-generation/specs/README.md | 1 + .../specs/h3-vllm-omni-serving.md | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 experimental/video-generation/specs/h3-vllm-omni-serving.md diff --git a/experimental/video-generation/specs/README.md b/experimental/video-generation/specs/README.md index 4ae3dc4eec..d6f4be5fda 100644 --- a/experimental/video-generation/specs/README.md +++ b/experimental/video-generation/specs/README.md @@ -2,6 +2,7 @@ `h3.yaml` defines the MiniMax-H3 text-to-video-and-audio workload that SGLang and vLLM-Omni both run. `h3_spec.py` loads and validates it, then builds the plan object that the runner from PR #2916 executes and hashes. +`h3-vllm-omni-serving.md` records the pinned vLLM-Omni image, server command, parallelism and attention backend that served this workload on 2x H100. ## What the YAML records diff --git a/experimental/video-generation/specs/h3-vllm-omni-serving.md b/experimental/video-generation/specs/h3-vllm-omni-serving.md new file mode 100644 index 0000000000..b6bb4381d6 --- /dev/null +++ b/experimental/video-generation/specs/h3-vllm-omni-serving.md @@ -0,0 +1,49 @@ +# Serving MiniMax-H3 on vLLM-Omni + +What the Phase A feasibility run used on 2x H100 80GB on 2026-10-17. Phase B benchmarking starts from this and records any change. + +## Pins + +| Component | Value | +| --- | --- | +| vLLM-Omni | release v0.30.0, commit `a8576ccb725c4e21cd13c3eb5f9a546b21149d2b` | +| Image | `vllm/vllm-omni:v0.30.0`, manifest digest `sha256:fc77cfaac7b1b43c30cbf32eaea214864434c6e0f303e6e9eb9f64104241a994`, linux/amd64 `sha256:b09c9eab7d2e30817208de92ccb482756d13d4339377d7cc4c4ce9f47bf04174` | +| In-image versions | vllm 0.30.0, vllm-omni 0.30.0, torch 2.13.0+cu130 | +| Model | `MiniMaxAI/MiniMax-H3` revision `42ed227ee7df40d41602854ae760620d6eb651fe` (same as `specs/h3.yaml` and PR #2916) | + +Only the `FL2VA/` partition plus root `model_index.json`, `LICENSE`, `README.md` and `Ref2VA/model_index.json` need to be on disk (129 GB); `--task-type fl2va` loads nothing else. Leave the root `modular_model_index.json` out of a local copy: with it present, vllm-omni misdetects the directory as FastH3 (upstream issue [#8796](https://github.com/vllm-project/vllm-omni/issues/8796)). + +## Server command + +``` +vllm serve --served-model-name MiniMaxAI/MiniMax-H3 --omni --host 0.0.0.0 --port 30283 \ + --trust-remote-code --task-type fl2va --num-gpus 2 --tensor-parallel-size 2 --usp 1 --ring 1 \ + --text-encoder-tp-size 2 --vae-patch-parallel-size 2 --vae-parallel-mode tile --vae-use-tiling \ + --enforce-eager +``` + +`` is the Hugging Face cache snapshot directory for the revision above. Serving the Hub ID with `--revision` while `HF_HUB_OFFLINE=1` fails at startup with `huggingface_hub.errors.IncompleteSnapshotError` because `snapshot_download` requires every file of the repository and resolves `refs/main`; the directory path avoids that. `--served-model-name` keeps the model name the evaluator sends. + +Environment: + +``` +HF_HUB_OFFLINE=1 +TRANSFORMERS_OFFLINE=1 +HF_HUB_CACHE= +VLLM_WORKER_MULTIPROC_METHOD=spawn +VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 +``` + +## Parallelism + +Tensor parallel 2 across both GPUs with Ulysses and ring degree 1, text encoder TP 2, VAE patch-parallel 2 in tile mode with tiling. This is the upstream recipe's only two-GPU resident layout; pure Ulysses replicates the whole DiT on every rank, so SGLang's `ulysses_degree 2 / tp_size 1` layout from #2916 does not fit 80 GB here. Phase A ran eager; Phase B should also try without `--enforce-eager` and report compile time separately. + +## Attention backend + +Resolved by the server at startup, no flag passed: `FLASH_ATTN` for `role='minimax_h3.token_refiner'` and for `role='self'` (log line from `selector.py:84/93`). + +## Observed on 2x H100 (informational, not a benchmark) + +- Model load: 61.3 GiB and 282 s per TP worker; `/health` 200 about 5 min 20 s after launch. +- One request from the `frozen-c124` set with `repetitions=1, warmup_runs=0`: 50 denoise steps at 2.35 s/it, 125.4 s submit to validated clip, both GPUs at the 700 W cap with 70.2 GB used. +- The request carries width, height, fps and num_frames plus `extra_params` (`task`, `target`, `audio_flow_shift`); the server logged `Applied extra_params` and produced 1344x768, 107 frames, 24 fps, 32 kHz stereo, matching the spec's clip checks. From b5948031ce8bd77c9e5d2ec9caa066b18e86b530 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:29 -0700 Subject: [PATCH 05/12] test: add the PR #2916 H3 serving spec as a fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:将 PR #2916 的 H3 serving spec 原样复制为测试 fixture。 --- .../video-generation/tests/fixtures/README.md | 3 + .../tests/fixtures/h3-serving-2gpu.json | 521 ++++++++++++++++++ 2 files changed, 524 insertions(+) create mode 100644 experimental/video-generation/tests/fixtures/README.md create mode 100644 experimental/video-generation/tests/fixtures/h3-serving-2gpu.json diff --git a/experimental/video-generation/tests/fixtures/README.md b/experimental/video-generation/tests/fixtures/README.md new file mode 100644 index 0000000000..0a87e1025d --- /dev/null +++ b/experimental/video-generation/tests/fixtures/README.md @@ -0,0 +1,3 @@ +`h3-serving-2gpu.json` is a verbatim copy of `experimental/video-generation/specs/h3-serving-2gpu.json` at commit 0cb4c847d8c8089cc9f25a8a8c826d781f9b9f22 of PR #2916 (git blob 404bc4f02). +Its SHA-256 is `a60583798a23f259a4d0185315d97b8bd3a1cca94e623068a2e7c95be5921b1d`. Do not edit it. +Recreate it with `git show 0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json > h3-serving-2gpu.json`. diff --git a/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json b/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json new file mode 100644 index 0000000000..404bc4f02b --- /dev/null +++ b/experimental/video-generation/tests/fixtures/h3-serving-2gpu.json @@ -0,0 +1,521 @@ +{ + "allocation": { + "label": "srt-slurm job assigned at launch", + "mode": "dedicated_ci" + }, + "authorization": { + "approval_reference": "InferenceX #2916 native srt-slurm H3 serving run, authorized by the repository owner on 2026-10-17 (SemiAnalysis clusters only); model use per the MiniMax-H3 license review recorded for the #2894 baseline (user-confirmation-2026-09-02).", + "compute_approved": true, + "model_license_reviewed": true + }, + "baseline": { + "python": "/usr/bin/python3", + "revision": "71de97b264b04dcd514cf904003028aefe9775c8", + "source": "/sgl-workspace/sglang", + "source_sha256": "e6069f8b948234075fdcfba23e459e2f56a45b11d0ba3ee5ecbb754e338deb13" + }, + "candidate": { + "python": "/usr/bin/python3", + "revision": "71de97b264b04dcd514cf904003028aefe9775c8", + "source": "/sgl-workspace/sglang", + "source_sha256": "e6069f8b948234075fdcfba23e459e2f56a45b11d0ba3ee5ecbb754e338deb13" + }, + "gpu_uuids": [ + "GPU-00000000-0000-0000-0000-000000000000", + "GPU-00000000-0000-0000-0000-000000000001" + ], + "job_id": "h3-serving-2gpu", + "limits": { + "cleanup_seconds": 60, + "command_seconds": 30, + "job_seconds": 3600, + "max_idle_memory_mib": 64, + "request_seconds": 900, + "startup_seconds": 900, + "telemetry_interval_seconds": 1 + }, + "lock_directory": "/logs/h3/locks", + "model": { + "files": [ + { + "path": "FL2VA/audio_vae/config.json", + "sha256": "d8f3bcc62e23c7e9806970fa63cca6139c06faa3797cf9c94034f60db8512771", + "size_bytes": 1973 + }, + { + "path": "FL2VA/audio_vae/config.yaml", + "sha256": "259f675c9f71eedc24ca4f23965cd32a1b3878fc894f56664c40328d99831a5e", + "size_bytes": 91 + }, + { + "path": "FL2VA/audio_vae/dac_activations.py", + "sha256": "74ca8bb9e8039f1ff362ca4219eee5240a7df9502f3dbdc3161d4e86fbd6dc04", + "size_bytes": 2189 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_act.py", + "sha256": "db8761e66c0eaf9fce2dcccb59162d52b22ca0de9f699cf7d21ec7cd507e86fd", + "size_bytes": 835 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_filter.py", + "sha256": "a369523cd7f14f4f299d8e2e47f3c473da76ed1910e6e9134fd7a9fce02537e8", + "size_bytes": 3300 + }, + { + "path": "FL2VA/audio_vae/dac_alias_free_resample.py", + "sha256": "fe35055893833c563e42adc5e5882cbe7628c82d48d33c300fcf0a9576997910", + "size_bytes": 1740 + }, + { + "path": "FL2VA/audio_vae/dac_attn_proj.py", + "sha256": "0e04556fa5fd38d4a71e62fcf4d3242fd9a2464fd72ed62baef2ec0980d910c6", + "size_bytes": 3317 + }, + { + "path": "FL2VA/audio_vae/dac_audio_vae.py", + "sha256": "ec04939602d4710d039e83f558a7572547669652f7fa967e3de0a3b6c48cfd48", + "size_bytes": 7266 + }, + { + "path": "FL2VA/audio_vae/dac_bigvgan.py", + "sha256": "f5df4e6f633e74da479549ef7a6d2684964e4b65ac42c6765be42f9c7fcb7bd2", + "size_bytes": 7102 + }, + { + "path": "FL2VA/audio_vae/dac_utils.py", + "sha256": "74ef0ba2c2a11ca35a7c0cc166fe88e564de01230000a0bcc8429103312ad9f0", + "size_bytes": 362 + }, + { + "path": "FL2VA/audio_vae/metadata.json", + "sha256": "755d0529d43b2b5c83590f6f44ca659bc68e6a21b01d5669c93e8b2965749bff", + "size_bytes": 440 + }, + { + "path": "FL2VA/audio_vae/minimax_h3_audio_vae.py", + "sha256": "63bc0dab6def69480cfd77ea820f9a9a84fc4bd6e56868e4f45ec1e1e200578f", + "size_bytes": 3532 + }, + { + "path": "FL2VA/audio_vae/model.safetensors", + "sha256": "37dddc2f3e6d5d5139d823d5ea283bbf304dadcb885b1ccda818aa13dade5ea2", + "size_bytes": 605429308 + }, + { + "path": "FL2VA/model_index.json", + "sha256": "d1113e0f123c69f79cd0de35ca1771606ebc3ec924270d257b771f96f584aa6b", + "size_bytes": 719 + }, + { + "path": "FL2VA/processor/chat_template.json", + "sha256": "5c72a170d2a4a1a3bc5adad2e689ae28138a9700e5b8c96c0266331e86c0acce", + "size_bytes": 5499 + }, + { + "path": "FL2VA/processor/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/processor/preprocessor_config.json", + "sha256": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516", + "size_bytes": 390 + }, + { + "path": "FL2VA/processor/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/processor/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/processor/video_preprocessor_config.json", + "sha256": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13", + "size_bytes": 385 + }, + { + "path": "FL2VA/processor/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/text_encoder/chat_template.json", + "sha256": "5c72a170d2a4a1a3bc5adad2e689ae28138a9700e5b8c96c0266331e86c0acce", + "size_bytes": 5499 + }, + { + "path": "FL2VA/text_encoder/config.json", + "sha256": "d2dd0c60d01b9e195d9447c52da61c7302d28828524914c044d9c6e1b81d0427", + "size_bytes": 1474 + }, + { + "path": "FL2VA/text_encoder/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/text_encoder/model-00001-of-00014.safetensors", + "sha256": "6b9dfbc930e505402ae9d7e5091a9d7d656cda5f34614f01cfe70bfb0cca27cb", + "size_bytes": 4932328944 + }, + { + "path": "FL2VA/text_encoder/model-00002-of-00014.safetensors", + "sha256": "d8bb44b4ff303fe76fe9e894022fb3dc71b15a2e716592790fe0e3c3e60478fa", + "size_bytes": 4875990528 + }, + { + "path": "FL2VA/text_encoder/model-00003-of-00014.safetensors", + "sha256": "54f22e8b3168f8dc962fac0d313607ebf52a12b433d4cf3098a0d82d9f042940", + "size_bytes": 4875990552 + }, + { + "path": "FL2VA/text_encoder/model-00004-of-00014.safetensors", + "sha256": "ad09c74d3c13ee29b5d0d84548fd8a3424a651564eaccd519946c296e59c557f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00005-of-00014.safetensors", + "sha256": "fc993c8a0e2a5b0570f383e1a95dc3a1281d1b224b6f3ee908f4827941e1dfc2", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00006-of-00014.safetensors", + "sha256": "82f05620d1f718a90c362b221d6a184ff1a0f53301d706882d3df49695fa1974", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00007-of-00014.safetensors", + "sha256": "fb91da8cb01ff4de3eef0eab1c3e769a734b3a1aafc61734068638a0d6c86934", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00008-of-00014.safetensors", + "sha256": "431ca56535c8781944ce3801f5eb61c45531e853ecc5846d936ebaf4761b764f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00009-of-00014.safetensors", + "sha256": "3825e3f4302f4d2f7d76aa7430d2ce0864fde6b9e540a5806bf0d8e38e4d9f47", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00010-of-00014.safetensors", + "sha256": "aded5a4d1d5e22dbd8b6f79266b6eb88c840411b09527c53917a1419ace22e2f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00011-of-00014.safetensors", + "sha256": "3820ffe8d8d6477f6fe8d614ef3c87abb264ee39accebf43a1507b970d80946f", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00012-of-00014.safetensors", + "sha256": "05ad2d08ce71963121c9b03f1d9ec5d7641052f4b23c6c12b80d71065eb8e98e", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00013-of-00014.safetensors", + "sha256": "b64f2289871261fdd1abbd3b78bcd66011b341de3dc8eeb2ed1a473ee7c8d95c", + "size_bytes": 4875990584 + }, + { + "path": "FL2VA/text_encoder/model-00014-of-00014.safetensors", + "sha256": "e45b6c9998c77ee5a6577f9f47bc76416c1d4d387169e50c4c9d3134ea51b13b", + "size_bytes": 3270697008 + }, + { + "path": "FL2VA/text_encoder/model.safetensors.index.json", + "sha256": "06c952c569285870b811989b794b9766493e280fb77fbcb957fc4e5fcf25403a", + "size_bytes": 97831 + }, + { + "path": "FL2VA/text_encoder/preprocessor_config.json", + "sha256": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516", + "size_bytes": 390 + }, + { + "path": "FL2VA/text_encoder/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/text_encoder/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/text_encoder/video_preprocessor_config.json", + "sha256": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13", + "size_bytes": 385 + }, + { + "path": "FL2VA/text_encoder/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/tokenizer/merges.txt", + "sha256": "599bab54075088774b1733fde865d5bd747cbcc7a547c5bc12610e874e26f5e3", + "size_bytes": 1671839 + }, + { + "path": "FL2VA/tokenizer/tokenizer.json", + "sha256": "a5d85b6dcc535e6b93115a9ef287e6132fdbf30270da6218194ba742261173c7", + "size_bytes": 7032403 + }, + { + "path": "FL2VA/tokenizer/tokenizer_config.json", + "sha256": "a07e942ac874baa13758de8d1fbdb186683cc03416b5589e1b6671c6b3057c68", + "size_bytes": 11003 + }, + { + "path": "FL2VA/tokenizer/vocab.json", + "sha256": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910", + "size_bytes": 2776833 + }, + { + "path": "FL2VA/transformer/config.json", + "sha256": "f619093a231fcfbcc3d035bec26c50ad864e7331a500d5c519f5045dc1e50458", + "size_bytes": 604 + }, + { + "path": "FL2VA/transformer/model-00001-of-00013.safetensors", + "sha256": "0b3386565e476bfdea287e9ea9f269d036e5c649ed14bb9b4afac1dc4661bd2a", + "size_bytes": 5227812968 + }, + { + "path": "FL2VA/transformer/model-00002-of-00013.safetensors", + "sha256": "9c98fd4579c9bc96d1b1bbf65c1243f9256df60bccd229570cecc61897773003", + "size_bytes": 5164578856 + }, + { + "path": "FL2VA/transformer/model-00003-of-00013.safetensors", + "sha256": "fa484c940d4170199fb69f5016739df2daf6ecd1150631b12a297e06e3964730", + "size_bytes": 5164578872 + }, + { + "path": "FL2VA/transformer/model-00004-of-00013.safetensors", + "sha256": "4a851df37cce2d14b39cb58df41def7b591b1f11b8520a32b421a5fa3e997c9f", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00005-of-00013.safetensors", + "sha256": "3fe6ff94dc9d3107c6776a25277d2b72b5443f88cf0f4ed835fd5b65a94ea5d4", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00006-of-00013.safetensors", + "sha256": "79b47e1b9ff03a2c6f06dd15a28972acf32c0867172ee34e5aa6e96b99494127", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00007-of-00013.safetensors", + "sha256": "6ac4d6ce639786b722a8dd99843ed56dcab48a0d174a4b0f9b8cdcbe5160fb0f", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00008-of-00013.safetensors", + "sha256": "03531812243fb0a333e01efa7e3d39750580bca8cbf2ba0529da419278eab736", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00009-of-00013.safetensors", + "sha256": "7e82a80b0d0d267026eb841b81523069cc7b956c1ebd8a18c07e462ffb262487", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00010-of-00013.safetensors", + "sha256": "ef0a1f6b65145232543dd2232d58d91a4d31f44b0da5084acf22cea123255481", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00011-of-00013.safetensors", + "sha256": "bf885b8f2f078c5499b75fded21094a26bfe0e6e00bee276e499443ecd2f298d", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00012-of-00013.safetensors", + "sha256": "dc717572d014ebf8527e35af815d10eb2f5a6bc87253b8a7d2f456965192db69", + "size_bytes": 5164578896 + }, + { + "path": "FL2VA/transformer/model-00013-of-00013.safetensors", + "sha256": "8bfd852d5817e9836de1d3ec8dbac1c5446b167568b717371f08282b22291aa2", + "size_bytes": 4242305176 + }, + { + "path": "FL2VA/transformer/model.safetensors.index.json", + "sha256": "fb457a26ffa6294660e249b0ddd03a337f2e5393f770b5c34c8b8f90a29a7efb", + "size_bytes": 38323 + }, + { + "path": "FL2VA/video_vae/attention.py", + "sha256": "c9db8465c57f0bfb40c0194227be6e34b2c1ff7c5b5d63abca9964eed6283767", + "size_bytes": 5785 + }, + { + "path": "FL2VA/video_vae/base_module.py", + "sha256": "0e7ddf5086179a306298693ac461ffcbc27ccb28dbc786b1a05060de7a3357c0", + "size_bytes": 9520 + }, + { + "path": "FL2VA/video_vae/config.json", + "sha256": "3edd2cdd1ebc823c868be55ef917e1b3b8a398fde4d3150dae44a3bf05d9f627", + "size_bytes": 1807 + }, + { + "path": "FL2VA/video_vae/conv.py", + "sha256": "b3adee35f27e5d372543242aa86ce5e0138f13b794b6c81f919001e3e2346723", + "size_bytes": 4487 + }, + { + "path": "FL2VA/video_vae/flash.py", + "sha256": "c1918463d303a16f278a670bb4f33d3f8d6f7653b0d473313ff5835699b86d04", + "size_bytes": 5763 + }, + { + "path": "FL2VA/video_vae/func.py", + "sha256": "0612c26a65f095699cc7acd51937cd153584fa5a0476956e46a4679aa575c22d", + "size_bytes": 5840 + }, + { + "path": "FL2VA/video_vae/klvae.py", + "sha256": "d05a22b4918ad303f147d6b0d0ae9038bd1af1f810a5553b04f3542ab9bcff96", + "size_bytes": 48594 + }, + { + "path": "FL2VA/video_vae/minimax_h3_video_vae.py", + "sha256": "77fdea69a48485b5434f65d08096657b5d78b78b7cdf24e1241038f237720138", + "size_bytes": 5123 + }, + { + "path": "FL2VA/video_vae/norm.py", + "sha256": "258b120ca8d9a16366d7b811fe93eebf98fe1564db0995e8b7747229c651919a", + "size_bytes": 10686 + }, + { + "path": "FL2VA/video_vae/normalize.py", + "sha256": "8db651acb9eb551c906a2850bbca8e9d851f00d07b30ccf66ceb6057da687a13", + "size_bytes": 1200 + }, + { + "path": "FL2VA/video_vae/parallel.py", + "sha256": "7193522beafd65bc3dbe40b9843e360ab8da3a0bb211e9bfb67bb0d5d0e66919", + "size_bytes": 12997 + }, + { + "path": "FL2VA/video_vae/source/config.json", + "sha256": "66c68f541e6578ce613ce7a0fc985eb59097038829e49f7535e6d08e6d95ab12", + "size_bytes": 1164 + }, + { + "path": "FL2VA/video_vae/source/model.safetensors", + "sha256": "5f0c2e161d895a9fee7645ca32d4a7e3a22b90cacfcbeba62ec999cdbbefe0d3", + "size_bytes": 10415548320 + }, + { + "path": "FL2VA/video_vae/utils.py", + "sha256": "309e45e9d9e33cd5516a10eac2d207eb9f940079a6d9bbe0ee80aaaf7eb42dcd", + "size_bytes": 764 + }, + { + "path": "FL2VA/video_vae/vae_cnn.py", + "sha256": "3949b701e6893bd2ea070cd5b18ed8135a7c21d884290df449b1997212d65b11", + "size_bytes": 8836 + }, + { + "path": "FL2VA/video_vae/vae_module.py", + "sha256": "9558fb302e10a619f198e12e04701c68988e897d0b627f958f33d27fd2a30d94", + "size_bytes": 1884 + }, + { + "path": "FL2VA/video_vae/vae_processor.py", + "sha256": "32870580a1802d9163104a4d68170e35a4772106867181e1bd343dff11f87d2e", + "size_bytes": 8369 + }, + { + "path": "FL2VA/video_vae/vae_vit.py", + "sha256": "a372c28441b56ec2dbc6041dbd6ce2766c64c0e61b2c69a16594a97a11da3f58", + "size_bytes": 13490 + }, + { + "path": "LICENSE", + "sha256": "59b99642b95ea21630e311198ddbfffbfe05aadba0c2f5d884cbdf4efcc90f44", + "size_bytes": 17604 + }, + { + "path": "README.md", + "sha256": "f0116a90332496bdfcc827320c603a26b849c73bf804f2674d03682fbbd2334a", + "size_bytes": 38406 + }, + { + "path": "model_index.json", + "sha256": "5a587fe13b2371427415ac892463142683aefcd8d322e274a3a095eac37ac7d2", + "size_bytes": 2936 + }, + { + "path": "modular_model_index.json", + "sha256": "a2b6a210e482ffb78e613b553f570c44e101afce6741bd4ed91429d0559af031", + "size_bytes": 2935 + } + ], + "path": "/mnt/hf_hub_cache/models--MiniMaxAI--MiniMax-H3/snapshots/42ed227ee7df40d41602854ae760620d6eb651fe", + "revision": "42ed227ee7df40d41602854ae760620d6eb651fe" + }, + "plan": { + "cases": [ + { + "case_id": "drum-taps", + "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 + } + ], + "generation": { + "aspect_ratio": "16:9", + "audio_channels": 2, + "audio_flow_shift": 3.0, + "audio_sample_rate_hz": 32000, + "duration_seconds": 4, + "flow_shift": 12.0, + "fps": 24, + "frame_count": 107, + "height": 768, + "num_inference_steps": 50, + "width": 1344 + }, + "model_id": "MiniMaxAI/MiniMax-H3", + "model_revision": "42ed227ee7df40d41602854ae760620d6eb651fe", + "plan_id": "h3-serving-2gpu-c124-v1", + "repetitions": 4, + "warmup_runs": 1 + }, + "policy": { + "calibration_status": "uncalibrated", + "max_audio_rms_ratio_error": 0.1, + "max_latency_increase_fraction": 0.1, + "max_memory_increase_fraction": 0.1, + "min_audio_spectral_cosine": 0.95, + "min_video_psnr_db": 30, + "policy_id": "h3-example-uncalibrated-gpu-v1" + }, + "port": 30283, + "schema_version": "0.1.0", + "server": { + "dit_cpu_offload": false, + "encoder_parallel": "auto", + "performance_mode": "speed", + "tp_size": 1, + "ulysses_degree": 2 + }, + "serving": { + "concurrency": 1, + "delivery_deadline_seconds": null, + "mode": "closed_loop" + } +} From ac9eef08d00f5d82ab0689546bb6c78b4cce6f56 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:33 -0700 Subject: [PATCH 06/12] feat: add a shared MiniMax-H3 workload spec with plan hashing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增 SGLang 与 vLLM-Omni 共用的 MiniMax-H3 workload spec 及 loader;生成的 plan 及其 plan_sha256 与 #2916 runner 一致。 --- experimental/video-generation/specs/README.md | 42 ++++ experimental/video-generation/specs/h3.yaml | 164 +++++++++++++ .../video-generation/specs/h3_spec.py | 231 ++++++++++++++++++ .../video-generation/tests/test_h3_spec.py | 209 ++++++++++++++++ 4 files changed, 646 insertions(+) create mode 100644 experimental/video-generation/specs/README.md create mode 100644 experimental/video-generation/specs/h3.yaml create mode 100644 experimental/video-generation/specs/h3_spec.py create mode 100644 experimental/video-generation/tests/test_h3_spec.py diff --git a/experimental/video-generation/specs/README.md b/experimental/video-generation/specs/README.md new file mode 100644 index 0000000000..4ae3dc4eec --- /dev/null +++ b/experimental/video-generation/specs/README.md @@ -0,0 +1,42 @@ +# H3 workload spec + +`h3.yaml` defines the MiniMax-H3 text-to-video-and-audio workload that SGLang and vLLM-Omni both run. +`h3_spec.py` loads and validates it, then builds the plan object that the runner from PR #2916 executes and hashes. + +## What the YAML records + +- `model` and every `generation` field hold a `value` and a `source` citation of the form `@::`. +- `sent.sglang` and `sent.vllm-omni` say whether that engine's request carries the field. + SGLang rejects caller-set fps and frame counts, runs at a fixed 24 fps, and derives the frame count and dimensions from its `target`. + Only vLLM-Omni receives fps, frame count, width, and height. +- `checked_on_clip` says whether the delivered clip is compared to the value. + The runner checks the clip's duration against `frame_count / fps` (107 / 24 = 4.458 s), so `duration_seconds` itself is not checked. +- `prompts` defines each prompt once with its motion and sound requirements. + Each entry in `sets` lists cases that pair a prompt with a seed, and its `seed_sources` cites every seed. +- `reference` records the SGLang reference run. It is informational, and no code reads or hashes it. + The run used workflow commit f7c7f58, whose spec, `mvp_runner.py`, and `mvp_media.py` match 0cb4c847d byte for byte. + +## Hash contract + +`canonical_json_bytes` is a byte-for-byte copy of the function in #2916's `evaluator/mvp_runner.py`. +It serializes with sorted keys, compact separators, UTF-8, and no NaN. + +- `plan_sha256(plan_for(spec, set_id))` equals the `plan_sha256` that #2916's runner records for the same plan. + The `frozen-c124` set reproduces #2916's `h3-serving-2gpu-c124-v1` plan, whose hash is `8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68`. +- `spec_sha256(spec)` covers the model, the generation values, and each set's resolved cases and counts. +- Citations, `sent`, `checked_on_clip`, and `reference` are outside both hashes, as are YAML key order, comments, and formatting. + Neither hash includes GPU UUIDs, job IDs, ports, or hosts. +- `plan_for(spec, set_id, repetitions=..., warmup_runs=...)` overrides the counts. + If either count differs from the set's own, the plan ID gains a `-r-w` suffix, so a shortened run never shares an ID or a hash with the full one. + +## Run the tests and print the hashes + +From the repository root: + +```bash +uv run --no-project --python 3.12 --with 'pyyaml>=6,<7' --with 'pytest>=8,<9' python -m pytest experimental/video-generation/tests/test_h3_spec.py -q +uv run --no-project --python 3.12 --with 'pyyaml>=6,<7' python experimental/video-generation/specs/h3_spec.py +``` + +The second command prints `spec_sha256` and each set's `plan_id` and `plan_sha256` as JSON. +To hash another spec file, pass its path as the argument. diff --git a/experimental/video-generation/specs/h3.yaml b/experimental/video-generation/specs/h3.yaml new file mode 100644 index 0000000000..f8283295ba --- /dev/null +++ b/experimental/video-generation/specs/h3.yaml @@ -0,0 +1,164 @@ +# `sent` follows the payload builders at +# feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/evaluator/mvp_runner.py:186-209 +# `checked_on_clip` follows the clip checks at +# feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/evaluator/mvp_media.py:384-425 +# against the expected media built at mvp_runner.py:613-615. + +schema_version: 1 + +model: + id: + value: MiniMaxAI/MiniMax-H3 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:492 + revision: + value: 42ed227ee7df40d41602854ae760620d6eb651fe + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:493 + +generation: + aspect_ratio: + value: "16:9" + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:480 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + audio_channels: + value: 2 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:481 + sent: {sglang: false, vllm-omni: false} + checked_on_clip: true + audio_flow_shift: + value: 3.0 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:482 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + audio_sample_rate_hz: + value: 32000 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:483 + sent: {sglang: false, vllm-omni: false} + checked_on_clip: true + duration_seconds: + value: 4 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:484 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + flow_shift: + value: 12.0 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:485 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + fps: + value: 24 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:486 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + frame_count: + # SGLang rejects caller-set fps and num_frames, runs at a fixed 24 fps, and + # snaps duration_seconds * fps = 96 up to its 17n+5 frame boundary, so the + # clip lasts 107 / 24 = 4.458 s rather than duration_seconds + # (sgl-project/sglang@71de97b26:python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/time_request.py:5). + value: 107 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:487 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + height: + value: 768 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:488 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + num_inference_steps: + value: 50 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:489 + sent: {sglang: true, vllm-omni: true} + checked_on_clip: false + width: + value: 1344 + source: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:490 + sent: {sglang: false, vllm-omni: true} + checked_on_clip: true + +prompts: + drum-taps: + text: >- + 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 + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:21 + pouring-water: + text: >- + A single continuous close-up of a hand steadily pouring water from a + clear glass jug into an empty drinking glass on a wooden table. The water + level visibly rises and the pouring and splashing sounds are clearly + audible. Daylight, a still camera, no speech, no music, no subtitles, and + no scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:28 + bicycle-pass: + text: >- + A single continuous wide shot of a cyclist riding from the left side of + the frame to the right along a quiet park path. The moving wheels remain + visible and the bicycle bell rings once as the cyclist passes the camera. + Soft outdoor ambience is audible. The camera stays still, with no music, + speech, subtitles, or scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:35 + footsteps: + text: >- + A single continuous low-angle shot of a person in ordinary shoes walking + across a wooden floor from left to right. Each step visibly lands on the + floor and the corresponding footsteps are audible in the quiet room. The + camera stays still. No music, speech, subtitles, or scene cuts. + requires_motion: true + requires_sound: true + source: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:42 + +sets: + frozen-c124: + plan_id: h3-serving-2gpu-c124-v1 + repetitions: 4 + warmup_runs: 1 + seed_sources: + 11: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:476 + cases: + - {case_id: drum-taps, prompt: drum-taps, seed: 11} + engine-compare-v1: + plan_id: h3-engine-compare-v1 + repetitions: 1 + warmup_runs: 1 + seed_sources: + 11: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:8 + 29: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:22 + cases: + - {case_id: drum-taps-s11, prompt: drum-taps, seed: 11} + - {case_id: drum-taps-s29, prompt: drum-taps, seed: 29} + - {case_id: pouring-water-s11, prompt: pouring-water, seed: 11} + - {case_id: pouring-water-s29, prompt: pouring-water, seed: 29} + - {case_id: bicycle-pass-s11, prompt: bicycle-pass, seed: 11} + - {case_id: bicycle-pass-s29, prompt: bicycle-pass, seed: 29} + - {case_id: footsteps-s11, prompt: footsteps, seed: 11} + - {case_id: footsteps-s29, prompt: footsteps, seed: 29} + +# Informational only; not hashed. +reference: + sglang: + pull_request: https://github.com/SemiAnalysisAI/InferenceX/pull/2916 + run: https://github.com/SemiAnalysisAI/InferenceX/actions/runs/39002901777 + hardware: H200 + workflow_sha: f7c7f581088468e0c4074836603d2f90fcd5986b + plan_id: h3-serving-2gpu-c124-v1 + recorded_plan_sha256: 8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68 + outcome: >- + Concurrency cells 1, 2 and 4 each completed 5 clips (1 warmup, 4 + measured); all 15 decoded as valid 1344x768, 107 frames, 24 fps, 32000 Hz + stereo. + sglang_revision: 71de97b264b04dcd514cf904003028aefe9775c8 + server: {ulysses_degree: 2, tp_size: 1, port: 30283} + sources: + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:13 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:507 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:513 + - feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/specs/h3-serving-2gpu.json:514 diff --git a/experimental/video-generation/specs/h3_spec.py b/experimental/video-generation/specs/h3_spec.py new file mode 100644 index 0000000000..370cb48324 --- /dev/null +++ b/experimental/video-generation/specs/h3_spec.py @@ -0,0 +1,231 @@ +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +import yaml + +ENGINES = ("sglang", "vllm-omni") +DEFAULT_SPEC = Path(__file__).with_name("h3.yaml") +_CITATION = re.compile(r"[\w./-]+@[0-9a-f]{7,40}:[\w./-]+:\d+") +_REVISION = re.compile(r"[0-9a-f]{40}") +_MAX_SEED = 2**63 + + +def canonical_json_bytes(value: Any) -> bytes: + """Stable hash representation, UTF-8, sorted keys, no trailing newline.""" + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False + ).encode("utf-8") + + +def _sha256(value: Any) -> str: + return hashlib.sha256(canonical_json_bytes(value)).hexdigest() + + +def _fields(value: Any, where: str, keys: set[str]) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != keys: + raise ValueError(f"{where} must be a mapping with exactly the keys {sorted(keys)}") + return value + + +def _text(value: Any, where: str) -> None: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{where} must be a nonempty string") + + +def _flag(value: Any, where: str) -> None: + if not isinstance(value, bool): + raise ValueError(f"{where} must be true or false") + + +def _integer(value: Any, where: str, *, minimum: int, below: int | None = None) -> None: + if ( + isinstance(value, bool) + or not isinstance(value, int) + or value < minimum + or (below is not None and value >= below) + ): + bound = f"in [{minimum}, {below})" if below is not None else f">= {minimum}" + raise ValueError(f"{where} must be an integer {bound}") + + +def _citation(value: Any, where: str) -> None: + if not isinstance(value, str) or not _CITATION.fullmatch(value): + raise ValueError(f"{where} must cite @::") + + +def _validate_set(set_id: str, prompt_set: Any, prompts: dict[str, Any]) -> None: + where = f"sets.{set_id}" + _fields(prompt_set, where, {"plan_id", "repetitions", "warmup_runs", "seed_sources", "cases"}) + _text(prompt_set["plan_id"], f"{where}.plan_id") + _integer(prompt_set["repetitions"], f"{where}.repetitions", minimum=1) + _integer(prompt_set["warmup_runs"], f"{where}.warmup_runs", minimum=0) + cases = prompt_set["cases"] + if not isinstance(cases, list) or not cases: + raise ValueError(f"{where}.cases must be a nonempty list") + case_ids: set[str] = set() + for index, case in enumerate(cases): + case_where = f"{where}.cases[{index}]" + _fields(case, case_where, {"case_id", "prompt", "seed"}) + _text(case["case_id"], f"{case_where}.case_id") + if case["case_id"] in case_ids: + raise ValueError(f"{case_where}.case_id {case['case_id']!r} is not unique") + case_ids.add(case["case_id"]) + if case["prompt"] not in prompts: + raise ValueError(f"{case_where}.prompt {case['prompt']!r} is not a defined prompt") + _integer(case["seed"], f"{case_where}.seed", minimum=0, below=_MAX_SEED) + seed_sources = prompt_set["seed_sources"] + seeds = {case["seed"] for case in cases} + if not isinstance(seed_sources, dict) or set(seed_sources) != seeds: + raise ValueError(f"{where}.seed_sources must cite exactly the seeds {sorted(seeds)}") + for seed, source in seed_sources.items(): + _citation(source, f"{where}.seed_sources[{seed}]") + + +def load_spec(path: str | Path) -> dict[str, Any]: + """Parse and validate a spec file; raise ValueError naming the first bad field.""" + spec = yaml.safe_load(Path(path).read_text(encoding="utf-8")) + _fields(spec, "spec", {"schema_version", "model", "generation", "prompts", "sets", "reference"}) + if isinstance(spec["schema_version"], bool) or spec["schema_version"] != 1: + raise ValueError("schema_version must be 1") + model = _fields(spec["model"], "model", {"id", "revision"}) + for name, entry in model.items(): + _fields(entry, f"model.{name}", {"value", "source"}) + _text(entry["value"], f"model.{name}.value") + _citation(entry["source"], f"model.{name}.source") + if not _REVISION.fullmatch(model["revision"]["value"]): + raise ValueError("model.revision.value must be a 40-character commit") + + generation = spec["generation"] + if not isinstance(generation, dict) or not generation: + raise ValueError("generation must be a nonempty mapping") + for name, field in generation.items(): + where = f"generation.{name}" + _fields(field, where, {"value", "source", "sent", "checked_on_clip"}) + if isinstance(field["value"], bool) or not isinstance(field["value"], int | float | str): + raise ValueError(f"{where}.value must be a number or string") + _citation(field["source"], f"{where}.source") + sent = _fields(field["sent"], f"{where}.sent", set(ENGINES)) + for engine, carried in sent.items(): + _flag(carried, f"{where}.sent.{engine}") + _flag(field["checked_on_clip"], f"{where}.checked_on_clip") + + prompts = spec["prompts"] + if not isinstance(prompts, dict) or not prompts: + raise ValueError("prompts must be a nonempty mapping") + for name, prompt in prompts.items(): + where = f"prompts.{name}" + _fields(prompt, where, {"text", "requires_motion", "requires_sound", "source"}) + _text(prompt["text"], f"{where}.text") + _flag(prompt["requires_motion"], f"{where}.requires_motion") + _flag(prompt["requires_sound"], f"{where}.requires_sound") + _citation(prompt["source"], f"{where}.source") + + sets = spec["sets"] + if not isinstance(sets, dict) or not sets: + raise ValueError("sets must be a nonempty mapping") + for set_id, prompt_set in sets.items(): + _validate_set(set_id, prompt_set, prompts) + + if not isinstance(spec["reference"], dict): + raise ValueError("reference must be a mapping") + return spec + + +def generation_values(spec: dict[str, Any]) -> dict[str, Any]: + return {name: field["value"] for name, field in spec["generation"].items()} + + +def _set_plan_fields(spec: dict[str, Any], set_id: str) -> dict[str, Any]: + prompt_set = spec["sets"][set_id] + cases = [] + for case in prompt_set["cases"]: + prompt = spec["prompts"][case["prompt"]] + cases.append( + { + "case_id": case["case_id"], + "prompt": prompt["text"], + "seed": case["seed"], + "requires_motion": prompt["requires_motion"], + "requires_sound": prompt["requires_sound"], + } + ) + return { + "plan_id": prompt_set["plan_id"], + "cases": cases, + "repetitions": prompt_set["repetitions"], + "warmup_runs": prompt_set["warmup_runs"], + } + + +def plan_for( + spec: dict[str, Any], + set_id: str, + *, + repetitions: int | None = None, + warmup_runs: int | None = None, +) -> dict[str, Any]: + """Build the #2916 plan object for one prompt set. + + Count overrides that differ from the set's own counts rename the plan + (``-r-w``), so a shortened run never + shares an identity with the full one. + """ + plan = { + "model_id": spec["model"]["id"]["value"], + "model_revision": spec["model"]["revision"]["value"], + "generation": generation_values(spec), + **_set_plan_fields(spec, set_id), + } + if repetitions is not None: + _integer(repetitions, "repetitions", minimum=1) + plan["repetitions"] = repetitions + if warmup_runs is not None: + _integer(warmup_runs, "warmup_runs", minimum=0) + plan["warmup_runs"] = warmup_runs + prompt_set = spec["sets"][set_id] + if (plan["repetitions"], plan["warmup_runs"]) != ( + prompt_set["repetitions"], + prompt_set["warmup_runs"], + ): + plan["plan_id"] += f"-r{plan['repetitions']}-w{plan['warmup_runs']}" + return plan + + +def plan_sha256(plan: dict[str, Any]) -> str: + """Equal to the ``plan_sha256`` that PR #2916's runner records for the same plan.""" + return _sha256(plan) + + +def spec_sha256(spec: dict[str, Any]) -> str: + """Hash the model, the generation values, and each set's plan fields; nothing else counts.""" + return _sha256( + { + "model": { + "id": spec["model"]["id"]["value"], + "revision": spec["model"]["revision"]["value"], + }, + "generation": generation_values(spec), + "sets": {set_id: _set_plan_fields(spec, set_id) for set_id in spec["sets"]}, + } + ) + + +def main(argv: list[str]) -> None: + spec = load_spec(argv[1] if len(argv) > 1 else DEFAULT_SPEC) + plans = {set_id: plan_for(spec, set_id) for set_id in spec["sets"]} + report = { + "spec_sha256": spec_sha256(spec), + "sets": { + set_id: {"plan_id": plan["plan_id"], "plan_sha256": plan_sha256(plan)} + for set_id, plan in plans.items() + }, + } + print(json.dumps(report, indent=2)) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/experimental/video-generation/tests/test_h3_spec.py b/experimental/video-generation/tests/test_h3_spec.py new file mode 100644 index 0000000000..fb05888d35 --- /dev/null +++ b/experimental/video-generation/tests/test_h3_spec.py @@ -0,0 +1,209 @@ +import copy +import functools +import json +import operator +import re +import sys +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest +import yaml + +SPECS = Path(__file__).resolve().parents[1] / "specs" +sys.path.insert(0, str(SPECS)) +import h3_spec # noqa: E402 + +SPEC_PATH = SPECS / "h3.yaml" +FIXTURE_PATH = Path(__file__).with_name("fixtures") / "h3-serving-2gpu.json" +RUN_39002901777_RECORDED_PLAN_SHA256 = ( + "8d51a472caa7db0f5d4c33adec1e9c9f852e7144cf25356085802edaf7d80d68" +) +FROZEN = "frozen-c124" +CITATION = "feat/example@0123456789abcdef:specs/source.json:1" + + +@pytest.fixture(scope="module") +def spec() -> dict[str, Any]: + return h3_spec.load_spec(SPEC_PATH) + + +@pytest.fixture(scope="module") +def reference_plan() -> dict[str, Any]: + return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))["plan"] + + +def _assign(tree: Any, path: tuple[Any, ...], value: Any) -> None: + *parents, last = path + functools.reduce(operator.getitem, parents, tree)[last] = value + + +def _leaves(value: Any, path: tuple[Any, ...] = ()) -> Iterator[tuple[tuple[Any, ...], Any]]: + if isinstance(value, dict): + for key, item in value.items(): + yield from _leaves(item, (*path, key)) + elif isinstance(value, list): + for index, item in enumerate(value): + yield from _leaves(item, (*path, index)) + else: + yield path, value + + +def _spec_path(spec: dict[str, Any], plan_path: tuple[Any, ...]) -> tuple[Any, ...]: + match plan_path: + case ("model_id",): + return ("model", "id", "value") + case ("model_revision",): + return ("model", "revision", "value") + case ("generation", name): + return ("generation", name, "value") + case ("cases", index, "prompt" | "requires_motion" | "requires_sound" as key): + prompt = spec["sets"][FROZEN]["cases"][index]["prompt"] + return ("prompts", prompt, "text" if key == "prompt" else key) + case ("cases", index, key): + return ("sets", FROZEN, "cases", index, key) + case (key,): + return ("sets", FROZEN, key) + raise AssertionError(plan_path) + + +def _mutated(value: Any) -> Any: + if isinstance(value, bool): + return not value + if isinstance(value, int | float): + return value + 1 + return f"{value}x" + + +def _reverse_keys(value: Any) -> Any: + if isinstance(value, dict): + return {key: _reverse_keys(value[key]) for key in reversed(value)} + if isinstance(value, list): + return [_reverse_keys(item) for item in value] + return value + + +def test_frozen_set_reproduces_the_2916_plan_and_its_recorded_hash( + spec: dict[str, Any], reference_plan: dict[str, Any] +) -> None: + plan = h3_spec.plan_for(spec, FROZEN) + assert plan == reference_plan + # Dict equality treats 3 and 3.0 alike; the hashes do not. + assert h3_spec.plan_sha256(plan) == RUN_39002901777_RECORDED_PLAN_SHA256 + assert h3_spec.plan_sha256(reference_plan) == RUN_39002901777_RECORDED_PLAN_SHA256 + + +def test_hashes_ignore_key_order_formatting_and_comments( + spec: dict[str, Any], tmp_path: Path +) -> None: + raw = yaml.safe_load(SPEC_PATH.read_text(encoding="utf-8")) + body = yaml.safe_dump(_reverse_keys(raw), sort_keys=False, indent=4, width=50) + rewritten = tmp_path / "h3.yaml" + rewritten.write_text(f"# rewritten copy\n{body}# end\n\n", encoding="utf-8") + reloaded = h3_spec.load_spec(rewritten) + for set_id in spec["sets"]: + assert h3_spec.plan_sha256(h3_spec.plan_for(reloaded, set_id)) == h3_spec.plan_sha256( + h3_spec.plan_for(spec, set_id) + ) + assert h3_spec.spec_sha256(reloaded) == h3_spec.spec_sha256(spec) + + +def test_each_spec_value_changes_only_its_plan_field_and_the_hash( + spec: dict[str, Any], reference_plan: dict[str, Any] +) -> None: + base = h3_spec.plan_for(spec, FROZEN) + base_leaves = dict(_leaves(base)) + owners = {h3_spec.plan_sha256(base): ("unmutated",)} + for plan_path, value in _leaves(reference_plan): + mutated = copy.deepcopy(spec) + _assign(mutated, _spec_path(mutated, plan_path), _mutated(value)) + plan = h3_spec.plan_for(mutated, FROZEN) + assert [path for path, item in _leaves(plan) if base_leaves[path] != item] == [plan_path] + digest = h3_spec.plan_sha256(plan) + assert digest not in owners, f"{plan_path} hashes like {owners[digest]}" + owners[digest] = plan_path + + +def test_count_overrides_rename_the_plan_and_change_its_hash(spec: dict[str, Any]) -> None: + full = h3_spec.plan_for(spec, FROZEN) + single = h3_spec.plan_for(spec, FROZEN, repetitions=1, warmup_runs=0) + assert single == { + **full, + "plan_id": f"{full['plan_id']}-r1-w0", + "repetitions": 1, + "warmup_runs": 0, + } + assert h3_spec.plan_sha256(single) != h3_spec.plan_sha256(full) + same_counts = h3_spec.plan_for( + spec, FROZEN, repetitions=full["repetitions"], warmup_runs=full["warmup_runs"] + ) + assert same_counts == full + with pytest.raises(ValueError, match=re.escape("repetitions must be an integer >= 1")): + h3_spec.plan_for(spec, FROZEN, repetitions=0) + + +def _minimal_spec() -> dict[str, Any]: + return { + "schema_version": 1, + "model": { + "id": {"value": "org/model", "source": CITATION}, + "revision": {"value": "a" * 40, "source": CITATION}, + }, + "generation": { + "fps": { + "value": 24, + "source": CITATION, + "sent": {"sglang": False, "vllm-omni": True}, + "checked_on_clip": True, + }, + }, + "prompts": { + "p": { + "text": "a prompt", + "requires_motion": True, + "requires_sound": False, + "source": CITATION, + }, + }, + "sets": { + "s": { + "plan_id": "plan", + "repetitions": 1, + "warmup_runs": 0, + "seed_sources": {7: CITATION}, + "cases": [{"case_id": "c", "prompt": "p", "seed": 7}], + }, + }, + "reference": {}, + } + + +@pytest.mark.parametrize( + ("path", "value", "message"), + [ + (("generation", "fps", "sent"), {"sglang": False}, "generation.fps.sent must be a mapping"), + (("generation", "fps", "source"), "mvp_runner.py:186", "generation.fps.source must cite"), + (("generation", "fps", "value"), True, "generation.fps.value must be a number or string"), + (("sets", "s", "cases", 0, "seed"), 2**63, "sets.s.cases[0].seed must be an integer"), + (("sets", "s", "cases", 0, "seed"), True, "sets.s.cases[0].seed must be an integer"), + (("sets", "s", "cases", 0, "prompt"), "q", "sets.s.cases[0].prompt 'q' is not a defined"), + ( + ("sets", "s", "cases"), + [{"case_id": "c", "prompt": "p", "seed": n} for n in (7, 7)], + "is not unique", + ), + (("sets", "s", "seed_sources"), {}, "sets.s.seed_sources must cite exactly the seeds [7]"), + (("sets",), {}, "sets must be a nonempty mapping"), + (("schema_version",), True, "schema_version must be 1"), + ], +) +def test_load_spec_rejects_malformed_specs( + tmp_path: Path, path: tuple[Any, ...], value: Any, message: str +) -> None: + spec = _minimal_spec() + _assign(spec, path, value) + target = tmp_path / "spec.yaml" + target.write_text(yaml.safe_dump(spec), encoding="utf-8") + with pytest.raises(ValueError, match=re.escape(message)): + h3_spec.load_spec(target) From dfcf6db63b5a5eed5aa552e9607dd913261f454b Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:51:35 -0700 Subject: [PATCH 07/12] ci: test and lint the H3 workload spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:新增 CI,对 H3 workload spec 运行测试、lint 和格式检查。 --- .github/workflows/test-h3-spec.yml | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/test-h3-spec.yml diff --git a/.github/workflows/test-h3-spec.yml b/.github/workflows/test-h3-spec.yml new file mode 100644 index 0000000000..a23203f3f7 --- /dev/null +++ b/.github/workflows/test-h3-spec.yml @@ -0,0 +1,43 @@ +name: Test H3 Spec + +on: + pull_request: + paths: &h3-spec-paths + - 'experimental/video-generation/specs/**' + - 'experimental/video-generation/tests/**' + - '.github/workflows/test-h3-spec.yml' + push: + paths: *h3-spec-paths + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: H3 spec loader and hashes + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - name: Test spec loader and hashes + run: >- + uv run --no-project --python 3.12 + --with 'pyyaml>=6,<7' --with 'pytest>=8,<9' + python -m pytest experimental/video-generation/tests/test_h3_spec.py -q + - name: Lint and format + # The infx config never lints tests/, so assert (S101) is allowed there. + # These standalone modules have no package, so INP001 does not apply. + run: | + uvx ruff@0.14.0 check --config inferencex-e2e/infx/ruff.toml --ignore INP001 \ + experimental/video-generation/specs/h3_spec.py + uvx ruff@0.14.0 check --config inferencex-e2e/infx/ruff.toml --ignore INP001,S101 \ + experimental/video-generation/tests/test_h3_spec.py + uvx ruff@0.14.0 format --check --config inferencex-e2e/infx/ruff.toml \ + experimental/video-generation/specs/h3_spec.py experimental/video-generation/tests/test_h3_spec.py From 4227658d5dc15f9a9fb73a9d6e225120baa0cedc Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Fri, 16 Oct 2026 23:57:22 -0700 Subject: [PATCH 08/12] docs: add vLLM-Omni serving notes for MiniMax-H3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:记录在 2x H100 上服务 MiniMax-H3 所用的 vLLM-Omni 镜像 pin、server 命令、并行配置与 attention backend,供 Phase B 使用。 --- experimental/video-generation/specs/README.md | 1 + .../specs/h3-vllm-omni-serving.md | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 experimental/video-generation/specs/h3-vllm-omni-serving.md diff --git a/experimental/video-generation/specs/README.md b/experimental/video-generation/specs/README.md index 4ae3dc4eec..d6f4be5fda 100644 --- a/experimental/video-generation/specs/README.md +++ b/experimental/video-generation/specs/README.md @@ -2,6 +2,7 @@ `h3.yaml` defines the MiniMax-H3 text-to-video-and-audio workload that SGLang and vLLM-Omni both run. `h3_spec.py` loads and validates it, then builds the plan object that the runner from PR #2916 executes and hashes. +`h3-vllm-omni-serving.md` records the pinned vLLM-Omni image, server command, parallelism and attention backend that served this workload on 2x H100. ## What the YAML records diff --git a/experimental/video-generation/specs/h3-vllm-omni-serving.md b/experimental/video-generation/specs/h3-vllm-omni-serving.md new file mode 100644 index 0000000000..b6bb4381d6 --- /dev/null +++ b/experimental/video-generation/specs/h3-vllm-omni-serving.md @@ -0,0 +1,49 @@ +# Serving MiniMax-H3 on vLLM-Omni + +What the Phase A feasibility run used on 2x H100 80GB on 2026-10-17. Phase B benchmarking starts from this and records any change. + +## Pins + +| Component | Value | +| --- | --- | +| vLLM-Omni | release v0.30.0, commit `a8576ccb725c4e21cd13c3eb5f9a546b21149d2b` | +| Image | `vllm/vllm-omni:v0.30.0`, manifest digest `sha256:fc77cfaac7b1b43c30cbf32eaea214864434c6e0f303e6e9eb9f64104241a994`, linux/amd64 `sha256:b09c9eab7d2e30817208de92ccb482756d13d4339377d7cc4c4ce9f47bf04174` | +| In-image versions | vllm 0.30.0, vllm-omni 0.30.0, torch 2.13.0+cu130 | +| Model | `MiniMaxAI/MiniMax-H3` revision `42ed227ee7df40d41602854ae760620d6eb651fe` (same as `specs/h3.yaml` and PR #2916) | + +Only the `FL2VA/` partition plus root `model_index.json`, `LICENSE`, `README.md` and `Ref2VA/model_index.json` need to be on disk (129 GB); `--task-type fl2va` loads nothing else. Leave the root `modular_model_index.json` out of a local copy: with it present, vllm-omni misdetects the directory as FastH3 (upstream issue [#8796](https://github.com/vllm-project/vllm-omni/issues/8796)). + +## Server command + +``` +vllm serve --served-model-name MiniMaxAI/MiniMax-H3 --omni --host 0.0.0.0 --port 30283 \ + --trust-remote-code --task-type fl2va --num-gpus 2 --tensor-parallel-size 2 --usp 1 --ring 1 \ + --text-encoder-tp-size 2 --vae-patch-parallel-size 2 --vae-parallel-mode tile --vae-use-tiling \ + --enforce-eager +``` + +`` is the Hugging Face cache snapshot directory for the revision above. Serving the Hub ID with `--revision` while `HF_HUB_OFFLINE=1` fails at startup with `huggingface_hub.errors.IncompleteSnapshotError` because `snapshot_download` requires every file of the repository and resolves `refs/main`; the directory path avoids that. `--served-model-name` keeps the model name the evaluator sends. + +Environment: + +``` +HF_HUB_OFFLINE=1 +TRANSFORMERS_OFFLINE=1 +HF_HUB_CACHE= +VLLM_WORKER_MULTIPROC_METHOD=spawn +VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 +``` + +## Parallelism + +Tensor parallel 2 across both GPUs with Ulysses and ring degree 1, text encoder TP 2, VAE patch-parallel 2 in tile mode with tiling. This is the upstream recipe's only two-GPU resident layout; pure Ulysses replicates the whole DiT on every rank, so SGLang's `ulysses_degree 2 / tp_size 1` layout from #2916 does not fit 80 GB here. Phase A ran eager; Phase B should also try without `--enforce-eager` and report compile time separately. + +## Attention backend + +Resolved by the server at startup, no flag passed: `FLASH_ATTN` for `role='minimax_h3.token_refiner'` and for `role='self'` (log line from `selector.py:84/93`). + +## Observed on 2x H100 (informational, not a benchmark) + +- Model load: 61.3 GiB and 282 s per TP worker; `/health` 200 about 5 min 20 s after launch. +- One request from the `frozen-c124` set with `repetitions=1, warmup_runs=0`: 50 denoise steps at 2.35 s/it, 125.4 s submit to validated clip, both GPUs at the 700 W cap with 70.2 GB used. +- The request carries width, height, fps and num_frames plus `extra_params` (`task`, `target`, `audio_flow_shift`); the server logged `Applied extra_params` and produced 1344x768, 107 frames, 24 fps, 32 kHz stereo, matching the spec's clip checks. From c77faf10e006e32655c58a8fb7c83f2922a1e16d Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 18 Oct 2026 01:26:31 -0700 Subject: [PATCH 09/12] feat: bind H3 engine comparison to native video sweep --- .../video-generation/evaluator/cli.py | 50 +++++++ .../evaluator/mvp_srt_selected.py | 132 ++++++++++++++++++ experimental/video-generation/pyproject.toml | 2 +- experimental/video-generation/specs/h3.yaml | 33 +++++ .../tests/test_srt_selected.py | 51 +++++++ inferencex-e2e/benchmarks/h3/run_client.sh | 9 ++ inferencex-e2e/benchmarks/h3/run_server.sh | 8 ++ .../h3/sglang/h200-bf16/serving-client.yaml | 1 + .../vllm-omni/h200-bf16/serving-client.yaml | 37 +++++ inferencex-e2e/configs/nvidia-master.yaml | 23 ++- inferencex-e2e/infx/srt_slurm/generate.py | 5 +- inferencex-e2e/infx/srt_slurm/video.py | 64 +++++++-- .../tests/srt_slurm/test_video_selected.py | 70 ++++++++++ 13 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 experimental/video-generation/evaluator/mvp_srt_selected.py create mode 100644 experimental/video-generation/tests/test_srt_selected.py create mode 100644 inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/vllm-omni/h200-bf16/serving-client.yaml create mode 100644 inferencex-e2e/infx/tests/srt_slurm/test_video_selected.py diff --git a/experimental/video-generation/evaluator/cli.py b/experimental/video-generation/evaluator/cli.py index e4c23d80b6..ba619565ee 100644 --- a/experimental/video-generation/evaluator/cli.py +++ b/experimental/video-generation/evaluator/cli.py @@ -100,6 +100,33 @@ def _mvp_command(arguments: argparse.Namespace) -> int: host=arguments.host, record=arguments.record, ) + if arguments.command == "srt-yaml-serve": + from .mvp_srt_selected import selected_plan, serve + + plan = selected_plan(arguments.spec, arguments.set, arguments.plan_sha256) + serve( + plan, runtime=arguments.runtime, runtime_revision=arguments.runtime_revision, + model_path=arguments.model_path, port=arguments.port, + gpus=arguments.gpus, output=arguments.record, + ) + return 0 + + if arguments.command == "srt-yaml-client": + from .mvp_srt_selected import run_matrix, selected_plan + + plan = selected_plan(arguments.spec, arguments.set, arguments.plan_sha256) + runs = run_matrix( + plan, runtime=arguments.runtime, runtime_revision=arguments.runtime_revision, + endpoint=arguments.endpoint, output=arguments.output, + concurrencies=arguments.concurrencies, gpus=arguments.gpus, + window_dir=arguments.window_dir, + ) + for concurrency, run in runs.items(): + print(json.dumps({"concurrency": concurrency, "summary": run.get("summary"), + "status": run.get("status")}, indent=2, sort_keys=True)) + return 0 if all(run["summary"]["valid"] == run["summary"]["scheduled"] + and run["summary"]["scheduled"] for run in runs.values()) else 1 + if arguments.command == "srt-client": from .mvp_srt import run_srt_client, run_srt_matrix @@ -264,6 +291,29 @@ def _parser() -> argparse.ArgumentParser: srt_client.add_argument("--window-dir", type=Path, help="srt-slurm measurement-window directory (SRT_MEASUREMENT_WINDOW_DIR)") + selected_server = subparsers.add_parser("srt-yaml-serve", help="serve a selected YAML video workload") + selected_server.add_argument("--spec", required=True, type=Path) + selected_server.add_argument("--set", required=True) + selected_server.add_argument("--plan-sha256", required=True) + selected_server.add_argument("--runtime", required=True, choices=("sglang", "vllm-omni")) + selected_server.add_argument("--runtime-revision", required=True) + selected_server.add_argument("--model-path", required=True, type=Path) + selected_server.add_argument("--port", required=True, type=int) + selected_server.add_argument("--gpus", required=True, type=int) + selected_server.add_argument("--record", required=True, type=Path) + + selected_client = subparsers.add_parser("srt-yaml-client", help="measure a selected YAML video workload") + selected_client.add_argument("--spec", required=True, type=Path) + selected_client.add_argument("--set", required=True) + selected_client.add_argument("--plan-sha256", required=True) + selected_client.add_argument("--runtime", required=True, choices=("sglang", "vllm-omni")) + selected_client.add_argument("--runtime-revision", required=True) + selected_client.add_argument("--endpoint", required=True) + selected_client.add_argument("--output", required=True, type=Path) + selected_client.add_argument("--concurrencies", required=True, nargs="+", type=int) + selected_client.add_argument("--gpus", required=True, type=int) + selected_client.add_argument("--window-dir", required=True, type=Path) + srt_finalize = subparsers.add_parser( "srt-finalize", help="assemble the app bundle from an srt-slurm H3 job and its power validation", diff --git a/experimental/video-generation/evaluator/mvp_srt_selected.py b/experimental/video-generation/evaluator/mvp_srt_selected.py new file mode 100644 index 0000000000..2a1dc4b131 --- /dev/null +++ b/experimental/video-generation/evaluator/mvp_srt_selected.py @@ -0,0 +1,132 @@ +"""Run a selected YAML video workload through the native srt-slurm service.""" + +from __future__ import annotations + +import os +import re +import time +from pathlib import Path + +from specs import h3_spec + +from .mvp_runner import run_plan, validate_plan +from .mvp_srt import BENCHMARK_STEM, CELL_DIR, _write, benchmark_window + + +def selected_plan(path: Path, set_id: str, expected_sha256: str) -> dict: + spec = h3_spec.load_spec(path) + if set_id not in spec["sets"]: + raise ValueError(f"unknown selected workload set: {set_id}") + plan = validate_plan(h3_spec.plan_for(spec, set_id)) + if h3_spec.plan_sha256(plan) != expected_sha256: + raise ValueError("selected workload plan does not match the bound plan_sha256") + return plan + + +def h3_vllm_model_view(snapshot: Path, output: Path) -> Path: + """Expose only the tested FL2VA files without copying weights or mutating the cache.""" + required = ("FL2VA", "model_index.json", "LICENSE", "README.md", "Ref2VA/model_index.json") + for name in required: + if not (snapshot / name).exists(): + raise ValueError(f"H3 model snapshot lacks {name}") + output.mkdir(parents=True, exist_ok=False) + for name in required: + target = output / name + target.parent.mkdir(parents=True, exist_ok=True) + target.symlink_to(snapshot / name) + return output + + +def server_argv(plan: dict, runtime: str, model_path: Path, port: int, gpus: int) -> list[str]: + if plan["model_id"] != "MiniMaxAI/MiniMax-H3" or gpus != 2: + raise ValueError("this H3 native profile requires two GPUs") + if runtime == "sglang": + from .mvp_gpu_job import _server_argv + + spec = { + "baseline": {"python": "/usr/bin/python3"}, + "model": {"path": str(model_path), "revision": plan["model_revision"]}, + "plan": plan, + "server": {"ulysses_degree": 2, "tp_size": 1, "encoder_parallel": "auto", + "performance_mode": "speed", "dit_cpu_offload": False}, + "gpu_uuids": ["assigned"] * gpus, + "port": port, + } + return _server_argv(spec, "baseline", host="0.0.0.0") + if runtime == "vllm-omni": + return [ + "vllm", "serve", str(model_path), "--served-model-name", plan["model_id"], + "--omni", "--host", "0.0.0.0", "--port", str(port), + "--trust-remote-code", "--task-type", "fl2va", "--num-gpus", "2", + "--tensor-parallel-size", "2", "--usp", "1", "--ring", "1", + "--text-encoder-tp-size", "2", "--vae-patch-parallel-size", "2", + "--vae-parallel-mode", "tile", "--vae-use-tiling", "--enforce-eager", + ] + raise ValueError(f"unsupported native video runtime: {runtime}") + + +def serve( + plan: dict, *, runtime: str, runtime_revision: str, model_path: Path, + port: int, gpus: int, output: Path, +) -> None: + if not re.fullmatch(r"[0-9a-f]{40}", runtime_revision): + raise ValueError("runtime revision must be an immutable commit") + if not model_path.is_dir(): + raise ValueError("the pinned model snapshot is unavailable") + if runtime == "vllm-omni": + model_path = h3_vllm_model_view(model_path, output / "model-view") + argv = server_argv(plan, runtime, model_path, port, gpus) + from .mvp_gpu_job import cuda_devices + + uuids = cuda_devices() + if len(uuids) != gpus: + raise RuntimeError("visible GPU count differs from the native recipe") + env = os.environ.copy() + env["VGBENCH_GPU_UUIDS"] = ",".join(uuids) + env["HF_HUB_OFFLINE"] = "1" + env["TRANSFORMERS_OFFLINE"] = "1" + if runtime == "vllm-omni": + env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" + env["VLLM_OMNI_VIDEO_SYNC_TIMEOUT"] = "14400" + storage = output / "storage" + storage.mkdir(parents=True, exist_ok=True) + env["VLLM_OMNI_SERVER_STORAGE__PATH"] = str(storage) + _write(output / "launch.json", { + "runtime": runtime, "runtime_revision": runtime_revision, + "model_id": plan["model_id"], "model_revision": plan["model_revision"], + "plan_sha256": h3_spec.plan_sha256(plan), "gpu_uuids": uuids, + "argv": argv, "slurm_job_id": env.get("SLURM_JOB_ID"), + }) + os.execvpe(argv[0], argv, env) + + +def run_matrix( + plan: dict, *, runtime: str, runtime_revision: str, endpoint: str, + output: Path, concurrencies: list[int], gpus: int, window_dir: Path, + request_seconds: float = 900, +) -> dict[int, dict]: + if runtime not in {"sglang", "vllm-omni"}: + raise ValueError("unsupported native video runtime") + if not concurrencies or len(set(concurrencies)) != len(concurrencies) or any(c < 1 for c in concurrencies): + raise ValueError("concurrencies must be distinct positive integers") + results = {} + for concurrency in concurrencies: + cell = output / CELL_DIR.format(concurrency=concurrency) + before = time.time() - time.monotonic() + run = run_plan( + plan, cell / "baseline", endpoint=endpoint, runtime=runtime, + runtime_revision=runtime_revision, + hardware_label=f"native srt-slurm {gpus} GPUs", + model_revision=plan["model_revision"], timeout_seconds=request_seconds, + serving_concurrency=concurrency, + ) + after = time.time() - time.monotonic() + contract = benchmark_window(run, utc_minus_monotonic=(before + after) / 2) + contract["clock"]["offset_spread_seconds"] = abs(after - before) + path = cell / f"{BENCHMARK_STEM.format(concurrency=concurrency)}.json" + _write(path, contract) + from infx.results.power.window import write_window + + write_window(path, concurrency, window_dir) + results[concurrency] = run + return results diff --git a/experimental/video-generation/pyproject.toml b/experimental/video-generation/pyproject.toml index cb62b4164b..dbabe2bc1c 100644 --- a/experimental/video-generation/pyproject.toml +++ b/experimental/video-generation/pyproject.toml @@ -7,7 +7,7 @@ name = "inferencex-h3-video-ci" version = "0.1.0" description = "Controlled H3 runtime regression measurements for InferenceX CI" requires-python = ">=3.12" -dependencies = ["av==16.1.0", "numpy==2.3.5"] +dependencies = ["av==16.1.0", "numpy==2.3.5", "pyyaml==6.0.3"] [project.optional-dependencies] dev = ["pytest==8.4.2"] diff --git a/experimental/video-generation/specs/h3.yaml b/experimental/video-generation/specs/h3.yaml index f8283295ba..f9c101411d 100644 --- a/experimental/video-generation/specs/h3.yaml +++ b/experimental/video-generation/specs/h3.yaml @@ -141,6 +141,39 @@ sets: - {case_id: bicycle-pass-s29, prompt: bicycle-pass, seed: 29} - {case_id: footsteps-s11, prompt: footsteps, seed: 11} - {case_id: footsteps-s29, prompt: footsteps, seed: 29} + release-v1: + # Twenty measured inputs: four existing prompts crossed with five previously used seeds. + # The generation block above keeps the actual 107-frame, 4.458 s H3 output contract. + plan_id: h3-native-engine-release-v1 + repetitions: 1 + warmup_runs: 1 + seed_sources: + 11: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:8 + 23: feat/wan-serving-runner@77f2080ce:experimental/video-generation/mvp/h3-smoke.plan.json:29 + 29: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:22 + 47: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:29 + 83: feat/h3-cross-hardware@0cb4c847d:experimental/video-generation/campaigns/h3-cross-hardware/formal-8s-plan.json:64 + cases: + - {case_id: drum-taps-s11, prompt: drum-taps, seed: 11} + - {case_id: drum-taps-s23, prompt: drum-taps, seed: 23} + - {case_id: drum-taps-s29, prompt: drum-taps, seed: 29} + - {case_id: drum-taps-s47, prompt: drum-taps, seed: 47} + - {case_id: drum-taps-s83, prompt: drum-taps, seed: 83} + - {case_id: pouring-water-s11, prompt: pouring-water, seed: 11} + - {case_id: pouring-water-s23, prompt: pouring-water, seed: 23} + - {case_id: pouring-water-s29, prompt: pouring-water, seed: 29} + - {case_id: pouring-water-s47, prompt: pouring-water, seed: 47} + - {case_id: pouring-water-s83, prompt: pouring-water, seed: 83} + - {case_id: bicycle-pass-s11, prompt: bicycle-pass, seed: 11} + - {case_id: bicycle-pass-s23, prompt: bicycle-pass, seed: 23} + - {case_id: bicycle-pass-s29, prompt: bicycle-pass, seed: 29} + - {case_id: bicycle-pass-s47, prompt: bicycle-pass, seed: 47} + - {case_id: bicycle-pass-s83, prompt: bicycle-pass, seed: 83} + - {case_id: footsteps-s11, prompt: footsteps, seed: 11} + - {case_id: footsteps-s23, prompt: footsteps, seed: 23} + - {case_id: footsteps-s29, prompt: footsteps, seed: 29} + - {case_id: footsteps-s47, prompt: footsteps, seed: 47} + - {case_id: footsteps-s83, prompt: footsteps, seed: 83} # Informational only; not hashed. reference: diff --git a/experimental/video-generation/tests/test_srt_selected.py b/experimental/video-generation/tests/test_srt_selected.py new file mode 100644 index 0000000000..5d294cba8f --- /dev/null +++ b/experimental/video-generation/tests/test_srt_selected.py @@ -0,0 +1,51 @@ +"""The native YAML route preserves the selected request and model-view contract.""" + +import sys +from pathlib import Path + +import pytest + +PACKAGE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PACKAGE)) + +from evaluator import mvp_srt_selected # noqa: E402 +from evaluator.mvp_runner import preview_plan # noqa: E402 +from specs import h3_spec # noqa: E402 + + +def test_h3_selected_plan_drives_both_request_mappings(): + path = PACKAGE / "specs/h3.yaml" + spec = h3_spec.load_spec(path) + expected = h3_spec.plan_sha256(h3_spec.plan_for(spec, "release-v1")) + plan = mvp_srt_selected.selected_plan(path, "release-v1", expected) + sglang = preview_plan(plan, runtime="sglang") + vllm = preview_plan(plan, runtime="vllm-omni") + assert sglang["plan_sha256"] == vllm["plan_sha256"] == expected + assert sglang["measurement_count"] == vllm["measurement_count"] == 20 + assert sglang["total_requests"] == vllm["total_requests"] == 21 + assert sglang["slots"][1]["request"]["prompt"] == vllm["slots"][1]["request"]["prompt"] + with pytest.raises(ValueError, match="plan_sha256"): + mvp_srt_selected.selected_plan(path, "release-v1", "0" * 64) + + +def test_h3_vllm_model_view_exposes_only_tested_partition(tmp_path): + snapshot = tmp_path / "snapshot" + (snapshot / "FL2VA").mkdir(parents=True) + (snapshot / "Ref2VA").mkdir() + for name in ("model_index.json", "LICENSE", "README.md", "Ref2VA/model_index.json", "modular_model_index.json"): + (snapshot / name).write_text(name) + view = mvp_srt_selected.h3_vllm_model_view(snapshot, tmp_path / "view") + assert (view / "FL2VA").resolve() == (snapshot / "FL2VA").resolve() + assert (view / "Ref2VA/model_index.json").read_text() == "Ref2VA/model_index.json" + assert not (view / "modular_model_index.json").exists() + + +def test_h3_vllm_command_uses_the_tested_two_gpu_fl2va_profile(): + plan = h3_spec.plan_for(h3_spec.load_spec(PACKAGE / "specs/h3.yaml"), "release-v1") + argv = mvp_srt_selected.server_argv(plan, "vllm-omni", Path("/model"), 30283, 2) + assert argv[:3] == ["vllm", "serve", "/model"] + assert argv[argv.index("--task-type") + 1] == "fl2va" + assert argv[argv.index("--tensor-parallel-size") + 1] == "2" + assert argv[argv.index("--num-gpus") + 1] == "2" + with pytest.raises(ValueError, match="two GPUs"): + mvp_srt_selected.server_argv(plan, "vllm-omni", Path("/model"), 30283, 4) diff --git a/inferencex-e2e/benchmarks/h3/run_client.sh b/inferencex-e2e/benchmarks/h3/run_client.sh index 74f7d33929..52b44b6025 100755 --- a/inferencex-e2e/benchmarks/h3/run_client.sh +++ b/inferencex-e2e/benchmarks/h3/run_client.sh @@ -24,6 +24,15 @@ mkdir -p "$H3_RESULT_DIR" "$SRT_MEASUREMENT_WINDOW_DIR" # shellcheck disable=SC2086 # CONC_LIST is a space-separated list of integers. export PYTHONPATH="${H3_PACKAGE_ROOT}:${INFMAX_CONTAINER_WORKSPACE}${PYTHONPATH:+:$PYTHONPATH}" +if [[ -n "${H3_SET:-}" ]]; then + check_env_vars H3_PLAN_SHA256 H3_RUNTIME H3_RUNTIME_REVISION GPU_COUNT + exec python3 -m evaluator.cli srt-yaml-client \ + --spec "$H3_SPEC_PATH" --set "$H3_SET" --plan-sha256 "$H3_PLAN_SHA256" \ + --runtime "$H3_RUNTIME" --runtime-revision "$H3_RUNTIME_REVISION" \ + --endpoint "$endpoint" --output "$H3_RESULT_DIR" \ + --window-dir "$SRT_MEASUREMENT_WINDOW_DIR" --gpus "$GPU_COUNT" \ + --concurrencies $CONC_LIST +fi exec python3 -m evaluator.cli srt-client \ --spec "$H3_SPEC_PATH" \ --endpoint "$endpoint" \ diff --git a/inferencex-e2e/benchmarks/h3/run_server.sh b/inferencex-e2e/benchmarks/h3/run_server.sh index 8842c3ddf8..a371be7ef8 100755 --- a/inferencex-e2e/benchmarks/h3/run_server.sh +++ b/inferencex-e2e/benchmarks/h3/run_server.sh @@ -10,6 +10,14 @@ source "$INFMAX_CONTAINER_WORKSPACE/benchmarks/check_env.sh" check_env_vars H3_SPEC_PATH H3_PACKAGE_ROOT H3_RESULT_DIR H3_SERVER_PORT H3_SERVER_ROLE export PYTHONPATH="${H3_PACKAGE_ROOT}:${INFMAX_CONTAINER_WORKSPACE}${PYTHONPATH:+:$PYTHONPATH}" +if [[ -n "${H3_SET:-}" ]]; then + check_env_vars H3_PLAN_SHA256 H3_RUNTIME H3_RUNTIME_REVISION H3_MODEL_PATH GPU_COUNT + exec python3 -m evaluator.cli srt-yaml-serve \ + --spec "$H3_SPEC_PATH" --set "$H3_SET" --plan-sha256 "$H3_PLAN_SHA256" \ + --runtime "$H3_RUNTIME" --runtime-revision "$H3_RUNTIME_REVISION" \ + --model-path "$H3_MODEL_PATH" --port "$H3_SERVER_PORT" \ + --gpus "$GPU_COUNT" --record "$H3_RESULT_DIR/server" +fi exec python3 -m evaluator.cli srt-serve \ --spec "$H3_SPEC_PATH" \ --role "$H3_SERVER_ROLE" \ diff --git a/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/sglang/h200-bf16/serving-client.yaml b/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/sglang/h200-bf16/serving-client.yaml index 890a6b24e4..5351907345 100644 --- a/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/sglang/h200-bf16/serving-client.yaml +++ b/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/sglang/h200-bf16/serving-client.yaml @@ -28,6 +28,7 @@ services: PYTHONUNBUFFERED: "1" H3_SERVER_ROLE: baseline H3_SERVER_PORT: "30283" + VIDEO_RUNTIME_REVISION: 71de97b264b04dcd514cf904003028aefe9775c8 readiness: http: port: 30283 diff --git a/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/vllm-omni/h200-bf16/serving-client.yaml b/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/vllm-omni/h200-bf16/serving-client.yaml new file mode 100644 index 0000000000..19abf15a9a --- /dev/null +++ b/inferencex-e2e/benchmarks/single_node/srt-slurm-recipes/h3/vllm-omni/h200-bf16/serving-client.yaml @@ -0,0 +1,37 @@ +schema: 2 +name: h3-vllm-omni-serving-client + +resources: + gpu_type: h200 + +frontend: + type: none + +services: + - name: h3-server + type: generic + nodes: 1 + critical: true + start: before_workers + command: + - bash + - /infmax-workspace/benchmarks/h3/run_server.sh + env: + PYTHONUNBUFFERED: "1" + H3_SERVER_ROLE: baseline + H3_SERVER_PORT: "30283" + VIDEO_RUNTIME_REVISION: a8576ccb725c4e21cd13c3eb5f9a546b21149d2b + readiness: + http: + port: 30283 + path: /health + timeout_seconds: 900 + interval_seconds: 5 + +benchmark: + type: custom + command: bash /infmax-workspace/benchmarks/h3/run_client.sh + env: + PYTHONUNBUFFERED: "1" + H3_SERVER_SERVICE: h3-server + H3_SERVER_PORT: "30283" diff --git a/inferencex-e2e/configs/nvidia-master.yaml b/inferencex-e2e/configs/nvidia-master.yaml index b6e9cfd313..6061b7968d 100644 --- a/inferencex-e2e/configs/nvidia-master.yaml +++ b/inferencex-e2e/configs/nvidia-master.yaml @@ -9827,11 +9827,30 @@ h3-bf16-h200-sglang-video: srt-recipe-dir: h3/sglang/h200-bf16 scenarios: video-generation: - - spec: h3-serving-2gpu.json + - spec: h3.yaml + set: release-v1 search-space: - gpus: 2 srt-recipe: serving-client.yaml - conc-list: [1, 2, 4] + conc-list: [1] + +h3-bf16-h200-vllm-omni-video: + image: vllm/vllm-omni:v0.30.0@sha256:fc77cfaac7b1b43c30cbf32eaea214864434c6e0f303e6e9eb9f64104241a994 + model: MiniMaxAI/MiniMax-H3 + model-prefix: h3 + runner: cluster:h200-dgxc + precision: bf16 + framework: vllm-omni + multinode: false + srt-recipe-dir: h3/vllm-omni/h200-bf16 + scenarios: + video-generation: + - spec: h3.yaml + set: release-v1 + search-space: + - gpus: 2 + srt-recipe: serving-client.yaml + conc-list: [1] h3-bf16-b200-sglang-video: # The B200 runtime is a CUDA 13 build whose SGLang lives in the /opt/sglang venv, so its diff --git a/inferencex-e2e/infx/srt_slurm/generate.py b/inferencex-e2e/infx/srt_slurm/generate.py index 021ed056df..af8bf5974a 100644 --- a/inferencex-e2e/infx/srt_slurm/generate.py +++ b/inferencex-e2e/infx/srt_slurm/generate.py @@ -42,7 +42,7 @@ def is_video(point: Mapping[str, Any]) -> bool: def point_environment(point: Mapping[str, Any]) -> dict[str, str]: """The workflow environment the launcher reads for ``point``.""" if is_video(point): - return { + environment = { "IMAGE": point["image"], "MODEL": point["model"], "MODEL_PREFIX": point["model-prefix"], @@ -63,6 +63,9 @@ def point_environment(point: Mapping[str, Any]) -> dict[str, str]: "HF_HUB_CACHE": HF_HUB_CACHE, "RESULT_FILENAME": point["exp-name"], } + if "set" in point: + environment["H3_SET"] = point["set"] + return environment agentic = point.get("scenario-type") == "agentic-coding" environment = { "IMAGE": point["image"], diff --git a/inferencex-e2e/infx/srt_slurm/video.py b/inferencex-e2e/infx/srt_slurm/video.py index b57ad86423..92985830f5 100644 --- a/inferencex-e2e/infx/srt_slurm/video.py +++ b/inferencex-e2e/infx/srt_slurm/video.py @@ -10,6 +10,7 @@ from __future__ import annotations import argparse +import importlib.util import json import os import re @@ -50,6 +51,10 @@ "H3_PACKAGE_ROOT", "H3_MODEL_PATH", "H3_RESULT_DIR", + "H3_SET", + "H3_PLAN_SHA256", + "H3_RUNTIME", + "H3_RUNTIME_REVISION", ) _BOUND_KEYS = ( ("model",), @@ -59,7 +64,10 @@ ("benchmark", "concurrencies"), *(("benchmark", "env", name) for name in VIDEO_ENV), ) -_SERVICE_ENV = ("H3_SPEC_PATH", "H3_PACKAGE_ROOT", "H3_MODEL_PATH", "H3_RESULT_DIR") +_SERVICE_ENV = ( + "H3_SPEC_PATH", "H3_PACKAGE_ROOT", "H3_MODEL_PATH", "H3_RESULT_DIR", + "H3_SET", "H3_PLAN_SHA256", "H3_RUNTIME", "H3_RUNTIME_REVISION", "GPU_COUNT", +) def video_package(root: Path | None = None) -> Path: @@ -121,15 +129,35 @@ def spec_model_path(hf_hub_cache: str, model: str, revision: str) -> str: return str(cache / f"models--{model.replace('/', '--')}" / "snapshots" / revision) -def load_spec(name: str, *, root: Path) -> dict[str, Any]: - """The frozen H3 workload ``name`` under the harness's ``specs`` directory.""" - if name != PurePosixPath(name).name or not name.endswith(".json"): +def load_spec(name: str, *, root: Path, set_id: str | None = None) -> dict[str, Any]: + """The selected video workload under the harness's ``specs`` directory.""" + if name != PurePosixPath(name).name or not name.endswith((".json", ".yaml", ".yml")): raise ValueError( - f"H3_SPEC must name a JSON file under {VIDEO_PACKAGE}/{SPEC_DIR}: {name!r}" + f"H3_SPEC must name a JSON or YAML file under {VIDEO_PACKAGE}/{SPEC_DIR}: {name!r}" ) path = video_package(root) / SPEC_DIR / name if not path.is_file(): raise ValueError(f"H3 spec not found: {path}") + if path.suffix in {".yaml", ".yml"}: + if not set_id or not re.fullmatch(r"[A-Za-z0-9_-]+", set_id): + raise ValueError("H3_SET must select a safe nonempty YAML workload set") + helper = path.with_name("h3_spec.py") + module_spec = importlib.util.spec_from_file_location("video_workload_spec", helper) + if module_spec is None or module_spec.loader is None: + raise ValueError(f"cannot load workload helper: {helper}") + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + source = module.load_spec(path) + if set_id not in source["sets"]: + raise ValueError(f"H3_SET {set_id!r} is not defined in {name}") + plan = module.plan_for(source, set_id) + return { + "plan": plan, + "model": {"revision": source["model"]["revision"]["value"]}, + "plan_sha256": module.plan_sha256(plan), + } + if set_id: + raise ValueError("H3_SET is only valid with a YAML workload") spec = json.loads(path.read_text()) if not isinstance(spec, dict): raise ValueError(f"{path}: spec must be an object") @@ -162,13 +190,14 @@ def bind_video( gpus = _positive(_required(environment, "GPU_COUNT"), "GPU_COUNT") concurrencies = parse_concurrencies(_required(environment, "CONC_LIST")) spec_name = _required(environment, "H3_SPEC") - spec = load_spec(spec_name, root=root) + set_id = environment.get("H3_SET") or None + spec = load_spec(spec_name, root=root, set_id=set_id) if spec["plan"]["model_id"] != model: raise ValueError(f"H3 spec serves {spec['plan']['model_id']!r}, the point {model!r}") model_path = spec_model_path( _required(environment, "HF_HUB_CACHE"), model, spec["model"]["revision"] ) - if spec["model"]["path"] != model_path: + if "path" in spec["model"] and spec["model"]["path"] != model_path: raise ValueError( f"H3 spec model.path {spec['model']['path']!r} is not the mounted hub cache " f"snapshot {model_path!r}" @@ -190,7 +219,7 @@ def bind_video( ): raise ValueError(f"video recipes run {CLIENT_SCRIPT} as benchmark.type custom") workload = benchmark.setdefault("env", {}) - port = str(spec["port"]) + port = str(spec.get("port", (service.get("readiness") or {}).get("http", {}).get("port", ""))) readiness = (service.get("readiness") or {}).get("http") or {} if ( str(workload.get("H3_SERVER_PORT")) != port @@ -214,8 +243,25 @@ def bind_video( "H3_MODEL_PATH": model_path, "H3_RESULT_DIR": RESULT_DIR, } + if set_id: + runtime = _required(environment, "FRAMEWORK") + if runtime not in {"sglang", "vllm-omni"}: + raise ValueError(f"unsupported YAML video runtime: {runtime!r}") + revision = str((service.get("env") or {}).get("VIDEO_RUNTIME_REVISION", "")) + if not re.fullmatch(r"[0-9a-f]{40}", revision): + raise ValueError("video service requires a pinned VIDEO_RUNTIME_REVISION") + values.update({ + "H3_SET": set_id, + "H3_PLAN_SHA256": spec["plan_sha256"], + "H3_RUNTIME": runtime, + "H3_RUNTIME_REVISION": revision, + "GPU_COUNT": str(gpus), + }) workload.update(values) - service["env"] = {**(service.get("env") or {}), **{k: values[k] for k in _SERVICE_ENV}} + service["env"] = { + **(service.get("env") or {}), + **{k: values[k] for k in _SERVICE_ENV if k in values}, + } if (bound.get("telemetry") or {}).get("enabled") is True: benchmark["concurrencies"] = list(concurrencies) required = environment.get("REQUIRE_POWER", "0") in _POWER_FLAGS diff --git a/inferencex-e2e/infx/tests/srt_slurm/test_video_selected.py b/inferencex-e2e/infx/tests/srt_slurm/test_video_selected.py new file mode 100644 index 0000000000..95d3687379 --- /dev/null +++ b/inferencex-e2e/infx/tests/srt_slurm/test_video_selected.py @@ -0,0 +1,70 @@ +"""Selected YAML workloads bind to the native video service and client.""" + +import pytest + +from infx.config import repository_root +from infx.srt_slurm.generate import point_environment +from infx.srt_slurm import video + +ROOT = repository_root() +RECIPES = ROOT / "benchmarks/single_node/srt-slurm-recipes/h3" +ENV = { + "IMAGE": "vllm/vllm-omni:v0.30.0", + "MODEL": "MiniMaxAI/MiniMax-H3", + "PRECISION": "bf16", + "GPU_COUNT": "2", + "CONC_LIST": "1", + "H3_SPEC": "h3.yaml", + "H3_SET": "release-v1", + "FRAMEWORK": "vllm-omni", + "HF_HUB_CACHE": "/mnt/hf_hub_cache", + "RESULT_FILENAME": "h3-native-release", +} + + +def test_selected_set_reaches_the_video_binder(): + point = { + "scenario-type": "video-generation", "image": ENV["IMAGE"], + "model": ENV["MODEL"], "model-prefix": "MiniMax-H3", "precision": "bf16", + "framework": "vllm-omni", "spec": "h3.yaml", "set": "release-v1", + "gpus": 2, "conc-list": [1], "exp-name": "h3-native-release", + } + environment = point_environment(point) + assert environment["H3_SET"] == "release-v1" + recipe = video.prepare( + RECIPES / "vllm-omni/h200-bf16/serving-client.yaml", environment, root=ROOT + ) + assert recipe["benchmark"]["env"]["H3_SET"] == "release-v1" + + +def test_both_h3_engines_bind_the_same_selected_plan(): + vllm = video.prepare(RECIPES / "vllm-omni/h200-bf16/serving-client.yaml", ENV, root=ROOT) + sglang = video.prepare( + RECIPES / "sglang/h200-bf16/serving-client.yaml", + {**ENV, "FRAMEWORK": "sglang", "IMAGE": "hongccc/sglang-omni:h3-runtime-71de97b"}, + root=ROOT, + ) + for recipe, runtime in ((vllm, "vllm-omni"), (sglang, "sglang")): + [service] = recipe["services"] + client = recipe["benchmark"]["env"] + assert service["env"]["H3_PLAN_SHA256"] == client["H3_PLAN_SHA256"] + assert service["env"]["H3_SET"] == client["H3_SET"] == "release-v1" + assert service["env"]["H3_RUNTIME"] == client["H3_RUNTIME"] == runtime + assert service["env"]["H3_RUNTIME_REVISION"] == client["H3_RUNTIME_REVISION"] + assert client["H3_SPEC_PATH"] == "/h3-package/specs/h3.yaml" + assert client["GPU_COUNT"] == "2" + assert vllm["benchmark"]["env"]["H3_PLAN_SHA256"] == sglang["benchmark"]["env"]["H3_PLAN_SHA256"] + + +@pytest.mark.parametrize("edits", [ + {"H3_SET": "missing"}, + {"H3_SET": "../release-v1"}, + {"MODEL": "Other/model"}, + {"FRAMEWORK": "other"}, +]) +def test_invalid_selected_plan_or_runtime_fails_before_submission(edits): + with pytest.raises(ValueError): + video.prepare( + RECIPES / "vllm-omni/h200-bf16/serving-client.yaml", + {**ENV, **edits}, root=ROOT, + ) From 2c5c0850c1b1fe3cfa56965cb97942161bbbcc27 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 18 Oct 2026 02:03:59 -0700 Subject: [PATCH 10/12] chore: declare H3 engine rows for native full sweep --- .../video-generation/specs/h3-vllm-omni-serving.md | 4 ++-- inferencex-e2e/perf-changelog.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/experimental/video-generation/specs/h3-vllm-omni-serving.md b/experimental/video-generation/specs/h3-vllm-omni-serving.md index b6bb4381d6..64a796231d 100644 --- a/experimental/video-generation/specs/h3-vllm-omni-serving.md +++ b/experimental/video-generation/specs/h3-vllm-omni-serving.md @@ -1,6 +1,6 @@ # Serving MiniMax-H3 on vLLM-Omni -What the Phase A feasibility run used on 2x H100 80GB on 2026-10-17. Phase B benchmarking starts from this and records any change. +Configuration observed for one MiniMax-H3 request on 2x H100 80GB on 2026-10-17. ## Pins @@ -36,7 +36,7 @@ VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 ## Parallelism -Tensor parallel 2 across both GPUs with Ulysses and ring degree 1, text encoder TP 2, VAE patch-parallel 2 in tile mode with tiling. This is the upstream recipe's only two-GPU resident layout; pure Ulysses replicates the whole DiT on every rank, so SGLang's `ulysses_degree 2 / tp_size 1` layout from #2916 does not fit 80 GB here. Phase A ran eager; Phase B should also try without `--enforce-eager` and report compile time separately. +Tensor parallel 2 across both GPUs with Ulysses and ring degree 1, text encoder TP 2, VAE patch-parallel 2 in tile mode with tiling. This is the upstream recipe's only two-GPU resident layout; pure Ulysses replicates the whole DiT on every rank, so SGLang's `ulysses_degree 2 / tp_size 1` layout from #2916 does not fit 80 GB here. The observed request used `--enforce-eager`. ## Attention backend diff --git a/inferencex-e2e/perf-changelog.yaml b/inferencex-e2e/perf-changelog.yaml index 463fc55ff1..77a4b434b4 100644 --- a/inferencex-e2e/perf-changelog.yaml +++ b/inferencex-e2e/perf-changelog.yaml @@ -10108,3 +10108,13 @@ - "The H3 server runs as a recipe service and the H3 client as the custom benchmark; GPU power comes from srt-slurm's native telemetry re-validated per concurrency with energy normalized per valid clip." - "Include video deployments in the PR canary/full sweep, seal per-deployment artifacts and native power provenance, and validate complete source coverage before video publication." pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2916 + +- config-keys: + - h3-bf16-h200-sglang-video + - h3-bf16-h200-vllm-omni-video + scenario-type: + - video-generation + description: + - "Run both H200 MiniMax-H3 engines at concurrency 1 on the same selected YAML workload: four existing prompts crossed with five cited seeds, 20 measured clips and one warmup." + - "Replace the H200 SGLang four-clip c1/c2/c4 recipe with the selected two-GPU c1 workload, and add the two-GPU vLLM-Omni FL2VA recipe with the pinned v0.30.0 image digest and TP2/VAE tile settings." + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/4243 From 48c3783c149d723843c0ce99f28e67fe093b97f3 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 18 Oct 2026 02:10:03 -0700 Subject: [PATCH 11/12] fix: seal selected native video execution spec --- .../evaluator/mvp_srt_selected.py | 13 ++++++++ .../tests/test_srt_selected.py | 33 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/experimental/video-generation/evaluator/mvp_srt_selected.py b/experimental/video-generation/evaluator/mvp_srt_selected.py index 2a1dc4b131..c168e4002c 100644 --- a/experimental/video-generation/evaluator/mvp_srt_selected.py +++ b/experimental/video-generation/evaluator/mvp_srt_selected.py @@ -91,6 +91,19 @@ def serve( storage = output / "storage" storage.mkdir(parents=True, exist_ok=True) env["VLLM_OMNI_SERVER_STORAGE__PATH"] = str(storage) + _write(output / "spec.json", { + "schema_version": "0.1.0", + "plan": plan, + "gpu_uuids": uuids, + "baseline": { + "name": runtime, + "revision": runtime_revision, + "model_path": str(model_path), + "model_revision": plan["model_revision"], + "argv": argv, + }, + "serving": {"mode": "closed_loop"}, + }) _write(output / "launch.json", { "runtime": runtime, "runtime_revision": runtime_revision, "model_id": plan["model_id"], "model_revision": plan["model_revision"], diff --git a/experimental/video-generation/tests/test_srt_selected.py b/experimental/video-generation/tests/test_srt_selected.py index 5d294cba8f..b2ff328a87 100644 --- a/experimental/video-generation/tests/test_srt_selected.py +++ b/experimental/video-generation/tests/test_srt_selected.py @@ -1,5 +1,6 @@ """The native YAML route preserves the selected request and model-view contract.""" +import json import sys from pathlib import Path @@ -8,7 +9,7 @@ PACKAGE = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PACKAGE)) -from evaluator import mvp_srt_selected # noqa: E402 +from evaluator import mvp_gpu_job, mvp_srt_selected # noqa: E402 from evaluator.mvp_runner import preview_plan # noqa: E402 from specs import h3_spec # noqa: E402 @@ -49,3 +50,33 @@ def test_h3_vllm_command_uses_the_tested_two_gpu_fl2va_profile(): assert argv[argv.index("--num-gpus") + 1] == "2" with pytest.raises(ValueError, match="two GPUs"): mvp_srt_selected.server_argv(plan, "vllm-omni", Path("/model"), 30283, 4) + + +@pytest.mark.parametrize("runtime", ["sglang", "vllm-omni"]) +def test_selected_server_seals_its_bound_execution_spec(tmp_path, monkeypatch, runtime): + plan = h3_spec.plan_for(h3_spec.load_spec(PACKAGE / "specs/h3.yaml"), "release-v1") + model = tmp_path / "model" + (model / "FL2VA").mkdir(parents=True) + (model / "Ref2VA").mkdir() + for name in ("model_index.json", "LICENSE", "README.md", "Ref2VA/model_index.json"): + (model / name).write_text(name) + uuids = ["GPU-1", "GPU-2"] + monkeypatch.setattr(mvp_gpu_job, "cuda_devices", lambda: uuids) + executed = [] + monkeypatch.setattr(mvp_srt_selected.os, "execvpe", lambda file, argv, env: executed.append((file, argv))) + output = tmp_path / "server" + mvp_srt_selected.serve( + plan, runtime=runtime, runtime_revision="a" * 40, + model_path=model, port=30283, gpus=2, output=output, + ) + spec = json.loads((output / "spec.json").read_text()) + launch = json.loads((output / "launch.json").read_text()) + assert spec["plan"] == plan and spec["gpu_uuids"] == launch["gpu_uuids"] == uuids + assert spec["serving"] == {"mode": "closed_loop"} + assert spec["baseline"]["name"] == launch["runtime"] == runtime + assert spec["baseline"]["revision"] == launch["runtime_revision"] == "a" * 40 + assert spec["baseline"]["argv"] == launch["argv"] == executed[0][1] + if runtime == "vllm-omni": + assert spec["baseline"]["model_path"] == executed[0][1][2] + else: + assert spec["baseline"]["model_path"] == str(model) From 07638040228488bc0563c2a0afc57ef64c923dd2 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 18 Oct 2026 02:25:03 -0700 Subject: [PATCH 12/12] chore: format selected video binder for CI --- inferencex-e2e/infx/srt_slurm/video.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/inferencex-e2e/infx/srt_slurm/video.py b/inferencex-e2e/infx/srt_slurm/video.py index 92985830f5..2c8c0a6401 100644 --- a/inferencex-e2e/infx/srt_slurm/video.py +++ b/inferencex-e2e/infx/srt_slurm/video.py @@ -65,8 +65,15 @@ *(("benchmark", "env", name) for name in VIDEO_ENV), ) _SERVICE_ENV = ( - "H3_SPEC_PATH", "H3_PACKAGE_ROOT", "H3_MODEL_PATH", "H3_RESULT_DIR", - "H3_SET", "H3_PLAN_SHA256", "H3_RUNTIME", "H3_RUNTIME_REVISION", "GPU_COUNT", + "H3_SPEC_PATH", + "H3_PACKAGE_ROOT", + "H3_MODEL_PATH", + "H3_RESULT_DIR", + "H3_SET", + "H3_PLAN_SHA256", + "H3_RUNTIME", + "H3_RUNTIME_REVISION", + "GPU_COUNT", ) @@ -250,13 +257,15 @@ def bind_video( revision = str((service.get("env") or {}).get("VIDEO_RUNTIME_REVISION", "")) if not re.fullmatch(r"[0-9a-f]{40}", revision): raise ValueError("video service requires a pinned VIDEO_RUNTIME_REVISION") - values.update({ - "H3_SET": set_id, - "H3_PLAN_SHA256": spec["plan_sha256"], - "H3_RUNTIME": runtime, - "H3_RUNTIME_REVISION": revision, - "GPU_COUNT": str(gpus), - }) + values.update( + { + "H3_SET": set_id, + "H3_PLAN_SHA256": spec["plan_sha256"], + "H3_RUNTIME": runtime, + "H3_RUNTIME_REVISION": revision, + "GPU_COUNT": str(gpus), + } + ) workload.update(values) service["env"] = { **(service.get("env") or {}),