From ebaa1c5609107df0bffea0115e293b1140bf34e2 Mon Sep 17 00:00:00 2001 From: Ivan Zatevakhin Date: Sat, 24 Oct 2026 03:23:20 +0100 Subject: [PATCH] chore: simplify v0.0.2 bindings tooling and examples --- CHANGELOG.md | 5 +- Justfile | 268 +-- README.md | 22 +- RELEASING.md | 250 +-- tests/release_tooling.py | 215 -- vllm-cpp/README.md | 2 +- vllm-cpp/examples/README.md | 27 +- vllm-cpp/examples/video_generate.rs | 2823 +++------------------------ vllm-cpp/src/engine.rs | 2 - 9 files changed, 375 insertions(+), 3239 deletions(-) delete mode 100644 tests/release_tooling.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2536f82..307feb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,12 @@ This entry describes the release candidate; it does not authorize or attest publ - Pinned bundled native vllm.cpp to tag `v0.0.2`, commit `7020de93652ca920424a10ac5255b34810dd2f24`, moving the stable C contract from ABI 10 with 19 functions to ABI 17 with 35 functions; system libraries must match the new ABI, layouts, signatures, and exports. - Model construction now obtains native defaults and overlays only explicit Rust settings, preserving helper defaults including `block_size=32`, `max_num_seqs=32`, and `gpu_memory_utilization=0.92`. - Hardened deterministic backend configuration, binding/export/package checks, package curation, and multi-architecture Triton packaging: all six vendored AOT trees are embedded, exact-SM dispatched, and paired with portable fallback on other accepted targets. -- Removed arbitrary package size and file-count caps for both crates. File counts, compressed sizes, regular-file payload sums, and SHA-256 hashes remain diagnostics, not release limits; exact inventories and all content, provenance, extracted-build, and downstream compatibility checks remain required. +- Removed arbitrary package size and file-count caps for both crates. File counts, compressed sizes, regular-file payload sums, and SHA-256 hashes remain diagnostics, not release limits; archive inventories must match Cargo's package lists, and content, provenance, extracted-build, and downstream compatibility checks remain required. - Contained the entire Rust logits-processor trampoline lifetime, including final capture destruction after concurrent registration teardown, while preserving callback panic reporting and panic-payload disposal. - Pinned online GGUF metadata and body retrieval to a validated immutable commit; explicit full-SHA requests reject different metadata or offline snapshots. Validate downloads before explicitly updating requested refs, and repair hf-hub's implicit pinned-SHA ref writes on rejection without rolling back mutable aliases. Offline resolution still requires a cache ref; mutable `main` remains the default requested revision. - Native C API fixture failures now retain the default scheduler's failing status; an alternate scheduler run cannot make the gate pass. Callback counts are not token counts, so the upstream callback-count assertion alone does not establish token loss. - Optional ASan/UBSan and native-only TSan recipes select exact package/target test executables from Cargo JSON, reject empty suites, and require each exact TSan filter to pass one named test. Removed the stale release number from the CUDA/Vulkan conflict diagnostic. +- Simplified duplicated packaging policy and release documentation, and replaced the video downloader/mux application with a minimal local-checkpoint PPM/WAV example. Public library APIs and centralized native-buffer safety checks are unchanged. ### Added @@ -25,7 +26,7 @@ This entry describes the release candidate; it does not authorize or attest publ - Pre-tokenized `Engine::complete_tokens` with owned token output, optional copied completion metadata, and explicit truncation reporting. - Thread-local, exclusive `TranscriptionEngine` and `EmbeddingEngine` owners with borrowed inputs and Rust-owned transcription and row-major embedding results. - Separate `VideoEngine` checkpoint-set ownership, blocking generation parameters/results, and standalone `VideoMuxParams`/`VideoMuxArgv` composition that never executes ffmpeg. -- Model-free acceptance gates for docs, tests, ABI/layout/signature/exports, native fixtures, sanitizers, link modes, packages, downstream consumers, publish dry-run, and exact MSRV. Focused Rust and tooling regressions cover callback destruction, loopback Hub branch movement/cache identity, default native-gate status, mixed artifacts, missing filters, and filesystem-independent payload accounting. These gate definitions are not evidence of an exact-candidate pass. +- Model-free acceptance gates for docs, tests, ABI/layout/signature/exports, native fixtures, sanitizers, link modes, packages, downstream consumers, publish dry-run, and exact MSRV. Focused Rust regressions cover callback destruction and loopback Hub branch movement/cache identity. Native-gate status, exact artifact selection, nonzero test counts and named filters, and regular-file payload accounting remain enforced by the gate implementations. These gate definitions are not evidence of an exact-candidate pass. ### Compatibility diff --git a/Justfile b/Justfile index 19eddf6..50f5b4f 100644 --- a/Justfile +++ b/Justfile @@ -410,21 +410,24 @@ package-test: echo "expected pinned native version 0.0.2, found $native_version" >&2 exit 1 } - jq -e --arg version "$version" ' - [.packages[] | select(.name == "vllm-cpp" or .name == "vllm-cpp-sys")] - | length == 2 - and all(.version == $version) - and all(.rust_version == "1.85") - and all(.license == "MIT OR Apache-2.0") - and all(.repository == "https://github.com/querymt/vllm-cpp-rs") - and all(.readme == "README.md") - and (map(select(.name == "vllm-cpp" and .documentation == "https://docs.rs/vllm-cpp")) | length == 1) - and (map(select(.name == "vllm-cpp-sys" and .documentation == "https://docs.rs/vllm-cpp-sys")) | length == 1) - and (map(select(.name == "vllm-cpp") | .dependencies[] | select(.name == "vllm-cpp-sys" and .req == ("=" + $version) and .uses_default_features == false)) | length == 1) - and (map(select(.name == "vllm-cpp") | .dependencies[] | select(.name == "hf-hub" and .req == "^0.5.0" and .optional == false and .uses_default_features == false and .features == ["ureq"])) | length == 1) - and (map(select(.name == "vllm-cpp") | .dependencies[] | select(.name == "serde_json" and .optional == false and .kind == null)) | length == 1) - and (map(select(.name == "vllm-cpp") | .dependencies[] | select(.name == "clap" and .req == "=4.6.1" and .kind == "dev" and .optional == false and .features == ["derive"])) | length == 1) - ' "$metadata" >/dev/null + check_package_metadata() { + jq -e --arg version "$version" ' + [.packages[] | select(.name == "vllm-cpp" or .name == "vllm-cpp-sys")] + | length > 0 + and all(.version == $version and .version == "0.0.2") + and all(.rust_version == "1.85") + and all(.license == "MIT OR Apache-2.0") + and all(.repository == "https://github.com/querymt/vllm-cpp-rs") + and all(.readme == "README.md") + and all(.documentation == ("https://docs.rs/" + .name)) + and all(.features.default == ["bundled"]) + and all(.[] | select(.name == "vllm-cpp"); + .features.serde == [] + and (.dependencies | map(select(.name == "vllm-cpp-sys" + and .req == ("=" + $version) and .uses_default_features == false)) | length == 1)) + ' "$1" >/dev/null + } + check_package_metadata "$metadata" sys_list="$temp/vllm-cpp-sys.list" safe_list="$temp/vllm-cpp.list" @@ -456,192 +459,23 @@ package-test: diff -u "$sys_list" <(archive_inventory "$sys_package" "vllm-cpp-sys-$version") diff -u "$safe_list" <(archive_inventory "$safe_package" "vllm-cpp-$safe_version") - native_exclude_pattern='^vllm\.cpp/cmake/(CudaArchFeaturesTest|CudaSourceGencodeTest|DumpTritonAOTContract|InSourceGuardTest|TritonAOTDefaultTest|TritonAOTMultiArchTest|VerifyExports)\.cmake$|^vllm\.cpp/include/vt/rocm/(rocm_gelu_mul_sep|rocm_gemma4_expert_geglu|rocm_matmul_batch|rocm_rmsnorm_plus_add)\.h$|^vllm\.cpp/src/vllm/entrypoints/openai/(api_server|server_main)\.cpp$|^vllm\.cpp/src/vllm/platforms/tenstorrent\.cpp$|^vllm\.cpp/src/vt/cuda/marlin/.*/generate_kernels\.py$|^vllm\.cpp/src/vt/rocm/[^/]+\.hip$|^vllm\.cpp/src/vt/tenstorrent/|^vllm\.cpp/src/vt/vulkan/shaders/' - native_inventory() { - local base=$1 - local member - shift - for member in "$@"; do - find "$base/$member" -type f -print - done | sed "s#^$base/##" | grep -Ev "$native_exclude_pattern" | LC_ALL=C sort - } - native_members=( - vllm.cpp/CMakeLists.txt - vllm.cpp/LICENSE - vllm.cpp/NOTICE - vllm.cpp/cmake - vllm.cpp/include - vllm.cpp/src - vllm.cpp/scripts/triton-aot-compile.py - vllm.cpp/tests/vt/test_rocm_backend.cpp - vllm.cpp/triton_kernels - vllm.cpp/third_party/README.md - vllm.cpp/third_party/blake3 - vllm.cpp/third_party/doctest/doctest.h - vllm.cpp/third_party/minja - vllm.cpp/third_party/nlohmann - vllm.cpp/third_party/vulkan - ) - sys_expected="$temp/vllm-cpp-sys.expected" - { - printf '%s\n' \ - .cargo_vcs_info.json \ - Cargo.lock \ - Cargo.toml \ - Cargo.toml.orig \ - LICENSE-APACHE \ - LICENSE-MIT \ - NOTICE \ - README.md \ - THIRD_PARTY.md \ - build.rs \ - licenses/DOCTEST-MIT.txt \ - licenses/FLASH-ATTENTION-BSD-3-CLAUSE.txt \ - licenses/FLASH-LINEAR-ATTENTION-MIT.txt \ - src/bindings.rs \ - src/build_config.rs \ - src/build_support.rs \ - src/lib.rs \ - tests/build_config.rs \ - tests/build_support.rs \ - tests/layout.c \ - tests/layout.rs \ - tests/symbols.rs \ - wrapper.h - native_inventory "$repo_root/vllm-cpp-sys" "${native_members[@]}" - } | LC_ALL=C sort > "$sys_expected" - diff -u "$sys_expected" "$sys_list" - diff -u \ - <(native_inventory "$repo_root/vllm-cpp-sys" "${native_members[@]}") \ - <(native_inventory "$sys_root" "${native_members[@]}") - diff -u \ - <(printf '%s\n' CMakeLists.txt LICENSE NOTICE cmake include scripts src tests third_party triton_kernels | LC_ALL=C sort) \ - <(find "$sys_root/vllm.cpp" -mindepth 1 -maxdepth 1 -printf '%f\n' | LC_ALL=C sort) - - safe_expected="$temp/vllm-cpp.expected" - printf '%s\n' \ - .cargo_vcs_info.json \ - Cargo.lock \ - Cargo.toml \ - Cargo.toml.orig \ - LICENSE-APACHE \ - LICENSE-MIT \ - README.md \ - examples/README.md \ - examples/chat.rs \ - examples/common/mod.rs \ - examples/complete.rs \ - examples/concurrent.rs \ - examples/setup_test_model.rs \ - examples/stream.rs \ - examples/structured.rs \ - examples/video_generate.rs \ - src/abi.rs \ - src/callback.rs \ - src/engine.rs \ - src/error.rs \ - src/hf.rs \ - src/lib.rs \ - src/params.rs \ - src/request.rs \ - tests/qwen3.rs \ - tests/safe_api.rs \ - | LC_ALL=C sort > "$safe_expected" - diff -u "$safe_expected" "$safe_list" - - required_sys_members=( - README.md - THIRD_PARTY.md - licenses/DOCTEST-MIT.txt - licenses/FLASH-ATTENTION-BSD-3-CLAUSE.txt - licenses/FLASH-LINEAR-ATTENTION-MIT.txt - vllm.cpp/include/vllm.h - vllm.cpp/src/capi/chat_prompt.cpp - vllm.cpp/src/capi/chat_prompt.h - vllm.cpp/src/capi/engine_handle.h - vllm.cpp/src/capi/vllm_c.cpp - vllm.cpp/src/vllm/version.cpp - vllm.cpp/src/vt/cuda/cuda_matmul_fp8_cutlass.cu - vllm.cpp/src/vt/cuda/flash_attn/src/flash.h - vllm.cpp/src/vt/cuda/marlin/core/scalar_type.hpp - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_80/MANIFEST - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_86/MANIFEST - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_89/MANIFEST - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_90a/MANIFEST - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_100a/MANIFEST - vllm.cpp/src/vt/cuda/triton_aot_vendored/sm_121a/MANIFEST - vllm.cpp/src/vt/metal/metal_mlx_provider.mm - vllm.cpp/src/vt/vulkan/vulkan_spirv.h - vllm.cpp/src/vllm/platforms/rocm.cpp - vllm.cpp/include/vt/rocm/rocm_arch.h - vllm.cpp/include/vt/rocm/rocm_runtime.h - vllm.cpp/tests/vt/test_rocm_backend.cpp - vllm.cpp/scripts/triton-aot-compile.py - vllm.cpp/triton_kernels/chunk_delta_h.py - vllm.cpp/third_party/README.md - vllm.cpp/third_party/blake3/LICENSE_A2 - vllm.cpp/third_party/blake3/LICENSE_CC0 - vllm.cpp/third_party/doctest/doctest.h - vllm.cpp/third_party/minja/LICENSE - vllm.cpp/third_party/nlohmann/json.hpp - vllm.cpp/third_party/vulkan/vulkan_core.h - ) - for member in "${required_sys_members[@]}"; do + # Cargo manifests own the inventory; keep only build-entry-point checks here. + for member in build.rs wrapper.h src/lib.rs src/bindings.rs \ + vllm.cpp/CMakeLists.txt vllm.cpp/include/vllm.h vllm.cpp/src/capi/vllm_c.cpp; do [[ -s $sys_root/$member ]] || { - echo "sys package is missing required member: $member" >&2 + echo "sys package is missing essential build input: $member" >&2 exit 1 } done + # Every packaged native input must match the pinned source, without mirroring + # Cargo's exclusions or maintaining a second complete source inventory. while IFS= read -r member; do - [[ -s $safe_root/$member ]] || { - echo "safe package is missing required member: $member" >&2 - exit 1 - } - done < "$safe_expected" - - denied_sys_members=( - Justfile - layout.c - vllm.cpp/.agents - vllm.cpp/.git - vllm.cpp/.github - vllm.cpp/assets - vllm.cpp/benchmarks - vllm.cpp/docs - vllm.cpp/examples - vllm.cpp/tools - vllm.cpp/third_party/httplib - vllm.cpp/cmake/CudaArchFeaturesTest.cmake - vllm.cpp/cmake/CudaSourceGencodeTest.cmake - vllm.cpp/cmake/DumpTritonAOTContract.cmake - vllm.cpp/cmake/InSourceGuardTest.cmake - vllm.cpp/cmake/TritonAOTDefaultTest.cmake - vllm.cpp/cmake/TritonAOTMultiArchTest.cmake - vllm.cpp/cmake/VerifyExports.cmake - vllm.cpp/src/vllm/entrypoints/openai/api_server.cpp - vllm.cpp/src/vllm/entrypoints/openai/server_main.cpp - vllm.cpp/src/vllm/platforms/tenstorrent.cpp - vllm.cpp/src/vt/cuda/marlin/libtorch_stable/moe/marlin_moe_wna16/generate_kernels.py - vllm.cpp/src/vt/rocm - vllm.cpp/src/vt/tenstorrent - vllm.cpp/src/vt/vulkan/shaders - ) - for member in "${denied_sys_members[@]}"; do - [[ ! -e $sys_root/$member ]] || { - echo "denied upstream tree or record leaked into sys package: $member" >&2 - exit 1 - } - done - diff -u \ - <(printf '%s\n' vllm.cpp/tests/vt/test_rocm_backend.cpp) \ - <(find "$sys_root/vllm.cpp/tests" -type f -printf '%P\n' \ - | sed 's#^#vllm.cpp/tests/#' | LC_ALL=C sort) - diff -u \ - <(printf '%s\n' vllm.cpp/third_party/doctest/doctest.h) \ - <(find "$sys_root/vllm.cpp/third_party/doctest" -type f -printf '%P\n' \ - | sed 's#^#vllm.cpp/third_party/doctest/#' | LC_ALL=C sort) + case $member in + vllm.cpp/*) cmp "$repo_root/vllm-cpp-sys/$member" "$sys_root/$member" ;; + esac + done < "$sys_list" - forbidden_pattern='(^|/)(target|stuff|\.git|\.github|__pycache__|\.cache|cache|fixtures?|downloads?|_deps|sdk)(/|$)|(^|/)(cutlass)(/|$)|(^|/)(model\.safetensors|tokenizer\.json|tokenizer_config\.json)$|\.(o|obj|a|so|dylib|dll|pyc|safetensors|gguf|pt|pth)$' + forbidden_pattern='(^|/)(target|stuff|\.git|\.github|\.agents|\.qmt|hardening-evidence|cleanup-evidence|__pycache__|\.cache|cache|fixtures?|downloads?|_deps|sdk)(/|$)|(^|/)(cutlass)(/|$)|(^|/)(AGENTS\.md|model\.safetensors|tokenizer\.json|tokenizer_config\.json)$|\.(o|obj|a|so|dylib|dll|pyc|safetensors|gguf|pt|pth)$' for listing in "$sys_list" "$safe_list"; do if grep -Eiq "$forbidden_pattern" "$listing"; then echo "forbidden package payload detected in $listing:" >&2 @@ -697,6 +531,13 @@ package-test: | grep -Ei '(^|/)(license[^/]*|copying[^/]*|notice[^/]*)$' \ | LC_ALL=C sort) + while IFS= read -r member; do + cmp "$repo_root/vllm-cpp-sys/$member" "$sys_root/$member" + done < "$sys_license_actual" + for member in LICENSE-APACHE LICENSE-MIT; do + cmp "$repo_root/vllm-cpp/$member" "$safe_root/$member" + done + check_package_links() { local package_root=$1 shift @@ -728,17 +569,8 @@ package-test: safe_package_sha256=$(sha256sum "$safe_package" | awk '{ print $1 }') safe_entry_count=$(wc -l < "$safe_list") - jq -e --arg version "$version" ' - .packages | length == 1 - and .[0].name == "vllm-cpp-sys" - and .[0].version == $version - and .[0].readme == "README.md" - and .[0].documentation == "https://docs.rs/vllm-cpp-sys" - and .[0].license == "MIT OR Apache-2.0" - and .[0].rust_version == "1.85" - and .[0].features.default == ["bundled"] - ' <(cargo metadata --manifest-path "$sys_root/Cargo.toml" \ - --locked --offline --no-deps --format-version 1) >/dev/null + check_package_metadata <(cargo metadata --manifest-path "$sys_root/Cargo.toml" \ + --locked --offline --no-deps --format-version 1) just --justfile "$repo_root/Justfile" _link-modes \ "$sys_root" "$temp/extracted-link-modes" @@ -793,22 +625,8 @@ package-test: CARGO_NET_OFFLINE=true CARGO_TARGET_DIR="$temp_target" \ cargo test --locked --release --offline --features bundled,serde ) - jq -e --arg version "$safe_version" ' - .packages | length == 1 - and .[0].name == "vllm-cpp" - and .[0].version == $version - and .[0].readme == "README.md" - and .[0].documentation == "https://docs.rs/vllm-cpp" - and .[0].license == "MIT OR Apache-2.0" - and .[0].rust_version == "1.85" - and .[0].features.default == ["bundled"] - and .[0].features.serde == [] - and (.[0].dependencies | map(select(.name == "vllm-cpp-sys" and .req == ("=" + $version) and .uses_default_features == false)) | length == 1) - and (.[0].dependencies | map(select(.name == "hf-hub" and .req == "^0.5.0" and .optional == false and .uses_default_features == false and .features == ["ureq"])) | length == 1) - and (.[0].dependencies | map(select(.name == "serde_json" and .optional == false and .kind == null)) | length == 1) - and (.[0].dependencies | map(select(.name == "clap" and .req == "=4.6.1" and .kind == "dev" and .optional == false and .features == ["derive"])) | length == 1) - ' <(cargo metadata --manifest-path "$safe_root/Cargo.toml" \ - --locked --offline --no-deps --format-version 1) >/dev/null + check_package_metadata <(cargo metadata --manifest-path "$safe_root/Cargo.toml" \ + --locked --offline --no-deps --format-version 1) safe_consumer="$temp/safe-consumer" mkdir -p "$safe_consumer/src" @@ -1117,10 +935,6 @@ tsan model=env_var_or_default("VLLM_CPP_TEST_MODEL", ""): just --justfile {{ quote(root + "/Justfile") }} _assert-rust-tests "$output" "$test" true done -# Test release evidence helpers without native builds or models. -tooling-test: - python3 {{ quote(root + "/tests/release_tooling.py") }} - [private] _package-payload archive: #!/usr/bin/env bash @@ -1195,4 +1009,4 @@ docs: RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --features vllm-cpp/serde # Run the complete maintainer gate serially; do not pass --jobs. -ci: fmt-check tooling-test lint docs test sys link-modes native-capi sanitizers-model-free package-test publish-dry-run +ci: fmt-check lint docs test sys link-modes native-capi sanitizers-model-free package-test publish-dry-run diff --git a/README.md b/README.md index d8207b2..fc72b78 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Rust bindings for [vllm.cpp](https://github.com/mudler/vllm.cpp), organized as: > **Work in progress:** these bindings track a pinned vllm.cpp release and may lag later upstream APIs or backend behavior. Check the exact native identity and support boundary before adoption. -The safe crate covers text completion, streaming, raw-JSON and optional serde chat, concurrent requests, structured output, custom logits processing, pre-tokenized completion, transcription, embeddings, video generation, and standalone video-mux argument composition. It also includes a synchronous Hugging Face resolver and text-focused examples. The sys crate exposes the complete 35-function stable C boundary at ABI 17 with checked-in generated bindings and C/Rust conformance checks. +The safe crate covers text completion, streaming, raw-JSON and optional serde chat, concurrent requests, structured output, custom logits processing, pre-tokenized completion, transcription, embeddings, video generation, and standalone video-mux argument composition. It also includes a synchronous Hugging Face resolver, text examples, and a minimal local-checkpoint video example. The sys crate exposes the complete 35-function stable C boundary at ABI 17 with checked-in generated bindings and C/Rust conformance checks. The native source is independently pinned to vllm.cpp tag `v0.0.2`, commit `7020de93652ca920424a10ac5255b34810dd2f24`. Native Linux x86_64 CPU is the supported runtime target, including bundled/system and static/dynamic linking. Linux ARM64, Apple ARM64, CUDA, external CUTLASS, Triton AOT, Vulkan, Metal, and external MLX are configured build or optional validation surfaces; they are not accelerator runtime-support claims. @@ -57,11 +57,11 @@ Text `Engine` is a cloneable `Send + Sync` RAII owner. It provides blocking comp `VideoEngine` separately owns a MiniMax-H3 checkpoint set and performs exclusive blocking generation. Video device numbering is `Cpu=0` and `Cuda=1`, with no `Auto`; explicit CUDA never falls back. Generation writes frame/audio artifacts, may leave stale or partial output, and trusts caller paths without sandboxing. `VideoMuxParams` and `VideoMuxArgv` only compose ordered `OsString` argument boundaries. This crate never executes ffmpeg, an HTTP server, or another process; `vllm_server_main` remains available only through the raw crate. -The high-level crate intentionally adds no tokenizer, task-query, raw-handle, process-execution, or HTTP-server wrapper. See [the examples guide](vllm-cpp/examples/README.md) for the intentionally text-focused binaries. Release-facing changes are recorded in the [changelog](CHANGELOG.md), and maintainers use the manual [release process](RELEASING.md). +The high-level crate intentionally adds no tokenizer, task-query, raw-handle, process-execution, or HTTP-server wrapper. See [the examples guide](vllm-cpp/examples/README.md) for text binaries and the experimental local-checkpoint video example. Release-facing changes are recorded in the [changelog](CHANGELOG.md), and maintainers use the manual [release process](RELEASING.md). ## Build and Test -Inside the Nix shell, use the root `Justfile` (requires Just 1.40 or newer) for maintainer workflows: +Use the root `Justfile` (requires Just 1.40 or newer) for maintainer workflows, optionally inside the Nix shell: ```console CMAKE_GENERATOR=Ninja cargo build --locked --release @@ -131,21 +131,13 @@ cargo package -p vllm-cpp --locked --list just package-test ``` -The package gate reports file counts, compressed sizes, payload bytes, and SHA-256 hashes as diagnostics only; neither crate has an arbitrary numeric package size or file-count cap. Payload bytes are the sum of regular archive member sizes, not filesystem-dependent extracted directory sizes. It validates deterministic inventories for both crates, package metadata, required source/docs/examples/tests/licenses/native inputs, forbidden payloads, and license provenance. It extracts and tests both crates offline, then runs independent sys and safe downstream consumers; the safe consumer resolves both extracted crates rather than this workspace. The sys package carries only native build inputs and required licenses/notices; upstream tests, large fixtures, media, benchmarks, fetched SDKs, external CUTLASS trees, and agent records are excluded. - -`just publish-dry-run` performs a sys-then-safe workspace packaging dry-run without uploading; it uses `--no-verify` to avoid the pre-publication registry cycle. As required by [RELEASING.md](RELEASING.md), after `vllm-cpp-sys` is available from crates.io, run the full `cargo publish -p vllm-cpp --locked --dry-run` verification before publishing the safe crate. - -## Validation Boundary - -Mandatory candidate evidence is Linux x86_64 CPU and model-free: formatting, release-tooling regressions, lint, docs, workspace tests, generated-binding/header/layout/signature/ABI/exact-export checks, all four CPU link modes, native C API fixtures, ASan/UBSan/leak checks over committed fixtures, package extraction/downstream checks, publication dry-run, and exact MSRV validation. - -`just native-capi` preserves the default scheduler result with `VT_ASYNC_SCHED` and `VT_ASYNC_RUNNER` unset. A failing default fixture blocks the candidate; a separate synchronous diagnostic run cannot make it green. Callback cardinality is not token cardinality. Dirty-tree development passes are not a substitute for independently approved clean-commit validation in [RELEASING.md](RELEASING.md). - -Prepared Qwen inference/sanitizers, native-only TSan, successful Rust MiniMax-H3 generation, Miri, Linux ARM64, Apple ARM64, Vulkan, CUDA/CUTLASS/Triton, Metal/MLX, and accelerator runtime are optional or deferred lanes. The manual platform workflows are configuration, not exact-candidate evidence unless separately dispatched and recorded. +Cargo manifests define package contents. The gate compares archives with `cargo package --list`, checks metadata, essential native inputs, licenses and forbidden payloads, then runs offline extracted tests, four extracted sys link modes, and independent sys and safe downstream consumers. File counts, compressed sizes, regular-file payload bytes and SHA-256 hashes are diagnostics only, with no arbitrary caps. ## Support -Native Linux x86_64 CPU is the supported runtime family. All accelerator features remain experimental build/configuration surfaces. Cross-platform compile or workflow configuration alone does not establish runtime support. +Native Linux x86_64 CPU is the supported runtime family. `just ci` runs the mandatory model-free gates; the exact MSRV check is separate. See [RELEASING.md](RELEASING.md) for the complete checklist and sys-first publication process, including full registry-resolved safe dry-run verification. + +Model-free tests do not establish prepared-model inference or successful MiniMax-H3 generation. Accelerator runtime, other platforms, prepared-model sanitizers, TSan and Miri remain optional or deferred. Configured workflows and successful compilation alone are not runtime-support evidence. ## Licensing and Affiliation diff --git a/RELEASING.md b/RELEASING.md index 86e0059..8a3bb0b 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,206 +1,86 @@ # Releasing -Releases are prepared and published manually. A candidate pass or dry-run does not authorize a Git tag, crates.io upload, merge, push, or GitHub release. Publication is an irreversible registry action and requires separate maintainer authorization. - -Uncommitted development runs may exercise the gates below, but must be reported as dirty-tree validation, not approved release proof. Formal release validation and publication require the independently approved exact commit and clean identities below. - -## Prepare a candidate - -1. Obtain `VLLM_CPP_RELEASE_COMMIT` from the independent approval of the exact release commit. Never derive the expected value from the current checkout. Require a lowercase, full 40-character SHA, verify that it names a commit object, and compare it with the root checkout before any candidate gate: - - ```bash - set -euo pipefail - : "${VLLM_CPP_RELEASE_COMMIT:?set this to the independently approved release commit}" - if [[ ! $VLLM_CPP_RELEASE_COMMIT =~ ^[0-9a-f]{40}$ ]]; then - echo 'VLLM_CPP_RELEASE_COMMIT must be exactly 40 lowercase hexadecimal characters' >&2 - exit 1 - fi - git cat-file -e "$VLLM_CPP_RELEASE_COMMIT^{commit}" - actual_release_commit=$(git rev-parse HEAD) - if [[ $actual_release_commit != "$VLLM_CPP_RELEASE_COMMIT" ]]; then - printf 'reviewed release commit mismatch: expected %s, found %s\n' \ - "$VLLM_CPP_RELEASE_COMMIT" "$actual_release_commit" >&2 - exit 1 - fi - ``` - - Preserve the externally supplied value and rerun this guard immediately before each separately authorized upload. - -2. Require a clean root worktree and index and a clean, detached native submodule: - - ```console - test -z "$(git status --short --untracked-files=all)" - git diff --quiet - git diff --cached --quiet - git submodule status --recursive - test "$(git -C vllm-cpp-sys/vllm.cpp symbolic-ref -q HEAD || true)" = "" - test -z "$(git -C vllm-cpp-sys/vllm.cpp status --short --untracked-files=all)" - ``` - -3. Verify native identity directly, never with `git describe`: - - ```console - native=vllm-cpp-sys/vllm.cpp - test "$(git rev-parse HEAD:vllm-cpp-sys/vllm.cpp)" = 7020de93652ca920424a10ac5255b34810dd2f24 - test "$(git -C "$native" rev-parse HEAD)" = 7020de93652ca920424a10ac5255b34810dd2f24 - test "$(git -C "$native" rev-parse 'refs/tags/v0.0.2^{}')" = 7020de93652ca920424a10ac5255b34810dd2f24 - test "$(git -C "$native" rev-parse 'HEAD^{tree}')" = 28df226f0ef9924e67d563c3bef4712d0e628c5a - ``` - -4. Use `cargo metadata --locked --no-deps --format-version 1` and normalized package manifests to require both Rust crates at `0.0.2` and the safe dependency requirement exactly `=0.0.2`. Confirm both local package records in `Cargo.lock`. Separately parse native `project(vllm_cpp VERSION 0.0.2 LANGUAGES CXX)` from `CMakeLists.txt`. Rust and native versions are independent release identities that happen to both be `0.0.2` here; equality is not a universal policy. -5. Require `VLLM_ABI_VERSION == 17` in the pinned header and generated bindings and exactly 35 stable C functions. Run binding-drift, C11/C++20 header, every C/Rust layout and signature, runtime ABI, all-function link, and exact dynamic-export checks. ABI-10 system libraries are incompatible. -6. Keep `Unreleased` empty above the dated release entry. Describe only validated support and preserve known limitations. -7. Audit root and crate dual-license metadata, `LICENSE-MIT`, `LICENSE-APACHE`, native `LICENSE`/`NOTICE`, `THIRD_PARTY.md`, and every imported license text against the exact package inventories. Reject models, media fixtures, build output, caches, SDKs, external CUTLASS trees, internal records, and repository-local paths. +Releases are manual. Review the candidate from a clean checkout before publishing; +validation and dry-runs do not upload crates or create tags. + +## Prepare + +- Check `git status --short` and `git submodule status --recursive`, including a + clean native worktree. Confirm the gitlink, native HEAD, and peeled `v0.0.2` tag + all name `7020de93652ca920424a10ac5255b34810dd2f24`: + + ```console + git rev-parse HEAD:vllm-cpp-sys/vllm.cpp + git -C vllm-cpp-sys/vllm.cpp rev-parse HEAD 'refs/tags/v0.0.2^{}' + ``` + +- Confirm both Rust crates are `0.0.2`, with exactly `vllm-cpp-sys =0.0.2` in the + safe crate, and native CMake reports `0.0.2`. Check manifests and lockfiles. +- Preserve ABI 17 and all 35 stable C functions: generated bindings, C11/C++20 + headers, layouts, signatures, runtime ABI, linking, and dynamic exports must + agree. ABI-10 system libraries are incompatible. +- Keep `Unreleased` empty above the dated changelog entry. Describe only tested + support: Linux x86_64 CPU is supported; accelerator backends remain experimental. +- Review packaged licenses/notices and `vllm-cpp-sys/THIRD_PARTY.md`, including + imported license texts. Packages must not contain models, media fixtures, build + output, caches, SDKs, external CUTLASS trees, internal records, or local paths. + Cargo manifests define package inclusion and exclusion. ## Validate -Run the mandatory Linux x86_64 CPU gates directly with the native Cargo and Just workflows: +Use the normal Rust/native build tools and Just 1.40 or newer: ```console -env -u VLLM_CPP_TEST_MODEL just ci +just ci RUSTUP_TOOLCHAIN=1.85.0 just msrv -# Equivalent exact-toolchain invocation: rustup run 1.85.0 just msrv -cargo check --locked --workspace --all-targets --features vllm-cpp/serde -RUSTDOCFLAGS='-D warnings' cargo doc --locked --workspace --no-deps --features vllm-cpp/serde git diff --check ``` -`just ci` includes formatting, deterministic release-tooling regressions, warnings-denied lint/docs, model-free workspace tests, generated bindings and ABI conformance, four CPU link modes, the native C API fixture, model-free ASan/UBSan/leak checks, package/extracted/downstream validation, and no-upload publish dry-run. Run the exact MSRV gate separately so stable-toolchain success cannot mask it. - -The native fixture gate always runs with `VT_ASYNC_SCHED` and `VT_ASYNC_RUNNER` unset and preserves any default-run failure. No assertion is waived or excluded. A separate `VT_ASYNC_SCHED=0` run may help diagnose callback coalescing, but cannot satisfy the default gate or establish token loss from callback cardinality alone. Record default and diagnostic commands/statuses independently; a default failure is a candidate blocker. If `just ci` stops, run the remaining recipes separately to expose other blockers, not to relabel the aggregate as passed. - -Nix support remains an optional convenience, not a prerequisite. Maintainers who choose it may run `nix develop --no-write-lock-file -c env -u VLLM_CPP_TEST_MODEL just ci` and `nix develop .#msrv --no-write-lock-file -c just msrv`; `nix flake check --no-build` is an additional Nix-specific evaluation check, not an ordinary release gate. - -Prepared-Qwen inference/sanitizers, native-only TSan, successful Rust MiniMax-H3 generation, Miri, Linux ARM64, Apple ARM64, Vulkan, CUDA/CUTLASS/Triton, Metal/MLX, and accelerator runtime are optional or deferred. Record one only when it ran against the exact candidate; configured workflows and older results are not candidate evidence. Sanitizer recipes select one package/target test artifact from Cargo JSON rather than target-directory timestamps. Full suites must report matching nonzero running/passed counts, and every exact TSan filter must report one named passed test; a missing filter is a failure, not coverage. - -## Inspect packages - -Use two fresh, separate `CARGO_TARGET_DIR` values; do not trust existing `target/package` files: - -```console -CARGO_NET_OFFLINE=true CARGO_TARGET_DIR=target/package-release-a just package-test -CARGO_NET_OFFLINE=true CARGO_TARGET_DIR=target/package-release-b just package-test -just publish-dry-run -``` - -For both runs, retain: - -- sorted `cargo package --list` inventories and archive basenames; -- normalized `Cargo.toml` manifests, including exact `vllm-cpp-sys =0.0.2`; -- file counts and payload/compressed sizes as diagnostics only, with no arbitrary numeric package size or file-count caps for either crate; payload means the sum of regular archive member sizes for both crates, excluding directory metadata and archive headers, not `du` of the extracted tree; exact inventory, content, provenance, and compatibility checks remain mandatory; -- complete license/notice/provenance inventories and checks that current READMEs, source, tests, examples, and required native/backend inputs are present; -- SHA-256 for both archives from each run and successful extracted/offline builds, all four extracted sys link modes, and independent sys and safe downstream consumers. - -Require identical sorted inventories and semantically identical normalized manifests between runs. Compare archive hashes and record both outcomes, but do not assume byte identity: Cargo-generated `.cargo_vcs_info.json`, archive metadata, or timestamps may differ. Claim reproducible bytes only when both hashes actually match and the comparison explains the metadata involved. - -`just publish-dry-run` uses Cargo's sys-first workspace order with `--no-verify` and never uploads. It cannot provide the safe crate's full registry-resolution verification before exact sys `0.0.2` is available from crates.io. Check both crate-version slots are available before any future upload; do not reserve or publish them during candidate preparation. - -## Inspect registry state - -Use the exact-version crates.io API read-only. This helper classifies only HTTP 200 with matching crate/version JSON as `accepted` and only HTTP 404 as `absent`; network errors, redirects that do not finish in either status, other HTTP statuses, and malformed or mismatched JSON are `ambiguous`. Stop on `ambiguous`. - -```bash -set -euo pipefail -command -v curl >/dev/null -command -v jq >/dev/null -registry_tmp=$(mktemp -d) -trap 'rm -rf "$registry_tmp"' EXIT HUP INT TERM - -registry_state() { - local crate=$1 - local version=$2 - local body status - if [[ ! $crate =~ ^[a-z0-9][a-z0-9_-]*$ || ! $version =~ ^[0-9A-Za-z.+-]+$ ]]; then - printf '%s\n' ambiguous - return - fi - if ! body=$(mktemp "$registry_tmp/response.XXXXXX"); then - printf '%s\n' ambiguous - return - fi - if ! status=$(curl --silent --show-error --location \ - --connect-timeout 10 --max-time 30 --retry 0 \ - --output "$body" --write-out '%{http_code}' -- \ - "https://crates.io/api/v1/crates/$crate/$version"); then - printf '%s\n' ambiguous - return - fi - case $status in - 200) - if jq -e --arg crate "$crate" --arg version "$version" \ - '.version.crate == $crate and .version.num == $version' \ - "$body" >/dev/null 2>&1; then - printf '%s\n' accepted - else - printf '%s\n' ambiguous - fi - ;; - 404) printf '%s\n' absent ;; - *) printf '%s\n' ambiguous ;; - esac -} - -sys_state=$(registry_state vllm-cpp-sys 0.0.2) -safe_state=$(registry_state vllm-cpp 0.0.2) -rm -rf "$registry_tmp" -trap - EXIT HUP INT TERM -printf 'vllm-cpp-sys 0.0.2: %s\nvllm-cpp 0.0.2: %s\n' \ - "$sys_state" "$safe_state" -``` - -The helper performs no registry mutation. Preserve its output with the release evidence. Before an initial sys upload, both states must be `absent`. Before any safe upload or retry, sys must be `accepted` and safe must be `absent`. If safe is already `accepted`, never republish it. Any other combination requires stopping for diagnosis. +`just ci` runs formatting, warnings-denied lint/docs, model-free workspace tests, +bindings/header/layout/signature/ABI/export +checks, four workspace CPU link modes, native C API fixtures, ASan/UBSan/leak +checks, package verification, and a no-upload publish dry-run. The MSRV gate +separately requires Rust and Cargo exactly 1.85.0. + +Run the package gate with a fresh target directory (the CI package run may serve +as this run). It compares archives with `cargo package --list`, checks metadata, +source inputs, licenses and leaks, then runs offline extracted tests, four +extracted sys link modes, and independent sys and safe downstream consumers. +File counts, payload/compressed sizes and SHA-256 hashes are diagnostics, not +caps. A second run is optional; claim byte reproducibility only if compared +archive hashes actually match. + +The native fixture gate unsets `VT_ASYNC_SCHED` and `VT_ASYNC_RUNNER`. A default +failure remains a failure; alternate-scheduler diagnostics cannot satisfy it. +Record any failed gate rather than treating later individual passes as a CI pass. + +Nix is optional: use `nix develop --no-write-lock-file -c just ci` and +`nix develop .#msrv --no-write-lock-file -c just msrv` instead if desired. +Prepared-Qwen inference/sanitizers, native-only TSan, Rust MiniMax-H3 generation, +Miri, other platforms, and accelerator runtime are optional or deferred lanes. +Report only what actually ran; model-free passes do not establish model inference. ## Publish -Only a separately authorized maintainer may publish from the exact reviewed commit with a clean root and detached submodule. Rerun the reviewed-root guard immediately before each upload. Before any safe upload, set and validate the independently approved `VLLM_CPP_SAFE_ARCHIVE_SHA256` as described below. Publish sys first: +Check crates.io for the exact versions before uploading. An unavailable or +ambiguous registry state needs diagnosis, not a trial upload. Publish sys first, +wait for registry availability, then run the **full** safe dry-run against registry +sys `0.0.2` before uploading the safe crate: ```console cargo publish -p vllm-cpp-sys --locked -# Wait until the exact-version helper reports sys accepted and safe absent. cargo publish -p vllm-cpp --locked --dry-run cargo publish -p vllm-cpp --locked ``` -The full safe dry-run must resolve registry sys `0.0.2` before the safe upload. After every successful safe upload, including retries, run the registry-download hash check below before continuing: Cargo repackages during publication, so the local archive check alone does not prove which bytes were uploaded. Then verify registry metadata, docs.rs, licenses, and a clean downstream build. Create a tag and GitHub release only after separate authorization and only for the exact published commit. - -### Retry or verify the safe crate - -A safe upload retry is allowed only for the same independently approved archive bytes. Obtain the approved hash externally; never copy a dirty-candidate hash into this document or derive the expected value from a rebuilt archive. - -```bash -set -euo pipefail -: "${VLLM_CPP_SAFE_ARCHIVE_SHA256:?set this to the approved safe archive SHA-256}" -if [[ ! $VLLM_CPP_SAFE_ARCHIVE_SHA256 =~ ^[0-9a-f]{64}$ ]]; then - echo 'VLLM_CPP_SAFE_ARCHIVE_SHA256 must be exactly 64 lowercase hexadecimal characters' >&2 - exit 1 -fi -safe_archive=target/package/vllm-cpp-0.0.2.crate -actual_safe_sha256=$(sha256sum "$safe_archive" | awk '{print $1}') -if [[ $actual_safe_sha256 != "$VLLM_CPP_SAFE_ARCHIVE_SHA256" ]]; then - echo 'safe archive differs from the approved bytes; prepare a new coordinated version' >&2 - exit 1 -fi -``` - -Rerun the registry helper after this check. Retry only when `sys_state=accepted` and `safe_state=absent`, and only after separate upload authorization. `ambiguous` means stop. If `safe_state=accepted`, do not upload; instead verify the accepted archive against the same approved hash before post-publication checks: - -```bash -set -euo pipefail -accepted_safe=$(mktemp) -trap 'rm -f "$accepted_safe"' EXIT HUP INT TERM -curl --fail --silent --show-error --location --connect-timeout 10 --max-time 60 \ - --retry 0 --output "$accepted_safe" -- \ - 'https://crates.io/api/v1/crates/vllm-cpp/0.0.2/download' -printf '%s %s\n' "$VLLM_CPP_SAFE_ARCHIVE_SHA256" "$accepted_safe" \ - | sha256sum --check --strict -``` - -A registry archive mismatch or any need to change the safe bytes requires a new coordinated version; never reuse `0.0.2` for different bytes. - -## Abort and recovery - -- Before upload, abort on any mismatch, failed gate, unexpected file, dirty state, changed lockfile, native identity/ABI/export drift, inaccurate support statement, or unavailable version. Fix it in a separately reviewed commit and restart. -- Accepted crates.io versions cannot be replaced or deleted. Never rebuild different bytes under the same version. -- If sys publishes but safe fails, stop and diagnose. Retry unchanged safe bytes only for a transient failure; otherwise prepare a new coordinated version. -- Yank only when leaving a version selectable would harm users. Yanking does not erase the crate or make the version reusable. -- Never use `cargo yank` as an ordinary abort mechanism or publish merely to test credentials or packaging. +The pre-publication `just publish-dry-run` uses `--no-verify` to avoid the registry +cycle; it does not replace that full safe dry-run. After publication, verify +registry metadata, docs.rs, licenses, and a clean downstream build before tagging +the published commit and creating a release. + +Accepted versions are immutable: never republish or replace them with different +bytes. If sys succeeds but safe fails, check registry state before retrying; +retry unchanged safe contents only for a transient failure, otherwise prepare a +new coordinated version. Stop on identity drift, failed gates, dirty release +state, or unexpected package content. Yanking does not erase a crate or make its +version reusable and is not an ordinary abort mechanism. diff --git a/tests/release_tooling.py b/tests/release_tooling.py deleted file mode 100644 index 9b1956b..0000000 --- a/tests/release_tooling.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Deterministic tests of release recipes; no native build or model required.""" - -import io -import json -import os -from pathlib import Path -import subprocess -import sys -import tarfile -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parents[1] -PACKAGE = "path+file:///fixture/vllm-cpp#0.0.2" - - -class ReleaseTooling(unittest.TestCase): - def setUp(self): - self.temp = tempfile.TemporaryDirectory(prefix="vllm-release-tooling-") - self.work = Path(self.temp.name) - self.addCleanup(self.temp.cleanup) - - def recipe(self, *args, env=None): - return subprocess.run( - ["just", "--justfile", str(ROOT / "Justfile"), *map(str, args)], - cwd=ROOT, env=env, text=True, capture_output=True, timeout=15, - ) - - def artifact(self, executable, package=PACKAGE, target="qwen3", kind="test"): - return { - "reason": "compiler-artifact", "package_id": package, - "target": {"name": target, "kind": [kind]}, - "profile": {"test": True}, "executable": str(executable), - } - - def select(self, messages): - path = self.work / "messages.json" - path.write_text("".join(json.dumps(message) + "\n" for message in messages)) - return self.recipe("_test-binary", path, PACKAGE, "qwen3", "test") - - def test_artifact_selection_ignores_newer_stale_and_foreign_executables(self): - current = self.work / "qwen3-current" - stale = self.work / "qwen3-stale" - for path in [current, stale]: - path.touch(mode=0o755) - os.utime(current, (1, 1)) - os.utime(stale, (999999, 999999)) - result = self.select([ - self.artifact(stale, package="registry+other#foreign@0.0.2"), - self.artifact(stale, kind="example"), - self.artifact(stale, target="safe_api"), - self.artifact(current), - ]) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout.strip(), str(current)) - - def test_artifact_selection_rejects_missing_or_ambiguous_matches(self): - for messages in [[], [self.artifact("one"), self.artifact("two")]]: - with self.subTest(messages=messages): - self.assertNotEqual(self.select(messages).returncode, 0) - - def assert_results(self, text, focused=False, name="suite"): - path = self.work / "output.txt" - path.write_text(text) - return self.recipe("_assert-rust-tests", path, name, str(focused).lower()) - - def test_complete_suite_requires_nonzero_matching_counts(self): - self.assertEqual(self.assert_results( - "running 3 tests\ntest result: ok. 3 passed; 0 failed; 0 ignored;\n" - ).returncode, 0) - for text in [ - "running 0 tests\ntest result: ok. 0 passed; 0 failed;\n", - "running 3 tests\ntest result: ok. 1 passed; 0 failed; 2 ignored;\n", - "running 1 test\ntest result: FAILED. 0 passed; 1 failed;\n", - "running 1 test\nrunning 1 test\ntest result: ok. 1 passed; 0 failed;\n", - ]: - with self.subTest(text=text): - self.assertNotEqual(self.assert_results(text).returncode, 0) - - def test_focused_test_requires_exactly_one_named_pass(self): - text = "running 1 test\ntest lifecycle ... ok\ntest result: ok. 1 passed; 0 failed;\n" - self.assertEqual(self.assert_results(text, True, "lifecycle").returncode, 0) - native_output = text.replace("... ok", "... native diagnostic\nok") - self.assertEqual(self.assert_results(native_output, True, "lifecycle").returncode, 0) - for output in [ - "running 0 tests\ntest result: ok. 0 passed; 0 failed;\n", - text.replace("lifecycle", "different"), - text.replace("1 passed", "0 passed"), - ]: - with self.subTest(output=output): - self.assertNotEqual(self.assert_results(output, True, "lifecycle").returncode, 0) - - def test_optional_lanes_use_cargo_artifacts_and_result_checks(self): - text = (ROOT / "Justfile").read_text() - for start, end in [("sanitizers model=", "# Run selected request"), - ("tsan model=", "# Check Just")]: - recipe = text.split(start, 1)[1].split(end, 1)[0] - self.assertIn("--message-format=json", recipe) - self.assertIn("_test-binary", recipe) - self.assertIn("_assert-rust-tests", recipe) - self.assertNotIn("find ", recipe) - - def test_package_payload_ignores_directory_metadata_and_links(self): - for count in [0, 100]: - archive = self.work / f"payload-{count}.crate" - with tarfile.open(archive, "w:gz") as stream: - for number in range(count): - member = tarfile.TarInfo(f"pkg/empty-{number}/") - member.type = tarfile.DIRTYPE - stream.addfile(member) - link = tarfile.TarInfo("pkg/link") - link.type = tarfile.SYMTYPE - link.linkname = "payload" - stream.addfile(link) - member = tarfile.TarInfo("pkg/payload") - member.size = 53 - stream.addfile(member, io.BytesIO(b"x" * 53)) - result = self.recipe("_package-payload", archive) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout.strip(), "53") - - def test_package_metrics_are_diagnostics_not_release_caps(self): - package = (ROOT / "Justfile").read_text().split( - "\npackage-test:\n", 1 - )[1].split("\n# Build and run the focused native", 1)[0] - for crate in ["sys", "safe"]: - for suffix in ["package_size", "payload_size", "entry_count", "package_sha256"]: - metric = f"{crate}_{suffix}" - uses = [line.strip() for line in package.splitlines() if metric in line] - with self.subTest(metric=metric): - # Metrics may be computed and printed, never used as gate conditions. - self.assertEqual(len(uses), 2, uses) - self.assertTrue(uses[0].startswith(f"{metric}=$("), uses) - self.assertIn(f'"${metric}"', uses[1]) - self.assertIn(f"{crate} package: %d entries, %d payload bytes, " - "%d compressed bytes, sha256 %s", package) - self.assertIn(f'_package-payload "${crate}_package"', package) - - def test_package_content_and_compatibility_checks_remain_required(self): - package = (ROOT / "Justfile").read_text().split( - "\npackage-test:\n", 1 - )[1].split("\n# Build and run the focused native", 1)[0] - for check in [ - 'diff -u "$sys_list" <(archive_inventory', - 'diff -u "$safe_list" <(archive_inventory', - 'diff -u "$sys_expected" "$sys_list"', - 'diff -u "$safe_expected" "$safe_list"', - 'required_sys_members=(', 'done < "$safe_expected"', - 'denied_sys_members=(', '"$forbidden_pattern" "$listing"', - 'scan_authored_paths "$sys_root"', 'scan_authored_paths "$safe_root"', - '"$sys_license_actual"', 'licenses/DOCTEST-MIT.txt', - 'check_package_links "$sys_root" README.md THIRD_PARTY.md', - 'check_package_links "$safe_root" README.md examples/README.md', - 'cargo metadata --manifest-path "$sys_root/Cargo.toml"', - 'cargo metadata --manifest-path "$safe_root/Cargo.toml"', - '.req == ("=" + $version)', - '"$sys_root" "$temp/extracted-link-modes"', - 'cargo test --locked --release --offline --features bundled,serde', - 'cd "$sys_consumer"', 'cd "$safe_consumer"', - '.manifest_path == $safe', '.manifest_path == $sys', - ]: - with self.subTest(check=check): - self.assertIn(check, package) - self.assertEqual(package.count('cargo run --locked --release --offline'), 2) - self.assertIn("package-test publish-dry-run", - (ROOT / "Justfile").read_text().split("\nci:", 1)[1]) - - def run_native(self, status): - tools = self.work / "bin" - tools.mkdir() - cmake = tools / "cmake" - cmake.write_text("#!/usr/bin/env bash\nexit 0\n") - cmake.chmod(0o755) - ctest = tools / "ctest" - ctest.write_text(f"#!{sys.executable}\n" + '''import json, os, sys -with open(os.environ["MOCK_LOG"], "a") as log: - log.write(json.dumps({"args": sys.argv[1:], "sched": os.getenv("VT_ASYNC_SCHED"), "runner": os.getenv("VT_ASYNC_RUNNER")}) + "\\n") -if "-N" in sys.argv: - print(" Test #1: test_capi\\nTotal Tests: 1") - sys.exit(0) -if os.getenv("VT_ASYNC_SCHED") == "0" or os.environ["MOCK_STATUS"] == "0": - print("100% tests passed, 0 tests failed out of 1") - sys.exit(0) -print("""TEST CASE: capi: vllm_complete_stream early-stop tears the request down cleanly -test_capi.cpp:680: ERROR: CHECK( acc.deltas == 2 ) is NOT correct! - values: CHECK( 1 == 2 ) -[doctest] test cases: 49 | 48 passed | 1 failed | 0 skipped -[doctest] assertions: 100 | 99 passed | 1 failed | -0% tests passed, 1 tests failed out of 1 - 1 - test_capi (Failed)""") -sys.exit(int(os.environ["MOCK_STATUS"])) -''') - ctest.chmod(0o755) - log = self.work / "ctest.jsonl" - env = dict(os.environ, PATH=f"{tools}:{os.environ['PATH']}", - CARGO_TARGET_DIR=str(self.work / "target"), MOCK_LOG=str(log), - MOCK_STATUS=str(status), VT_ASYNC_SCHED="inherited", VT_ASYNC_RUNNER="inherited") - result = self.recipe("native-capi", env=env) - calls = [json.loads(line) for line in log.read_text().splitlines()] - self.assertEqual(len(calls), 2, result.stdout + result.stderr) - self.assertIsNone(calls[1]["sched"]) - self.assertIsNone(calls[1]["runner"]) - return result - - def test_native_success_uses_default_environment(self): - result = self.run_native(0) - self.assertEqual(result.returncode, 0, result.stderr) - - def test_native_failure_stays_failed_without_alternate_configuration(self): - result = self.run_native(8) - self.assertEqual(result.returncode, 8, result.stdout + result.stderr) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/vllm-cpp/README.md b/vllm-cpp/README.md index ab6f431..e9f9064 100644 --- a/vllm-cpp/README.md +++ b/vllm-cpp/README.md @@ -58,7 +58,7 @@ All native completion, chat, transcription, embedding, video, stream, argv, and ABI 17 exposes no task query. Loading `Engine`, `TranscriptionEngine`, or `EmbeddingEngine` does not inspect or infer the checkpoint task; native wrong-task `Error::InvalidArgument` diagnostics remain authoritative. Video model format, partition, task, media, and capability checks are also native authority. -Video generation creates or truncates frame/audio artifacts, may leave stale files or partial output after failure, and has no cancellation, timeout, quota, rollback, or sandbox. Paths are trusted as supplied; Rust does not canonicalize, confine, reject symlinks, or clean outputs. On Unix, paths and mux arguments preserve raw bytes through `OsString`; non-Unix native conversion requires valid UTF-8. Mux arguments remain separate process arguments and must not be shell-joined. This crate does not execute ffmpeg, a server, or any other process. +Video generation creates or truncates frame/audio artifacts, may leave stale files or partial output after failure, and has no cancellation, timeout, quota, rollback, or sandbox. Paths are trusted as supplied; Rust does not canonicalize, confine, reject symlinks, or clean outputs. The [local-checkpoint example](examples/README.md#experimental-minimax-h3-video-cli) requires a fresh output directory and demonstrates a small subset of the API without downloads or mux execution. On Unix, paths and mux arguments preserve raw bytes through `OsString`; non-Unix native conversion requires valid UTF-8. Mux arguments remain separate process arguments and must not be shell-joined. This crate does not execute ffmpeg, a server, or any other process. `SchedulerPolicy::Priority` selects the native queue. Raw and serde chat JSON may carry `priority`; direct completion, streaming, and `Request` submission currently use priority zero and tie by arrival. diff --git a/vllm-cpp/examples/README.md b/vllm-cpp/examples/README.md index 82c4824..c35d44c 100644 --- a/vllm-cpp/examples/README.md +++ b/vllm-cpp/examples/README.md @@ -9,7 +9,7 @@ Text/video examples use the safe `vllm-cpp` api. `video_generate` is experimenta | [`concurrent`](concurrent.rs) | submits two asynchronous streaming requests and waits for both | | [`chat`](chat.rs) | runs a Clap-based interactive chat with conversation history and streaming output | | [`structured`](structured.rs) | extracts a fixed weather report under a JSON Schema | -| [`video_generate`](video_generate.rs) | experimental MiniMax-H3 PPM/WAV generation with optional ffmpeg MP4 muxing; not validated end-to-end | +| [`video_generate`](video_generate.rs) | minimal local-checkpoint MiniMax-H3 PPM/WAV generation; not validated end-to-end | | [`setup_test_model`](setup_test_model.rs) | resolves the pinned Qwen test fixture for `just setup-test-model`; not a general inference CLI | The library also supports pre-tokenized completion without adding another binary: @@ -109,25 +109,28 @@ At `user>` enter `/clear` to retain the system message while clearing other hist ## experimental MiniMax-H3 video CLI -Experimental `video_generate --prompt [OPTIONS]` needs MiniMax-H3 checkpoints, ample memory/storage, and a compatible backend. See `--help`; end-to-end validation is pending. +`video_generate` demonstrates blocking generation from a local MiniMax-H3 checkpoint set. It does not download files, manage a Hub cache, execute ffmpeg, or publish an MP4. End-to-end MiniMax-H3 generation is not validated here; compatible checkpoints, ample memory/storage, and a suitable backend are the caller's responsibility. -Before running default `--device cuda`, satisfy the [CUDA prerequisites](#experimental-cuda) and export `VLLM_CPP_CUDA_ARCHITECTURES` for your GPU and `CARGO_TARGET_DIR` naming a fresh build directory: +CPU is the default and requires an F32 DiT, not quantized GGUF or BF16 weights. Supply explicit local files, including both VAE configs: ```console CMAKE_GENERATOR=Ninja \ - cargo run --locked --release -p vllm-cpp --features cuda --example video_generate -- \ - --prompt "A quiet shoreline at sunrise" --workdir minimax-h3-output + cargo run --locked --release -p vllm-cpp --example video_generate -- \ + --dit /models/minimax/dit-f32.safetensors \ + --encoder /models/minimax/encoder.gguf \ + --tokenizer /models/minimax/tokenizer.json \ + --video-vae /models/minimax/video-vae.safetensors \ + --video-vae-config /models/minimax/video-vae-config.json \ + --audio-vae /models/minimax/audio-vae.safetensors \ + --audio-vae-config /models/minimax/audio-vae-config.json \ + --prompt "A quiet shoreline at sunrise" --output-dir minimax-h3-output ``` -On Nix, prefix `cargo run` with `nix develop .#cuda -c`. Default bundled builds reject CUDA before workdir creation/locking or Hub access. With `--no-default-features --features system`, native checks determine CUDA capability. +The example defaults to `--partition fl2va`, 256x256, five requested frames and two steps; `--width`, `--height`, `--frames`, `--steps` and `--seed` expose a small generation subset. For Ref2VA, select its matching DiT and add `--partition ref2va --reference-image /path/to/reference.ppm` (P6 PPM). Other reference media and FL2VA keyframes remain available through the library, not this CLI. Native checks determine checkpoint, partition, dimensions and media compatibility. -CPU needs F32 DiT, not the default quantized GGUF or `--dequant-bf16` BF16. Custom files do not establish backend support. +For quantized CUDA checkpoints, satisfy the [CUDA prerequisites](#experimental-cuda), use `--features cuda` and `--device cuda`, and set the target architecture and a fresh build directory. There is no silent CPU fallback; system builds defer backend capability to the installed library. -Seven artifacts use the Hub cache (`HF_HOME`), nonempty `HF_TOKEN` or cached login. Hub queries pin revision groups to commits and fetch cache misses; no offline flag. Defaults follow mutable `main`; pin `--gguf-revision`, `--encoder-revision`, `--vae-revision`, and `--config-revision` to commits. - -`--partition ref2va` needs image/frames/audio, not FL2VA keyframes. Images: P6 PPM; audio: PCM16 WAV, 32 kHz. - -Workdir: `minimax-h3-output` by default; `.video_generate.lock` excludes writers. Existing native outputs are refused. Outputs: `frame_%06d.ppm`, `audio.wav`. `--output-mp4 ` runs ffmpeg (`--ffmpeg `), publishing private output to a new path in an existing directory; reserved aliases are refused. Failure preserves PPM/WAV. +`--output-dir` must be a new directory in an existing parent; any existing entry is refused before native loading. Generation writes `frame_%06d.ppm` and `audio.wav`. Load/generation failures leave the directory and any partial output for inspection. Paths are trusted, not sandboxed; do not change the directory concurrently. See `--help` for the complete small CLI. ## optional nix shell diff --git a/vllm-cpp/examples/video_generate.rs b/vllm-cpp/examples/video_generate.rs index 94cc813..7176b6e 100644 --- a/vllm-cpp/examples/video_generate.rs +++ b/vllm-cpp/examples/video_generate.rs @@ -1,178 +1,57 @@ -use std::collections::BTreeMap; -use std::env; use std::error::Error; -use std::fmt; -use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::path::{Component, Path, PathBuf}; -use std::process::{Command, ExitCode}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::fs; +use std::io; +use std::path::PathBuf; +use std::process::ExitCode; use clap::{Parser, ValueEnum}; -use hf_hub::api::sync::{Api, ApiBuilder}; -use hf_hub::{Cache, Repo, RepoType}; -use vllm_cpp::{ - compose_video_mux_argv, VideoDevice, VideoEngine, VideoGenerationParams, VideoMuxParams, - VideoPartition, VideoResult, -}; - -const GGUF_REPOSITORY: &str = "realrebelai/MiniMax-H3_GGUFs"; -const FL2VA_DIT: &str = "MiniMax-H3-FL2VA-Q4_K_M.gguf"; -const REF2VA_DIT: &str = "MiniMax-H3-REF2VA-Q4_K_M.gguf"; -const ENCODER: &str = "qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf"; -const VAE_REPOSITORY: &str = "Comfy-Org/MiniMax-H3"; -const VIDEO_VAE: &str = "vae/minimax_h3_video_vae_fp16.safetensors"; -const AUDIO_VAE: &str = "vae/minimax_h3_audio_vae_fp32.safetensors"; -const CONFIG_REPOSITORY: &str = "MiniMaxAI/MiniMax-H3"; -const VIDEO_VAE_CONFIG: &str = "FL2VA/video_vae/config.json"; -const AUDIO_VAE_CONFIG: &str = "FL2VA/audio_vae/config.json"; -const TOKENIZER: &str = "FL2VA/tokenizer/tokenizer.json"; -const DEFAULT_REVISION: &str = "main"; -const HF_TOKEN: &str = "HF_TOKEN"; -const WORKDIR_LOCK_FILE: &str = ".video_generate.lock"; -const PRIVATE_MUX_DIR_PREFIX: &str = ".video_generate-mux-"; - -static NEXT_PRIVATE_MUX: AtomicU64 = AtomicU64::new(0); +use vllm_cpp::{VideoDevice, VideoEngine, VideoGenerationParams, VideoPartition}; #[derive(Debug, Parser)] #[command( name = "video_generate", - about = "Generate MiniMax-H3 PPM frames and WAV audio from Hugging Face artifacts", - long_about = "Resolve a complete MiniMax-H3 checkpoint set through the normal Hugging Face cache, then generate PPM frames and WAV audio. The optional MP4 step is run by this example through ffmpeg; vllm-cpp only composes its argument boundaries." + about = "Generate MiniMax-H3 PPM frames and WAV audio from local checkpoints", + long_about = "Demonstrate blocking video generation with local checkpoints and a fresh output directory. No downloads or ffmpeg execution. CPU requires an F32 DiT; quantized CUDA checkpoints require a compatible CUDA build and GPU. Checkpoint, partition and media compatibility remain native checks. Partial output is left in place on failure." )] struct Args { - /// Text used by the Qwen3-VL encoder to condition generation. - #[arg(long, value_name = "TEXT")] + #[arg(long)] prompt: String, - - /// Served MiniMax-H3 DiT partition. + /// Local DiT weights compatible with the selected partition and device. + #[arg(long, value_name = "FILE")] + dit: PathBuf, + #[arg(long, value_name = "FILE")] + encoder: PathBuf, + #[arg(long, value_name = "FILE")] + tokenizer: PathBuf, + #[arg(long, value_name = "FILE")] + video_vae: PathBuf, + #[arg(long, value_name = "JSON")] + video_vae_config: PathBuf, + #[arg(long, value_name = "FILE")] + audio_vae: PathBuf, + #[arg(long, value_name = "JSON")] + audio_vae_config: PathBuf, + /// New directory in an existing parent; existing entries are never reused. + #[arg(long, value_name = "DIR")] + output_dir: PathBuf, + #[arg(long, value_enum, default_value = "cpu")] + device: DeviceArg, #[arg(long, value_enum, default_value = "fl2va")] partition: PartitionArg, - - /// Video engine device. CUDA requires a CUDA-enabled build and suitable hardware. - #[arg(long, value_enum, default_value = "cuda")] - device: DeviceArg, - - /// Output width in pixels; must be a multiple of 32. + /// P6 PPM image required by this example's Ref2VA mode. + #[arg(long, value_name = "PPM")] + reference_image: Option, #[arg(long, default_value_t = 256)] width: u32, - - /// Output height in pixels; must be a multiple of 32. #[arg(long, default_value_t = 256)] height: u32, - - /// Requested frame count. Native code rounds up to the next 17n+5 count. + /// Native code rounds up to the next 17n+5 frame count. #[arg(long, default_value_t = 5)] frames: u32, - - /// Number of denoising steps; must be at least 2. #[arg(long, default_value_t = 2)] steps: u32, - - /// Optional deterministic generation seed. #[arg(long)] seed: Option, - - /// Writable directory that receives frame_%06d.ppm and audio.wav. - #[arg(long, default_value = "minimax-h3-output", value_name = "DIR")] - workdir: PathBuf, - - /// First FL2VA keyframe in binary PPM (P6) format. - #[arg(long, value_name = "PPM")] - first_frame: Option, - - /// Last FL2VA keyframe in binary PPM (P6) format. - #[arg(long, value_name = "PPM")] - last_frame: Option, - - /// Ref2VA reference image in binary PPM (P6) format. - #[arg(long, value_name = "PPM")] - reference_image: Option, - - /// Ref2VA directory containing frame_%06d.ppm reference frames. - #[arg(long, value_name = "DIR")] - reference_video: Option, - - /// Ref2VA reference audio as uncompressed PCM16 WAV at exactly 32 kHz. - #[arg(long, value_name = "WAV")] - reference_audio: Option, - - /// Dequantize supported model weights to BF16 instead of keeping them quantized. - #[arg(long)] - dequant_bf16: bool, - - /// Keep supported FP4 weights resident on the selected device. - #[arg(long)] - fp4_resident: bool, - - /// Mux to a new MP4 in an existing directory; existing entries are refused. - #[arg(long, value_name = "MP4")] - output_mp4: Option, - - /// ffmpeg executable used by this example for --output-mp4. - #[arg(long, default_value = "ffmpeg", value_name = "PATH")] - ffmpeg: PathBuf, - - /// Repository containing the DiT file. - #[arg(long, default_value = GGUF_REPOSITORY, value_name = "REPO")] - gguf_repo: String, - - /// Branch, tag, or commit for --gguf-repo. - #[arg(long, default_value = DEFAULT_REVISION, value_name = "REVISION")] - gguf_revision: String, - - /// DiT path inside --gguf-repo; overrides must serve --partition. - #[arg(long, value_name = "PATH")] - dit_file: Option, - - /// Repository containing the prompt encoder file. - #[arg(long, default_value = GGUF_REPOSITORY, value_name = "REPO")] - encoder_repo: String, - - /// Branch, tag, or commit for --encoder-repo. - #[arg(long, default_value = DEFAULT_REVISION, value_name = "REVISION")] - encoder_revision: String, - - /// Encoder path inside --encoder-repo. - #[arg(long, default_value = ENCODER, value_name = "PATH")] - encoder_file: String, - - /// Repository containing both VAE weight files. - #[arg(long, default_value = VAE_REPOSITORY, value_name = "REPO")] - vae_repo: String, - - /// Branch, tag, or commit for --vae-repo. - #[arg(long, default_value = DEFAULT_REVISION, value_name = "REVISION")] - vae_revision: String, - - /// Video VAE weight path inside --vae-repo. - #[arg(long, default_value = VIDEO_VAE, value_name = "PATH")] - video_vae_file: String, - - /// Audio VAE weight path inside --vae-repo. - #[arg(long, default_value = AUDIO_VAE, value_name = "PATH")] - audio_vae_file: String, - - /// Repository containing both VAE configs and the tokenizer. - #[arg(long, default_value = CONFIG_REPOSITORY, value_name = "REPO")] - config_repo: String, - - /// Branch, tag, or commit for --config-repo. - #[arg(long, default_value = DEFAULT_REVISION, value_name = "REVISION")] - config_revision: String, - - /// Video VAE config path inside --config-repo. - #[arg(long, default_value = VIDEO_VAE_CONFIG, value_name = "PATH")] - video_vae_config_file: String, - - /// Audio VAE config path inside --config-repo. - #[arg(long, default_value = AUDIO_VAE_CONFIG, value_name = "PATH")] - audio_vae_config_file: String, - - /// Tokenizer path inside --config-repo. - #[arg(long, default_value = TOKENIZER, value_name = "PATH")] - tokenizer_file: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)] @@ -181,15 +60,6 @@ enum PartitionArg { Ref2va, } -impl PartitionArg { - const fn default_dit(self) -> &'static str { - match self { - Self::Fl2va => FL2VA_DIT, - Self::Ref2va => REF2VA_DIT, - } - } -} - impl From for VideoPartition { fn from(value: PartitionArg) -> Self { match value { @@ -214,332 +84,6 @@ impl From for VideoDevice { } } -#[derive(Debug)] -struct AppError(String); - -impl fmt::Display for AppError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.0) - } -} - -impl Error for AppError {} - -type AppResult = Result; - -#[derive(Debug)] -struct WorkdirLock { - path: PathBuf, - file: Option, -} - -impl WorkdirLock { - fn acquire(workdir: &Path) -> AppResult { - let path = workdir.join(WORKDIR_LOCK_FILE); - let file = match OpenOptions::new().write(true).create_new(true).open(&path) { - Ok(file) => file, - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { - return Err(app_error(format!( - "--workdir `{}` is already in use because lock `{}` exists; after an abnormal termination, remove a stale lock manually only after confirming no process owns the workdir", - workdir.display(), - path.display() - ))); - } - Err(error) => { - return Err(app_error(format!( - "could not atomically lock --workdir `{}` by creating `{}`: {error}", - workdir.display(), - path.display() - ))); - } - }; - - let mut lock = Self { - path, - file: Some(file), - }; - if let Err(error) = writeln!( - lock.file.as_mut().expect("newly acquired lock has a file"), - "video_generate process {} owns this workdir for resolve, load, generate, and mux", - std::process::id() - ) { - return Err(app_error(format!( - "could not write ownership context to workdir lock `{}`: {error}", - lock.path.display() - ))); - } - Ok(lock) - } - - fn remove_owned_file(&mut self) -> io::Result<()> { - if self.file.is_none() { - return Ok(()); - } - - #[cfg(unix)] - let result = { - use std::os::unix::fs::MetadataExt; - - (|| { - let owned = self.file.as_ref().expect("checked lock file").metadata()?; - let current = match fs::symlink_metadata(&self.path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), - Err(error) => return Err(error), - }; - if (owned.dev(), owned.ino()) != (current.dev(), current.ino()) { - return Err(io::Error::other( - "lock path now names a different filesystem object; refusing to remove it", - )); - } - fs::remove_file(&self.path) - })() - }; - - #[cfg(not(unix))] - let result = Err(io::Error::new( - io::ErrorKind::Unsupported, - "this platform cannot prove lock-path identity; retaining the lock", - )); - - self.file.take(); - result - } -} - -impl Drop for WorkdirLock { - fn drop(&mut self) { - if let Err(error) = self.remove_owned_file() { - let _ = writeln!( - io::stderr().lock(), - "warning: could not remove owned workdir lock `{}`: {error}", - self.path.display() - ); - } - } -} - -#[derive(Debug)] -struct PrivateMuxTarget { - directory: PathBuf, - path: PathBuf, - owned_output: Option, - active: bool, - #[cfg(unix)] - directory_file: File, -} - -impl PrivateMuxTarget { - fn create(parent: &Path) -> AppResult { - loop { - let sequence = NEXT_PRIVATE_MUX.fetch_add(1, Ordering::Relaxed); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_or(0, |duration| duration.as_nanos()); - let directory = parent.join(format!( - "{PRIVATE_MUX_DIR_PREFIX}{}-{timestamp:032x}-{sequence:016x}", - std::process::id() - )); - match create_private_mux_directory(&directory) { - Ok(()) => { - #[cfg(unix)] - let directory_file = match File::open(&directory) { - Ok(file) => file, - Err(error) => { - if let Err(cleanup_error) = fs::remove_dir(&directory) { - let _ = writeln!( - io::stderr().lock(), - "warning: could not remove newly created private MP4 directory `{}` after failing to open it: {cleanup_error}", - directory.display() - ); - } - return Err(app_error(format!( - "could not retain ownership of private MP4 directory `{}`: {error}", - directory.display() - ))); - } - }; - return Ok(Self { - path: directory.join("mux.mp4"), - directory, - owned_output: None, - active: true, - #[cfg(unix)] - directory_file, - }); - } - Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {} - Err(error) => { - return Err(app_error(format!( - "could not create a private MP4 directory in `{}`: {error}", - parent.display() - ))); - } - } - } - } - - fn path(&self) -> &Path { - &self.path - } - - fn capture_output(&mut self) -> io::Result { - if self.owned_output.is_some() { - return Ok(true); - } - let metadata = match fs::symlink_metadata(&self.path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false), - Err(error) => return Err(error), - }; - if !metadata.file_type().is_file() { - return Err(io::Error::other( - "ffmpeg output is not a regular private temporary file", - )); - } - - let file = File::open(&self.path)?; - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - - let opened = file.metadata()?; - let current = fs::symlink_metadata(&self.path)?; - if !current.file_type().is_file() - || (opened.dev(), opened.ino()) != (current.dev(), current.ino()) - { - return Err(io::Error::other( - "private MP4 path changed while recording its ownership", - )); - } - } - self.owned_output = Some(file); - Ok(true) - } - - #[cfg(unix)] - fn verify_directory_ownership(&self) -> io::Result<()> { - use std::os::unix::fs::MetadataExt; - - let owned = self.directory_file.metadata()?; - let current = fs::symlink_metadata(&self.directory)?; - if !current.file_type().is_dir() - || (owned.dev(), owned.ino()) != (current.dev(), current.ino()) - { - return Err(io::Error::other( - "private MP4 directory path no longer names the directory created by this run", - )); - } - Ok(()) - } - - #[cfg(not(unix))] - fn verify_directory_ownership(&self) -> io::Result<()> { - Ok(()) - } - - fn remove_owned_output(&self) -> io::Result<()> { - let current = match fs::symlink_metadata(&self.path) { - Ok(metadata) => metadata, - Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), - Err(error) => return Err(error), - }; - let Some(owned_output) = self.owned_output.as_ref() else { - return Err(io::Error::other( - "private MP4 path exists but its ownership was not established; refusing to remove it", - )); - }; - - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt; - - let owned = owned_output.metadata()?; - if !current.file_type().is_file() - || (owned.dev(), owned.ino()) != (current.dev(), current.ino()) - { - return Err(io::Error::other( - "private MP4 path now names a different filesystem object; refusing to remove it", - )); - } - } - fs::remove_file(&self.path) - } - - fn cleanup(&mut self) -> io::Result<()> { - if !self.active { - return Ok(()); - } - self.verify_directory_ownership()?; - self.remove_owned_output()?; - self.verify_directory_ownership()?; - fs::remove_dir(&self.directory)?; - self.owned_output.take(); - self.active = false; - Ok(()) - } -} - -impl Drop for PrivateMuxTarget { - fn drop(&mut self) { - if let Err(error) = self.cleanup() { - let _ = writeln!( - io::stderr().lock(), - "warning: could not clean private MP4 temporary path `{}`: {error}", - self.path.display() - ); - } - } -} - -#[cfg(unix)] -fn create_private_mux_directory(path: &Path) -> io::Result<()> { - use std::os::unix::fs::DirBuilderExt; - - let mut builder = fs::DirBuilder::new(); - builder.mode(0o700).create(path) -} - -#[cfg(not(unix))] -fn create_private_mux_directory(path: &Path) -> io::Result<()> { - fs::create_dir(path) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct HubArtifact { - label: &'static str, - repo: String, - revision: String, - path: String, -} - -impl HubArtifact { - fn new( - label: &'static str, - repo: impl Into, - revision: impl Into, - path: impl Into, - ) -> Self { - Self { - label, - repo: repo.into(), - revision: revision.into(), - path: path.into(), - } - } -} - -#[derive(Debug)] -struct ResolvedArtifacts { - dit: PathBuf, - encoder: PathBuf, - video_vae: PathBuf, - audio_vae: PathBuf, - video_vae_config: PathBuf, - audio_vae_config: PathBuf, - tokenizer: PathBuf, -} - fn main() -> ExitCode { match run(Args::parse()) { Ok(()) => ExitCode::SUCCESS, @@ -550,2144 +94,263 @@ fn main() -> ExitCode { } } -fn run(args: Args) -> AppResult<()> { - validate_args(&args)?; - let generation = generation_params(&args); - generation - .validate() - .map_err(|error| app_error(format!("invalid generation parameters: {error}")))?; - validate_device_build_compatibility( - args.device, - cfg!(feature = "bundled"), - cfg!(feature = "cuda"), - )?; - let workdir_lock = prepare_workdir(&args.workdir)?; - - let sources = artifact_sources(&args); - let artifacts = resolve_artifacts(&sources)?; - - eprintln!("Loading the MiniMax-H3 checkpoint set..."); - let mut engine = VideoEngine::builder(&artifacts.dit) - .encoder_path(&artifacts.encoder) - .tokenizer_path(&artifacts.tokenizer) - .video_vae_path(&artifacts.video_vae) - .video_vae_config_path(&artifacts.video_vae_config) - .audio_vae_path(&artifacts.audio_vae) - .audio_vae_config_path(&artifacts.audio_vae_config) +fn run(args: Args) -> Result<(), Box> { + let generation = prepare(&args)?; + let mut engine = VideoEngine::builder(&args.dit) + .encoder_path(&args.encoder) + .tokenizer_path(&args.tokenizer) + .video_vae_path(&args.video_vae) + .video_vae_config_path(&args.video_vae_config) + .audio_vae_path(&args.audio_vae) + .audio_vae_config_path(&args.audio_vae_config) .partition(args.partition.into()) .device(args.device.into()) - .dequant_bf16(args.dequant_bf16) - .fp4_resident(args.fp4_resident) - .load() - .map_err(|error| app_error(format!("could not load MiniMax-H3: {error}")))?; - - eprintln!("Generating PPM frames and WAV audio..."); - let result = engine - .generate(&generation) - .map_err(|error| app_error(format!("generation failed: {error}")))?; - report_artifacts(&result); - - if let Some(output) = &args.output_mp4 { - mux_mp4( - result.frame_dir(), - result.audio_path(), - result.fps(), - output, - &args.ffmpeg, - )?; - } - drop(workdir_lock); + .load()?; + let result = engine.generate(&generation)?; + println!( + "wrote {} PPM frames to {} ({}x{} at {} fps)", + result.frame_count(), + result.frame_dir().display(), + result.width(), + result.height(), + result.fps() + ); + println!("wrote WAV audio to {}", result.audio_path().display()); Ok(()) } fn generation_params(args: &Args) -> VideoGenerationParams { - let mut params = VideoGenerationParams::new(&args.prompt, &args.workdir) + let mut params = VideoGenerationParams::new(&args.prompt, &args.output_dir) .dimensions(args.width, args.height) .num_frames(args.frames) .steps(args.steps); if let Some(seed) = args.seed { params = params.seed(seed); } - if let Some(path) = &args.first_frame { - params = params.first_frame(path); - } - if let Some(path) = &args.last_frame { - params = params.last_frame(path); - } if let Some(path) = &args.reference_image { params = params.reference_image(path); } - if let Some(path) = &args.reference_video { - params = params.reference_video(path); - } - if let Some(path) = &args.reference_audio { - params = params.reference_audio(path); - } params } -fn validate_args(args: &Args) -> AppResult<()> { +fn prepare(args: &Args) -> Result> { + let generation = generation_params(args); + generation.validate()?; if args.prompt.trim().is_empty() { - return Err(app_error("--prompt must not be empty")); - } - if args.workdir.as_os_str().is_empty() { - return Err(app_error("--workdir must not be empty")); - } - validate_cpu_dit_compatibility(args)?; - if args.width % 32 != 0 || args.height % 32 != 0 { - return Err(app_error("--width and --height must be multiples of 32")); - } - let width = u64::from(args.width); - let height = u64::from(args.height); - if width > 4 * height || height > 4 * width { - return Err(app_error( - "--width and --height must have an aspect ratio in [1:4, 4:1]", - )); - } - if args.frames <= 1 { - return Err(app_error("--frames must be greater than one")); - } - if args.steps < 2 { - return Err(app_error("--steps must be at least 2")); + return Err("--prompt must not be empty".into()); } - if args - .output_mp4 - .as_ref() - .is_some_and(|path| path.as_os_str().is_empty()) - { - return Err(app_error("--output-mp4 must not be empty")); + if (args.partition == PartitionArg::Ref2va) != args.reference_image.is_some() { + return Err("use --reference-image with --partition ref2va; omit it for fl2va".into()); } - - let has_keyframes = args.first_frame.is_some() || args.last_frame.is_some(); - let has_references = args.reference_image.is_some() - || args.reference_video.is_some() - || args.reference_audio.is_some(); - if has_keyframes && has_references { - return Err(app_error( - "FL2VA keyframes cannot be combined with Ref2VA references", - )); - } - if args.reference_image.is_some() && args.reference_video.is_some() { - return Err(app_error( - "--reference-image and --reference-video are mutually exclusive", - )); + // System libraries configure their backends independently of Cargo features. + if args.device == DeviceArg::Cuda && cfg!(feature = "bundled") && !cfg!(feature = "cuda") { + return Err( + "--device cuda requires a CUDA-enabled build; no CPU fallback is performed".into(), + ); } - let effective_dit_name = portable_hub_path_file_name(effective_dit(args)); - match args.partition { - PartitionArg::Fl2va if effective_dit_name == REF2VA_DIT => { - return Err(app_error(format!( - "DiT `{REF2VA_DIT}` is the known Ref2VA model and cannot serve --partition fl2va" - ))); - } - PartitionArg::Ref2va if effective_dit_name == FL2VA_DIT => { - return Err(app_error(format!( - "DiT `{FL2VA_DIT}` is the known FL2VA model and cannot serve --partition ref2va" - ))); - } - PartitionArg::Fl2va if has_references => { - return Err(app_error( - "Ref2VA references require --partition ref2va and its matching DiT", - )); - } - PartitionArg::Ref2va if has_keyframes => { - return Err(app_error( - "keyframes require --partition fl2va and its matching DiT", - )); - } - PartitionArg::Ref2va if !has_references => { - return Err(app_error( - "--partition ref2va requires a reference image, video, or audio", - )); + for (flag, path) in [ + ("--dit", &args.dit), + ("--encoder", &args.encoder), + ("--tokenizer", &args.tokenizer), + ("--video-vae", &args.video_vae), + ("--video-vae-config", &args.video_vae_config), + ("--audio-vae", &args.audio_vae), + ("--audio-vae-config", &args.audio_vae_config), + ] + .into_iter() + .chain( + args.reference_image + .as_ref() + .map(|path| ("--reference-image", path)), + ) { + if !path.is_file() { + return Err(format!("{flag} must name a local file: {}", path.display()).into()); } - _ => {} } - - require_file(args.first_frame.as_deref(), "--first-frame")?; - require_file(args.last_frame.as_deref(), "--last-frame")?; - require_file(args.reference_image.as_deref(), "--reference-image")?; - require_file(args.reference_audio.as_deref(), "--reference-audio")?; - require_directory(args.reference_video.as_deref(), "--reference-video")?; - if let Some(path) = &args.reference_audio { - validate_reference_wav(path)?; - } - if let Some(path) = &args.output_mp4 { - validate_mp4_output(path, &args.workdir)?; - } - Ok(()) -} - -fn require_file(path: Option<&Path>, flag: &str) -> AppResult<()> { - require_path_kind(path, flag, "file", std::fs::Metadata::is_file) -} - -fn require_directory(path: Option<&Path>, flag: &str) -> AppResult<()> { - require_path_kind(path, flag, "directory", std::fs::Metadata::is_dir) + // create_dir atomically refuses existing entries, including symlinks. Native + // generation trusts these caller paths; this is not a filesystem sandbox. + fs::create_dir(&args.output_dir).map_err(|error| { + io::Error::new( + error.kind(), + format!( + "cannot create --output-dir {}: {error}; choose a fresh directory in an existing parent", + args.output_dir.display() + ), + ) + })?; + Ok(generation) } -fn validate_reference_wav(path: &Path) -> AppResult<()> { - const MIN_WAV_BYTES: u64 = 44; - const MODEL_SAMPLE_RATE: u32 = 32_000; - - let mut file = File::open(path).map_err(|error| { - app_error(format!( - "could not open --reference-audio `{}`: {error}", - path.display() - )) - })?; - let file_len = file - .metadata() - .map_err(|error| { - app_error(format!( - "could not inspect --reference-audio `{}`: {error}", - path.display() - )) - })? - .len(); - if file_len < MIN_WAV_BYTES { - return Err(invalid_reference_wav( - path, - "too short to be a RIFF/WAVE file", - )); - } +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; - let mut riff_header = [0_u8; 12]; - file.read_exact(&mut riff_header).map_err(|error| { - invalid_reference_wav(path, format!("could not read RIFF header: {error}")) - })?; - if &riff_header[..4] != b"RIFF" || &riff_header[8..] != b"WAVE" { - return Err(invalid_reference_wav(path, "not a RIFF/WAVE file")); - } + static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); - let mut offset = 12_u64; - let mut format = None; - let mut data_len = None; - while offset - .checked_add(8) - .is_some_and(|header_end| header_end <= file_len) - { - file.seek(SeekFrom::Start(offset)).map_err(|error| { - invalid_reference_wav(path, format!("could not seek to WAV chunk: {error}")) - })?; - let mut chunk_header = [0_u8; 8]; - file.read_exact(&mut chunk_header).map_err(|error| { - invalid_reference_wav(path, format!("could not read WAV chunk header: {error}")) - })?; - let declared_len = u64::from(u32::from_le_bytes( - chunk_header[4..8] - .try_into() - .expect("four-byte chunk length"), - )); - let body = offset.checked_add(8).ok_or_else(|| { - invalid_reference_wav( - path, - "WAV chunk body offset overflows the file address space", - ) - })?; + struct TempDir(PathBuf); - if &chunk_header[..4] == b"fmt " - && declared_len >= 16 - && body - .checked_add(16) - .is_some_and(|format_end| format_end <= file_len) - { - file.seek(SeekFrom::Start(body)).map_err(|error| { - invalid_reference_wav(path, format!("could not seek to fmt chunk: {error}")) - })?; - let mut fields = [0_u8; 16]; - file.read_exact(&mut fields).map_err(|error| { - invalid_reference_wav(path, format!("could not read fmt chunk: {error}")) - })?; - let format_tag = u16::from_le_bytes([fields[0], fields[1]]); - if format_tag != 1 { - return Err(invalid_reference_wav( - path, - "only uncompressed PCM (format tag 1) is supported", - )); - } - format = Some(( - u16::from_le_bytes([fields[2], fields[3]]), - u32::from_le_bytes([fields[4], fields[5], fields[6], fields[7]]), - u16::from_le_bytes([fields[14], fields[15]]), + impl TempDir { + fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "vllm-local-video-{}-{}", + std::process::id(), + NEXT_TEMP.fetch_add(1, Ordering::Relaxed) )); - } else if &chunk_header[..4] == b"data" { - data_len = Some(declared_len.min(file_len.saturating_sub(body))); - break; + fs::create_dir(&path).unwrap(); + Self(path) + } + + fn args(&self, extra: &[&str]) -> Args { + let file = self.0.join("checkpoint"); + fs::write(&file, b"local test placeholder, not a model").unwrap(); + let mut args = vec![ + "video_generate".into(), + "--prompt".into(), + "a shoreline".into(), + ]; + for flag in [ + "--dit", + "--encoder", + "--tokenizer", + "--video-vae", + "--video-vae-config", + "--audio-vae", + "--audio-vae-config", + ] { + args.extend([ + std::ffi::OsString::from(flag), + file.clone().into_os_string(), + ]); + } + args.extend([ + "--output-dir".into(), + self.0.join("output").into_os_string(), + ]); + args.extend(extra.iter().map(std::ffi::OsString::from)); + Args::try_parse_from(args).unwrap() } - - offset = body - .checked_add(declared_len) - .and_then(|end| end.checked_add(declared_len & 1)) - .ok_or_else(|| { - invalid_reference_wav(path, "WAV chunk offset overflows the file address space") - })?; } - let Some((channels, sample_rate, bits_per_sample)) = format else { - return Err(invalid_reference_wav( - path, - "no complete fmt chunk before data", - )); - }; - let Some(data_len) = data_len else { - return Err(invalid_reference_wav(path, "no data chunk after fmt")); - }; - if bits_per_sample != 16 { - return Err(invalid_reference_wav( - path, - format!("expected 16-bit PCM, found {bits_per_sample}-bit samples"), - )); - } - if channels == 0 { - return Err(invalid_reference_wav( - path, - "the fmt chunk declares zero channels", - )); - } - if sample_rate != MODEL_SAMPLE_RATE { - return Err(invalid_reference_wav( - path, - format!( - "expected exactly {MODEL_SAMPLE_RATE} Hz, found {sample_rate} Hz; resample the file first" - ), - )); + impl Drop for TempDir { + fn drop(&mut self) { + fs::remove_dir_all(&self.0).unwrap(); + } } - let source_frame_bytes = u64::from(channels) * 2; - if data_len < source_frame_bytes { - return Err(invalid_reference_wav( - path, - "the data chunk contains no complete source frame", - )); + + #[test] + fn parses_local_defaults_and_explicit_generation_options() { + let temp = TempDir::new(); + let defaults = temp.args(&[]); + assert_eq!(defaults.device, DeviceArg::Cpu); + assert_eq!(defaults.partition, PartitionArg::Fl2va); + let args = temp.args(&[ + "--width", "320", "--height", "192", "--frames", "22", "--steps", "4", "--seed", "42", + "--device", "cuda", + ]); + assert_eq!( + (args.width, args.height, args.frames, args.steps, args.seed), + (320, 192, 22, 4, Some(42)) + ); + assert_eq!(VideoDevice::from(args.device), VideoDevice::Cuda); + assert_eq!( + VideoPartition::from(PartitionArg::Ref2va), + VideoPartition::Ref2va + ); + generation_params(&args).validate().unwrap(); + assert!(!args.output_dir.exists()); } - Ok(()) -} -fn invalid_reference_wav(path: &Path, reason: impl fmt::Display) -> AppError { - app_error(format!( - "invalid --reference-audio `{}`: {reason}", - path.display() - )) -} + #[test] + fn help_and_missing_arguments_exit_at_the_parser() { + assert_eq!( + Args::try_parse_from(["video_generate", "--help"]) + .unwrap_err() + .kind(), + clap::error::ErrorKind::DisplayHelp + ); + assert_eq!( + Args::try_parse_from(["video_generate"]).unwrap_err().kind(), + clap::error::ErrorKind::MissingRequiredArgument + ); + } -fn validate_mp4_output(output: &Path, workdir: &Path) -> AppResult<()> { - match fs::symlink_metadata(output) { - Ok(_) => { - return Err(app_error(format!( - "--output-mp4 `{}` already exists; refusing to replace any existing entry", - output.display() - ))); - } - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => { - return Err(app_error(format!( - "could not inspect --output-mp4 `{}` without following its final component: {error}", - output.display() - ))); + #[test] + fn invalid_parameters_and_missing_files_have_no_output_side_effects() { + let temp = TempDir::new(); + for extra in [ + vec!["--width", "0"], + vec!["--frames", "1"], + vec!["--steps", "0"], + ] { + let args = temp.args(&extra); + let output = args.output_dir.clone(); + assert!(run(args).is_err()); + assert!(!output.exists()); + } + let mut args = temp.args(&[]); + args.prompt.clear(); + let output = args.output_dir.clone(); + assert!(run(args).unwrap_err().to_string().contains("--prompt")); + assert!(!output.exists()); + let mut args = temp.args(&[]); + args.dit = temp.0.join("missing"); + assert!(prepare(&args).unwrap_err().to_string().contains("--dit")); + assert!(!args.output_dir.exists()); + if cfg!(feature = "bundled") && !cfg!(feature = "cuda") { + let args = temp.args(&["--device", "cuda"]); + let output = args.output_dir.clone(); + assert!(run(args) + .unwrap_err() + .to_string() + .contains("CUDA-enabled build")); + assert!(!output.exists()); } } - let file_name = output.file_name().ok_or_else(|| { - app_error(format!( - "--output-mp4 `{}` has no output file name", - output.display() - )) - })?; - let parent = output - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - let resolved_parent = fs::canonicalize(parent).map_err(|error| { - app_error(format!( - "could not safely resolve --output-mp4 parent `{}`: {error}", - parent.display() - )) - })?; - if !fs::metadata(&resolved_parent) - .map_err(|error| { - app_error(format!( - "could not inspect resolved --output-mp4 parent `{}`: {error}", - resolved_parent.display() - )) - })? - .is_dir() - { - return Err(app_error(format!( - "--output-mp4 parent `{}` is not a directory", - parent.display() - ))); + #[test] + fn fresh_output_is_created_and_existing_entries_are_preserved() { + let temp = TempDir::new(); + let args = temp.args(&[]); + prepare(&args).unwrap(); + let marker = args.output_dir.join("keep"); + fs::write(&marker, b"unchanged").unwrap(); + assert!(run(args) + .unwrap_err() + .to_string() + .contains("fresh directory")); + assert_eq!(fs::read(marker).unwrap(), b"unchanged"); + let mut args = temp.args(&[]); + args.output_dir = temp.0.join("checkpoint"); + let before = fs::read(&args.output_dir).unwrap(); + assert!(prepare(&args).is_err()); + assert_eq!(fs::read(&args.output_dir).unwrap(), before); } - let resolved_workdir = match fs::canonicalize(workdir) { - Ok(path) => Some(path), - Err(error) if error.kind() == io::ErrorKind::NotFound => None, - Err(error) => { - return Err(app_error(format!( - "could not resolve --workdir `{}` while checking --output-mp4 aliases: {error}", - workdir.display() - ))); - } - }; - if resolved_workdir.as_deref() == Some(resolved_parent.as_path()) - && is_reserved_workdir_output(file_name) - { - return Err(app_error(format!( - "--output-mp4 `{}` aliases reserved workdir output `{}`", - output.display(), - file_name.to_string_lossy() - ))); - } - Ok(()) -} - -fn is_reserved_workdir_output(file_name: &std::ffi::OsStr) -> bool { - file_name == WORKDIR_LOCK_FILE - || file_name == "audio.wav" - || file_name.to_str().is_some_and(is_native_frame_output_name) -} - -fn prepare_workdir(path: &Path) -> AppResult { - match fs::metadata(path) { - Ok(metadata) if !metadata.is_dir() => { - return Err(app_error(format!( - "--workdir `{}` is not a directory", - path.display() - ))); - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - fs::create_dir_all(path).map_err(|error| { - app_error(format!( - "could not create --workdir directory `{}`: {error}", - path.display() - )) - })?; - } - Err(error) => { - return Err(app_error(format!( - "could not inspect or prepare --workdir `{}`: {error}", - path.display() - ))); - } - } - - let metadata = fs::metadata(path).map_err(|error| { - app_error(format!( - "could not inspect prepared --workdir `{}`: {error}", - path.display() - )) - })?; - if !metadata.is_dir() { - return Err(app_error(format!( - "--workdir `{}` is not a directory", - path.display() - ))); - } - - let lock = WorkdirLock::acquire(path)?; - reject_existing_native_outputs(path)?; - Ok(lock) -} - -fn reject_existing_native_outputs(path: &Path) -> AppResult<()> { - let entries = fs::read_dir(path).map_err(|error| { - app_error(format!( - "could not read --workdir `{}` while checking native output destinations: {error}", - path.display() - )) - })?; - for entry in entries { - let entry = entry.map_err(|error| { - app_error(format!( - "could not inspect an entry in --workdir `{}` while checking native output destinations: {error}", - path.display() - )) - })?; - let file_name = entry.file_name(); - let Some(file_name) = file_name.to_str() else { - continue; - }; - if file_name == WORKDIR_LOCK_FILE { - continue; - } - if file_name == "audio.wav" || is_native_frame_output_name(file_name) { - return Err(app_error(format!( - "--workdir `{}` already contains native output destination `{file_name}`", - path.display() - ))); - } - } - Ok(()) -} - -fn is_native_frame_output_name(file_name: &str) -> bool { - let Some(index) = file_name - .strip_prefix("frame_") - .and_then(|name| name.strip_suffix(".ppm")) - else { - return false; - }; - // Native `%06lld` uses six as a minimum width and emits wider indices unchanged. - index.len() >= 6 && index.bytes().all(|byte| byte.is_ascii_digit()) -} - -fn require_path_kind( - path: Option<&Path>, - flag: &str, - expected: &str, - matches: impl FnOnce(&std::fs::Metadata) -> bool, -) -> AppResult<()> { - let Some(path) = path else { - return Ok(()); - }; - let metadata = std::fs::metadata(path).map_err(|error| { - app_error(format!( - "could not inspect {flag} `{}`: {error}", - path.display() - )) - })?; - if !matches(&metadata) { - return Err(app_error(format!( - "{flag} `{}` is not a {expected}", - path.display() - ))); - } - Ok(()) -} - -fn effective_dit(args: &Args) -> &str { - args.dit_file - .as_deref() - .unwrap_or_else(|| args.partition.default_dit()) -} - -fn validate_device_build_compatibility( - device: DeviceArg, - bundled: bool, - cuda: bool, -) -> AppResult<()> { - // System libraries configure backends independently of Cargo features. - if device == DeviceArg::Cuda && bundled && !cuda { - return Err(app_error( - "--device cuda is unavailable: this bundled build has CUDA disabled; rebuild with --features cuda and a compatible CUDA toolkit/GPU. Selecting --device cpu does not support the default GGUF DiT", - )); - } - Ok(()) -} - -fn validate_cpu_dit_compatibility(args: &Args) -> AppResult<()> { - let dit = effective_dit(args); - let is_gguf = portable_hub_path_file_name(dit) - .to_ascii_lowercase() - .ends_with(".gguf"); - if args.device == DeviceArg::Cpu && is_gguf { - return Err(app_error(format!( - "--device cpu cannot use GGUF DiT `{dit}` with the pinned vllm.cpp: the CPU MiniMax-H3 host forward requires F32 weights, while GGUF provides block-quantized weights or BF16 with --dequant-bf16; lower quantization does not fix this routing. Use --device cuda or a future native quantized CPU-forward implementation" - ))); - } - Ok(()) -} - -fn portable_hub_path_file_name(path: &str) -> &str { - path.rsplit('/').next().unwrap_or(path) -} - -fn artifact_sources(args: &Args) -> [HubArtifact; 7] { - [ - HubArtifact::new( - "DiT", - &args.gguf_repo, - &args.gguf_revision, - effective_dit(args), - ), - HubArtifact::new( - "prompt encoder", - &args.encoder_repo, - &args.encoder_revision, - &args.encoder_file, - ), - HubArtifact::new( - "video VAE", - &args.vae_repo, - &args.vae_revision, - &args.video_vae_file, - ), - HubArtifact::new( - "audio VAE", - &args.vae_repo, - &args.vae_revision, - &args.audio_vae_file, - ), - HubArtifact::new( - "video VAE config", - &args.config_repo, - &args.config_revision, - &args.video_vae_config_file, - ), - HubArtifact::new( - "audio VAE config", - &args.config_repo, - &args.config_revision, - &args.audio_vae_config_file, - ), - HubArtifact::new( - "tokenizer", - &args.config_repo, - &args.config_revision, - &args.tokenizer_file, - ), - ] -} - -fn cached_commit_snapshot_or_else( - cache: &Cache, - repo: &str, - commit: &str, - artifact_path: &str, - fallback: impl FnOnce() -> Result, -) -> Result { - let snapshot_path = cache - .repo(Repo::new(repo.to_owned(), RepoType::Model)) - .pointer_path(commit) - .join(artifact_path); - if snapshot_path.is_file() { - Ok(snapshot_path) - } else { - fallback() - } -} - -fn validate_artifact_sources(sources: &[HubArtifact; 7]) -> AppResult<()> { - for source in sources { - validate_repo_id(&source.repo, source.label)?; - validate_hub_path(&source.revision, &format!("{} revision", source.label))?; - validate_hub_path(&source.path, &format!("{} artifact path", source.label))?; - } - Ok(()) -} - -fn source_revision_groups(sources: &[HubArtifact; 7]) -> BTreeMap<(String, String), usize> { - let mut groups = BTreeMap::new(); - for (index, source) in sources.iter().enumerate() { - groups - .entry((source.repo.clone(), source.revision.clone())) - .or_insert(index); - } - groups -} - -fn resolve_source_revisions( - sources: &[HubArtifact; 7], - mut resolve: impl FnMut(&HubArtifact) -> Result, -) -> Result, E> { - let groups = source_revision_groups(sources); - let mut commits = BTreeMap::new(); - for (index, source) in sources.iter().enumerate() { - let key = (source.repo.clone(), source.revision.clone()); - if groups.get(&key) == Some(&index) { - commits.insert(key, resolve(source)?); - } - } - Ok(commits) -} - -fn resolve_artifacts(sources: &[HubArtifact; 7]) -> AppResult { - validate_artifact_sources(sources)?; - - let cache = Cache::from_env(); - let mut builder = ApiBuilder::from_cache(cache.clone()).with_progress(true); - if let Some(token) = selected_hf_token(env::var(HF_TOKEN).ok()) { - builder = builder.with_token(Some(token)); - } - let api = builder - .build() - .map_err(|error| app_error(format!("could not create Hugging Face API: {error}")))?; - - let commits = resolve_source_revisions(sources, |source| resolve_commit(&api, source))?; - - let mut paths = Vec::with_capacity(sources.len()); - for source in sources { - let key = (source.repo.clone(), source.revision.clone()); - let sha = commits - .get(&key) - .expect("every validated artifact source has a revision group") - .clone(); - eprintln!( - "Resolving {} `{}` from `{}` at `{}`...", - source.label, source.path, source.repo, source.revision - ); - let path = - cached_commit_snapshot_or_else(&cache, &source.repo, &sha, &source.path, || { - api.repo(Repo::with_revision( - source.repo.clone(), - RepoType::Model, - sha.clone(), - )) - .get(&source.path) - .map_err(|error| { - app_error(format!( - "could not resolve {} `{}` from `{}` at `{}`: {error}", - source.label, source.path, source.repo, source.revision - )) - }) - })?; - if !path.is_file() { - return Err(app_error(format!( - "resolved {} path `{}` is not a file", - source.label, - path.display() - ))); - } - paths.push(path); - } - - for ((repo, revision), sha) in commits { - cache - .repo(Repo::with_revision( - repo.clone(), - RepoType::Model, - revision.clone(), - )) - .create_ref(&sha) - .map_err(|error| { - app_error(format!( - "could not update Hugging Face cache ref `{revision}` for `{repo}`: {error}" - )) - })?; - } - - let [dit, encoder, video_vae, audio_vae, video_vae_config, audio_vae_config, tokenizer] = - paths.try_into().map_err(|_| { - app_error("internal error: Hugging Face artifact count changed unexpectedly") - })?; - Ok(ResolvedArtifacts { - dit, - encoder, - video_vae, - audio_vae, - video_vae_config, - audio_vae_config, - tokenizer, - }) -} - -fn selected_hf_token(environment_token: Option) -> Option { - environment_token.filter(|token| !token.trim().is_empty()) -} - -fn resolve_commit(api: &Api, source: &HubArtifact) -> AppResult { - let requested = Repo::with_revision( - source.repo.clone(), - RepoType::Model, - source.revision.clone(), - ); - let info = api.repo(requested).info().map_err(|error| { - app_error(format!( - "could not read metadata for `{}` at `{}`: {error}", - source.repo, source.revision - )) - })?; - let sha = info.sha.trim(); - validate_single_component(sha, "repository metadata commit SHA")?; - Ok(sha.to_owned()) -} - -fn validate_repo_id(repo: &str, label: &str) -> AppResult<()> { - validate_hub_path(repo, &format!("{label} repository ID"))?; - if repo.split('/').count() > 2 { - return Err(app_error(format!( - "{label} repository ID must be `name` or `namespace/name`" - ))); - } - Ok(()) -} - -fn validate_single_component(value: &str, label: &str) -> AppResult<()> { - validate_hub_path(value, label)?; - if Path::new(value).components().count() != 1 { - return Err(app_error(format!( - "{label} must be a single path component" - ))); - } - Ok(()) -} - -fn validate_hub_path(value: &str, label: &str) -> AppResult<()> { - if value.trim().is_empty() { - return Err(app_error(format!("{label} must not be empty"))); - } - if value.contains(['\\', '\0', ':']) - || value.starts_with('~') - || value.split('/').any(str::is_empty) - { - return Err(app_error(format!( - "{label} must use portable repository-relative path syntax" - ))); - } - let path = Path::new(value); - if path.is_absolute() - || path - .components() - .any(|component| !matches!(component, Component::Normal(_))) - { - return Err(app_error(format!( - "{label} must not be absolute or contain `.` or `..` components" - ))); - } - for component in path.components() { - let Component::Normal(component) = component else { - unreachable!("non-normal components were rejected above"); - }; - let component = component.to_string_lossy(); - if component.ends_with([' ', '.']) - || component.chars().any(|character| { - character.is_control() || matches!(character, '<' | '>' | '"' | '|' | '?' | '*') - }) - { - return Err(app_error(format!( - "{label} contains characters that are not portable path syntax" - ))); - } - } - Ok(()) -} - -fn report_artifacts(result: &VideoResult) { - println!( - "wrote {} PPM frames to {} ({}x{} at {} fps)", - result.frame_count(), - result.frame_dir().display(), - result.width(), - result.height(), - result.fps() - ); - println!( - "wrote WAV audio to {} ({} Hz)", - result.audio_path().display(), - result.sample_rate() - ); -} - -fn mux_mp4( - frame_dir: &Path, - audio_path: &Path, - fps: u32, - output: &Path, - ffmpeg: &Path, -) -> AppResult<()> { - let file_name = output.file_name().ok_or_else(|| { - app_error(format!( - "--output-mp4 `{}` has no output file name", - output.display() - )) - })?; - let requested_parent = output - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - let canonical_parent = fs::canonicalize(requested_parent).map_err(|error| { - app_error(format!( - "could not safely resolve --output-mp4 parent `{}` for muxing: {error}", - requested_parent.display() - )) - })?; - if !fs::metadata(&canonical_parent) - .map_err(|error| { - app_error(format!( - "could not inspect resolved --output-mp4 parent `{}` for muxing: {error}", - canonical_parent.display() - )) - })? - .is_dir() - { - return Err(app_error(format!( - "--output-mp4 parent `{}` is not a directory", - requested_parent.display() - ))); - } - let destination = canonical_parent.join(file_name); - let mut private_output = PrivateMuxTarget::create(&canonical_parent)?; - - let frame_pattern = { - // Escape the directory, not ffmpeg's frame-number placeholder. - #[cfg(unix)] - let frame_dir = { - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - let mut bytes = Vec::new(); - for &b in frame_dir.as_os_str().as_bytes() { - bytes.push(b); - if b == b'%' { - bytes.push(b); - } - } - PathBuf::from(std::ffi::OsString::from_vec(bytes)) - }; - frame_dir.join("frame_%06d.ppm") - }; - let argv = compose_video_mux_argv( - &VideoMuxParams::new(frame_pattern, private_output.path()) - .audio_path(audio_path) - .fps(fps), - ) - .map_err(|error| app_error(format!("could not compose ffmpeg arguments: {error}")))?; - let mut arguments = argv.into_args(); - if arguments.is_empty() { - return Err(app_error("ffmpeg argument composer returned an empty argv")); - } - let overwrite_policy = arguments - .get_mut(1) - .filter(|argument| argument.as_os_str() == "-y") - .ok_or_else(|| { - app_error("ffmpeg argument composer did not return the expected overwrite policy") - })?; - *overwrite_policy = "-n".into(); - - eprintln!( - "Running ffmpeg from this example to create a private MP4 for `{}`...", - output.display() - ); - let status = Command::new(ffmpeg) - .args(&arguments[1..]) - .status() - .map_err(|error| { - if error.kind() == std::io::ErrorKind::NotFound { - app_error(format!( - "ffmpeg executable `{}` was not found; install ffmpeg or pass --ffmpeg PATH (the PPM/WAV artifacts remain in `{}`)", - ffmpeg.display(), - frame_dir.display() - )) - } else { - app_error(format!( - "could not start ffmpeg executable `{}`: {error} (the PPM/WAV artifacts remain in `{}`)", - ffmpeg.display(), - frame_dir.display() - )) - } - })?; - let captured_output = private_output.capture_output(); - if !status.success() { - if let Err(error) = captured_output { - eprintln!( - "warning: could not establish ownership of ffmpeg's private output `{}` for cleanup: {error}", - private_output.path().display() - ); - } - let outcome = status.code().map_or_else( - || "after a signal".to_owned(), - |code| format!("with status {code}"), - ); - return Err(app_error(format!( - "ffmpeg exited {outcome}; the PPM/WAV artifacts remain in `{}`", - frame_dir.display() - ))); - } - if !captured_output.map_err(|error| { - app_error(format!( - "could not establish ownership of ffmpeg's private MP4 output `{}`: {error}", - private_output.path().display() - )) - })? { - return Err(app_error(format!( - "ffmpeg succeeded without creating its private MP4 output `{}`", - private_output.path().display() - ))); - } - - match fs::hard_link(private_output.path(), &destination) { - Ok(()) => {} - Err(error) if error.kind() == io::ErrorKind::AlreadyExists => { - return Err(app_error(format!( - "--output-mp4 `{}` appeared before atomic publication; refusing to replace the existing entry", - output.display() - ))); - } - Err(error) => { - return Err(app_error(format!( - "could not atomically publish --output-mp4 `{}` with a same-filesystem hard link from its private mux: {error}; the destination filesystem must support hard links", - output.display() - ))); - } - } - private_output.cleanup().map_err(|error| { - app_error(format!( - "published --output-mp4 `{}` but could not remove its private temporary link `{}`: {error}", - output.display(), - private_output.path().display() - )) - })?; - - println!("wrote MP4 to {}", output.display()); - Ok(()) -} - -fn app_error(message: impl Into) -> AppError { - AppError(message.into()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::cell::Cell; - use std::sync::atomic::{AtomicU64, Ordering}; - use std::sync::Mutex; - - static NEXT_TEMP: AtomicU64 = AtomicU64::new(0); - static ENV_LOCK: Mutex<()> = Mutex::new(()); - - struct TempDir(PathBuf); - - struct EnvVarGuard { - key: &'static str, - previous: Option, - } - - impl EnvVarGuard { - fn set(key: &'static str, value: &Path) -> Self { - let previous = env::var_os(key); - env::set_var(key, value); - Self { key, previous } - } - } - - impl Drop for EnvVarGuard { - fn drop(&mut self) { - if let Some(value) = &self.previous { - env::set_var(self.key, value); - } else { - env::remove_var(self.key); - } - } - } - - impl TempDir { - fn new() -> Self { - loop { - let id = NEXT_TEMP.fetch_add(1, Ordering::Relaxed); - let path = env::temp_dir().join(format!( - "vllm-cpp-video-generate-{}-{id}", - std::process::id() - )); - match fs::create_dir(&path) { - Ok(()) => return Self(path), - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} - Err(error) => panic!("could not create temporary directory: {error}"), - } - } - } - } - - impl Drop for TempDir { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } - } - - fn parse(extra: &[&str]) -> Args { - let mut arguments = vec!["video_generate", "--prompt", "a test prompt"]; - arguments.extend_from_slice(extra); - Args::try_parse_from(arguments).expect("valid arguments") - } - - fn pcm_format(format_tag: u16, channels: u16, sample_rate: u32, bits: u16) -> Vec { - let mut fields = Vec::with_capacity(16); - fields.extend_from_slice(&format_tag.to_le_bytes()); - fields.extend_from_slice(&channels.to_le_bytes()); - fields.extend_from_slice(&sample_rate.to_le_bytes()); - fields.extend_from_slice(&0_u32.to_le_bytes()); - fields.extend_from_slice(&0_u16.to_le_bytes()); - fields.extend_from_slice(&bits.to_le_bytes()); - fields - } - - fn wav_fixture(chunks: Vec<([u8; 4], Vec)>) -> Vec { - let mut wav = b"RIFF\0\0\0\0WAVE".to_vec(); - for (id, body) in chunks { - wav.extend_from_slice(&id); - wav.extend_from_slice( - &u32::try_from(body.len()) - .expect("small fixture") - .to_le_bytes(), - ); - wav.extend_from_slice(&body); - if body.len() % 2 != 0 { - wav.push(0); - } - } - let riff_len = u32::try_from(wav.len() - 8).expect("small fixture"); - wav[4..8].copy_from_slice(&riff_len.to_le_bytes()); - wav - } - - fn write_wav(path: &Path, chunks: Vec<([u8; 4], Vec)>) { - fs::write(path, wav_fixture(chunks)).expect("write WAV fixture"); - } - - #[test] - fn smoke_defaults_use_verified_hugging_face_artifacts() { - let args = parse(&[]); - assert_eq!(args.partition, PartitionArg::Fl2va); - assert_eq!(args.device, DeviceArg::Cuda); - assert_eq!( - (args.width, args.height, args.frames, args.steps), - (256, 256, 5, 2) - ); - - let sources = artifact_sources(&args); - assert_eq!(sources[0].repo, GGUF_REPOSITORY); - assert_eq!(sources[0].revision, DEFAULT_REVISION); - assert_eq!(sources[0].path, FL2VA_DIT); - assert_eq!(args.encoder_repo, GGUF_REPOSITORY); - assert_eq!(args.encoder_revision, DEFAULT_REVISION); - assert_eq!(args.encoder_file, ENCODER); - assert_eq!(sources[1].repo, GGUF_REPOSITORY); - assert_eq!(sources[1].revision, DEFAULT_REVISION); - assert_eq!(sources[1].path, ENCODER); - assert_eq!(sources[2].repo, VAE_REPOSITORY); - assert_eq!(sources[2].path, VIDEO_VAE); - assert_eq!(sources[3].path, AUDIO_VAE); - assert_eq!(sources[4].repo, CONFIG_REPOSITORY); - assert_eq!(sources[4].path, VIDEO_VAE_CONFIG); - assert_eq!(sources[5].path, AUDIO_VAE_CONFIG); - assert_eq!(sources[6].path, TOKENIZER); - validate_args(&args).expect("valid smoke defaults"); - generation_params(&args) - .validate() - .expect("safe API accepts smoke defaults"); - } - - #[test] - fn encoder_source_is_independent_and_identical_groups_coalesce() { - let defaults = artifact_sources(&parse(&[])); - let default_groups = source_revision_groups(&defaults); - assert_eq!(default_groups.len(), 3); - assert_eq!( - default_groups.get(&(GGUF_REPOSITORY.to_owned(), DEFAULT_REVISION.to_owned())), - Some(&0) - ); - let mut default_resolutions = 0; - let default_commits = resolve_source_revisions(&defaults, |source| { - default_resolutions += 1; - Ok::<_, std::convert::Infallible>(format!("{}@{}", source.repo, source.revision)) - }) - .expect("offline resolver cannot fail"); - assert_eq!(default_resolutions, 3); - assert_eq!( - default_commits.get(&(GGUF_REPOSITORY.to_owned(), DEFAULT_REVISION.to_owned())), - Some(&format!("{GGUF_REPOSITORY}@{DEFAULT_REVISION}")) - ); - - let dit_only = parse(&[ - "--gguf-repo", - "dit/repository", - "--gguf-revision", - "dit-revision", - ]); - assert_eq!(dit_only.encoder_repo, GGUF_REPOSITORY); - assert_eq!(dit_only.encoder_revision, DEFAULT_REVISION); - - let args = parse(&[ - "--gguf-repo", - "dit/repository", - "--gguf-revision", - "dit-revision", - "--encoder-repo", - "encoder/repository", - "--encoder-revision", - "encoder-revision", - "--encoder-file", - "nested/encoder-Q2_K.gguf", - ]); - let sources = artifact_sources(&args); - assert_eq!(sources[0].repo, "dit/repository"); - assert_eq!(sources[0].revision, "dit-revision"); - assert_eq!(sources[1].repo, "encoder/repository"); - assert_eq!(sources[1].revision, "encoder-revision"); - assert_eq!(sources[1].path, "nested/encoder-Q2_K.gguf"); - assert_eq!(source_revision_groups(&sources).len(), 4); - validate_artifact_sources(&sources).expect("independent sources are valid offline"); - - let mut resolved = Vec::new(); - let commits = resolve_source_revisions(&sources, |source| { - resolved.push((source.repo.clone(), source.revision.clone())); - Ok::<_, std::convert::Infallible>(format!("{}@{}", source.repo, source.revision)) - }) - .expect("offline resolver cannot fail"); - assert_eq!(resolved.len(), 4); - assert_eq!( - commits.get(&("dit/repository".to_owned(), "dit-revision".to_owned())), - Some(&"dit/repository@dit-revision".to_owned()) - ); - assert_eq!( - commits.get(&( - "encoder/repository".to_owned(), - "encoder-revision".to_owned() - )), - Some(&"encoder/repository@encoder-revision".to_owned()) - ); - } - - #[test] - fn help_lists_independent_encoder_source_options() { - let help = Args::try_parse_from(["video_generate", "--help"]) - .expect_err("--help exits through Clap") - .to_string(); - assert!(help.contains("--encoder-repo ")); - assert!(help.contains("--encoder-revision ")); - assert!(help.contains("--encoder-file ")); - } - - #[test] - fn rejects_fewer_than_two_denoising_steps() { - let args = parse(&["--steps", "1"]); - assert!(validate_args(&args) - .unwrap_err() - .to_string() - .contains("at least 2")); - } - - #[test] - fn device_build_guard_rejects_only_known_bundled_cuda_incompatibility() { - // These are the valid source-mode/CUDA combinations; system + cuda is - // rejected by the sys build contract before the example can compile. - for (bundled, cuda, rejects_cuda) in [ - (true, false, true), - (true, true, false), - (false, false, false), - ] { - assert_eq!( - validate_device_build_compatibility(DeviceArg::Cuda, bundled, cuda).is_err(), - rejects_cuda, - "bundled={bundled}, cuda={cuda}" - ); - validate_device_build_compatibility(DeviceArg::Cpu, bundled, cuda) - .expect("build guard does not reject CPU selection"); - } - } - - #[cfg(all(feature = "bundled", not(feature = "cuda")))] - #[test] - fn rejects_default_cuda_before_workdir_or_hub_side_effects() { - let _env_lock = ENV_LOCK.lock().expect("lock environment"); - let temp = TempDir::new(); - let isolated_hf_home = temp.0.join("hf-home"); - let _hf_home = EnvVarGuard::set("HF_HOME", &isolated_hf_home); - let workdir = temp.0.join("must-not-be-created"); - let mut args = parse(&[]); - args.workdir = workdir.clone(); - args.gguf_repo = "must/not/be/resolved".to_owned(); - validate_args(&args).expect("default CLI is semantically valid"); - generation_params(&args) - .validate() - .expect("default generation parameters are valid"); - - let error = run(args).expect_err("CUDA build guard must fail before setup"); - assert!(error - .to_string() - .contains("bundled build has CUDA disabled")); - assert!(!workdir.exists()); - assert!(!isolated_hf_home.exists()); - } - - #[cfg(all(feature = "bundled", not(feature = "cuda")))] - #[test] - fn rejects_default_cuda_before_locking_existing_workdir() { - let _env_lock = ENV_LOCK.lock().expect("lock environment"); - let temp = TempDir::new(); - let isolated_hf_home = temp.0.join("hf-home"); - let _hf_home = EnvVarGuard::set("HF_HOME", &isolated_hf_home); - let workdir = temp.0.join("existing-workdir"); - fs::create_dir(&workdir).expect("create existing workdir"); - let lock_path = workdir.join(WORKDIR_LOCK_FILE); - - for locked in [false, true] { - if locked { - fs::write(&lock_path, b"owned elsewhere").expect("write competing lock"); - } - let mut args = parse(&[]); - args.workdir = workdir.clone(); - args.gguf_repo = "must/not/be/resolved".to_owned(); - let error = run(args).expect_err("CUDA build guard precedes lock acquisition"); - assert!(error - .to_string() - .contains("bundled build has CUDA disabled")); - assert_eq!( - fs::read_dir(&workdir).expect("inspect workdir").count(), - usize::from(locked) - ); - if locked { - assert_eq!( - fs::read(&lock_path).expect("read competing lock"), - b"owned elsewhere" - ); - } - } - assert!(!isolated_hf_home.exists()); - } - - #[test] - fn rejects_cpu_gguf_before_workdir_or_hub_side_effects() { - let _env_lock = ENV_LOCK.lock().expect("lock environment"); - let temp = TempDir::new(); - let isolated_hf_home = temp.0.join("hf-home"); - let _hf_home = EnvVarGuard::set("HF_HOME", &isolated_hf_home); - - for (case, dit) in [ - ("default", None), - ("lower-q", Some("nested/minimax_h3_fl2va_pruned-Q2_K.gguf")), - ] { - let workdir = temp.0.join(format!("{case}-must-not-be-created")); - let mut args = parse(&["--device", "cpu"]); - args.workdir = workdir.clone(); - args.gguf_repo = "must/not/be/resolved".to_owned(); - args.dit_file = dit.map(str::to_owned); - - let error = run(args).expect_err("CPU GGUF must fail before setup"); - let message = error.to_string(); - assert!(message.contains("CPU MiniMax-H3 host forward requires F32")); - assert!(message.contains("lower quantization does not fix this routing")); - assert!(!workdir.exists()); - } - assert!(!isolated_hf_home.exists()); - } - - #[test] - fn cpu_dit_guard_accepts_safetensors_and_leaves_cuda_gguf_supported() { - validate_args(&parse(&[])).expect("CUDA GGUF remains structurally accepted"); - validate_args(&parse(&[ - "--device", - "cpu", - "--dit-file", - "nested/minimax_h3_fl2va_pruned_nvfp4.safetensors", - ])) - .expect("CPU Safetensors remains structurally accepted"); - - let mixed_case = parse(&[ - "--device", - "cpu", - "--dit-file", - "nested/minimax_h3_fl2va_pruned-Q2_K.GgUf", - ]); - assert!(validate_args(&mixed_case).is_err()); - } - - #[test] - fn ref2va_selects_matching_default_dit_and_requires_a_reference() { - let without_reference = parse(&["--partition", "ref2va"]); - assert!(validate_args(&without_reference) - .unwrap_err() - .to_string() - .contains("requires a reference")); - - let mut with_reference = without_reference; - with_reference.reference_audio = Some(PathBuf::from("reference.wav")); - assert_eq!(artifact_sources(&with_reference)[0].path, REF2VA_DIT); - } - - #[test] - fn rejects_known_opposite_dit_for_effective_partition() { - let nested_ref2va_dit = format!("nested/{REF2VA_DIT}"); - for dit in [REF2VA_DIT, nested_ref2va_dit.as_str()] { - let fl2va_with_ref2va_dit = parse(&["--dit-file", dit]); - assert!(validate_args(&fl2va_with_ref2va_dit) - .unwrap_err() - .to_string() - .contains("cannot serve --partition fl2va")); - } - - let nested_fl2va_dit = format!("nested/{FL2VA_DIT}"); - for dit in [FL2VA_DIT, nested_fl2va_dit.as_str()] { - let mut ref2va_with_fl2va_dit = parse(&["--partition", "ref2va", "--dit-file", dit]); - ref2va_with_fl2va_dit.reference_audio = Some(PathBuf::from("reference.wav")); - assert!(validate_args(&ref2va_with_fl2va_dit) - .unwrap_err() - .to_string() - .contains("cannot serve --partition ref2va")); - } - - validate_args(&parse(&["--dit-file", "nested/renamed.gguf"])) - .expect("arbitrarily renamed DiT remains the user's responsibility"); - } - - #[test] - fn rejects_partition_mismatches_before_hub_resolution() { - let mut fl2va_with_reference = parse(&[]); - fl2va_with_reference.reference_image = Some(PathBuf::from("reference.ppm")); - assert!(validate_args(&fl2va_with_reference) - .unwrap_err() - .to_string() - .contains("--partition ref2va")); - - let mut ref2va_with_keyframe = parse(&["--partition", "ref2va"]); - ref2va_with_keyframe.first_frame = Some(PathBuf::from("first.ppm")); - ref2va_with_keyframe.reference_audio = Some(PathBuf::from("reference.wav")); - assert!(validate_args(&ref2va_with_keyframe) - .unwrap_err() - .to_string() - .contains("cannot be combined")); - } - - #[test] - fn prepares_new_and_existing_workdirs_without_removing_content() { - let temp = TempDir::new(); - let new_workdir = temp.0.join("new").join("nested"); - drop(prepare_workdir(&new_workdir).expect("create writable workdir")); - assert!(new_workdir.is_dir()); - - let nonmatching_names = [ - "marker", - "audio.wav.bak", - "frame_00000.ppm", - "frame_00000x.ppm", - "frame_000000.ppm.bak", - "xframe_000000.ppm", - ]; - for name in nonmatching_names { - fs::write(temp.0.join(name), b"keep").expect("write nonmatching marker"); - } - drop(prepare_workdir(&temp.0).expect("accept writable existing directory")); - for name in nonmatching_names { - assert_eq!( - fs::read(temp.0.join(name)).expect("read nonmatching marker"), - b"keep" - ); - } - } - - #[test] - fn workdir_lock_excludes_concurrent_acquisition_and_cleans_up() { - let temp = TempDir::new(); - let lock_path = temp.0.join(WORKDIR_LOCK_FILE); - - let mut first = prepare_workdir(&temp.0).expect("first lock acquisition succeeds"); - assert!(fs::read_to_string(&lock_path) - .expect("read lock context") - .contains(&std::process::id().to_string())); - - let error = prepare_workdir(&temp.0).expect_err("second acquisition must fail"); - assert!(error.to_string().contains("already in use")); - assert!(lock_path.is_file()); - - first - .remove_owned_file() - .expect("explicit owned-lock cleanup succeeds"); - assert!(!lock_path.exists()); - - let reacquired = prepare_workdir(&temp.0).expect("acquire after normal cleanup"); - drop(reacquired); - assert!(!lock_path.exists()); - } - - #[test] - fn preserves_a_preexisting_workdir_lock() { - let temp = TempDir::new(); - let lock_path = temp.0.join(WORKDIR_LOCK_FILE); - fs::write(&lock_path, b"owned elsewhere").expect("write pre-existing lock"); - - let error = prepare_workdir(&temp.0).expect_err("pre-existing lock must exclude run"); - assert!(error.to_string().contains("already in use")); - assert_eq!( - fs::read(&lock_path).expect("read preserved lock"), - b"owned elsewhere" - ); - } - - #[cfg(unix)] - #[test] - fn dropping_a_removed_lock_does_not_remove_its_replacement() { - let temp = TempDir::new(); - let lock_path = temp.0.join(WORKDIR_LOCK_FILE); - let original = prepare_workdir(&temp.0).expect("acquire original lock"); - - fs::remove_file(&lock_path).expect("externally remove original lock path"); - fs::write(&lock_path, b"replacement owner").expect("install replacement lock"); - drop(original); - - assert_eq!( - fs::read(&lock_path).expect("replacement lock survives original guard drop"), - b"replacement owner" - ); - } - - #[cfg(unix)] - #[test] - fn preserves_a_preexisting_symlink_lock_without_following_it() { - use std::os::unix::fs::symlink; - - let temp = TempDir::new(); - let sentinel = temp.0.join("sentinel"); - let lock_path = temp.0.join(WORKDIR_LOCK_FILE); - fs::write(&sentinel, b"keep").expect("write symlink target"); - symlink(&sentinel, &lock_path).expect("create pre-existing lock symlink"); - - let error = prepare_workdir(&temp.0).expect_err("symlink lock must exclude run"); - assert!(error.to_string().contains("already in use")); - assert!(fs::symlink_metadata(&lock_path) - .expect("inspect preserved symlink") - .file_type() - .is_symlink()); - assert_eq!(fs::read(&sentinel).expect("read sentinel"), b"keep"); - } - - #[test] - fn early_failures_do_not_reach_hub_resolution() { - let temp = TempDir::new(); - let invalid_workdir = temp.0.join("must-not-be-created"); - let mut invalid_args = parse(&["--steps", "1"]); - invalid_args.workdir = invalid_workdir.clone(); - invalid_args.gguf_repo = "invalid/repository/shape".to_owned(); - let error = run(invalid_args).expect_err("argument validation fails first"); - assert!(error.to_string().contains("at least 2")); - assert!(!invalid_workdir.exists()); - - let lock_path = temp.0.join(WORKDIR_LOCK_FILE); - fs::write(&lock_path, b"owned elsewhere").expect("write competing lock"); - // Use a structurally valid CPU DiT selection to reach workdir validation - // even when this example is built without CUDA. No model is loaded. - let mut locked_args = parse(&["--device", "cpu", "--dit-file", "model.safetensors"]); - locked_args.workdir = temp.0.clone(); - locked_args.gguf_repo = "invalid/repository/shape".to_owned(); - let error = run(locked_args).expect_err("workdir lock fails before Hub resolution"); - assert!(error.to_string().contains("already in use")); - assert_eq!( - fs::read(&lock_path).expect("read competing lock"), - b"owned elsewhere" - ); - } - - #[test] - fn rejects_existing_audio_output_files_and_directories() { - let temp = TempDir::new(); - for entry_kind in ["file", "directory"] { - let workdir = temp.0.join(entry_kind); - fs::create_dir(&workdir).expect("create workdir fixture"); - let output = workdir.join("audio.wav"); - if entry_kind == "file" { - fs::write(&output, b"keep").expect("write audio collision"); - } else { - fs::create_dir(&output).expect("create audio directory collision"); - } - - let error = prepare_workdir(&workdir).expect_err("reject existing audio output"); - assert!(error.to_string().contains("audio.wav")); - assert!(!workdir.join(WORKDIR_LOCK_FILE).exists()); - if entry_kind == "file" { - assert_eq!(fs::read(&output).expect("read audio collision"), b"keep"); - } else { - assert!(output.is_dir()); - } - } - } - - #[test] - fn rejects_existing_frame_output_files_and_directories() { - let temp = TempDir::new(); - assert!(is_native_frame_output_name("frame_000000.ppm")); - assert!(is_native_frame_output_name("frame_1000000.ppm")); - - for entry_kind in ["file", "directory"] { - let workdir = temp.0.join(entry_kind); - fs::create_dir(&workdir).expect("create workdir fixture"); - let output = workdir.join("frame_000000.ppm"); - if entry_kind == "file" { - fs::write(&output, b"keep").expect("write frame collision"); - } else { - fs::create_dir(&output).expect("create frame directory collision"); - } - - let error = prepare_workdir(&workdir).expect_err("reject existing frame output"); - assert!(error.to_string().contains("frame_000000.ppm")); - assert!(!workdir.join(WORKDIR_LOCK_FILE).exists()); - if entry_kind == "file" { - assert_eq!(fs::read(&output).expect("read frame collision"), b"keep"); - } else { - assert!(output.is_dir()); - } - } - } - - #[test] - fn rejects_file_and_invalid_parent_as_workdir() { - let temp = TempDir::new(); - let file = temp.0.join("not-a-directory"); - fs::write(&file, b"content").expect("write fixture"); - assert!(prepare_workdir(&file) - .unwrap_err() - .to_string() - .contains("is not a directory")); - - let child = file.join("child"); - assert!(prepare_workdir(&child) - .unwrap_err() - .to_string() - .contains("could not inspect or prepare --workdir")); - } - - #[test] - fn accepts_native_compatible_reference_wav_layouts() { - let temp = TempDir::new(); - let fixtures = [ - ( - "mono.wav", - vec![ - (*b"fmt ", pcm_format(1, 1, 32_000, 16)), - (*b"data", vec![0, 0]), - ], - ), - ( - "stereo.wav", - vec![ - (*b"fmt ", pcm_format(1, 2, 32_000, 16)), - (*b"data", vec![0, 0, 1, 0]), - ], - ), - ( - "padded-unknown.wav", - vec![ - (*b"fmt ", pcm_format(1, 3, 32_000, 16)), - (*b"JUNK", vec![1, 2, 3]), - (*b"data", vec![0, 0, 1, 0, 2, 0]), - ], - ), - ]; - for (name, chunks) in fixtures { - let path = temp.0.join(name); - write_wav(&path, chunks); - validate_reference_wav(&path).expect("native-compatible WAV"); - } - } - - #[test] - fn rejects_incompatible_reference_wav_formats() { - let temp = TempDir::new(); - for (name, format, expected) in [ - ( - "wrong-rate.wav", - pcm_format(1, 2, 44_100, 16), - "exactly 32000 Hz", - ), - ( - "compressed.wav", - pcm_format(3, 2, 32_000, 16), - "format tag 1", - ), - ( - "wrong-depth.wav", - pcm_format(1, 2, 32_000, 24), - "16-bit PCM", - ), - ( - "zero-channels.wav", - pcm_format(1, 0, 32_000, 16), - "zero channels", - ), - ] { - let path = temp.0.join(name); - write_wav( - &path, - vec![(*b"fmt ", format), (*b"data", vec![0, 0, 0, 0])], - ); - assert!(validate_reference_wav(&path) - .unwrap_err() - .to_string() - .contains(expected)); - } - } - - #[test] - fn rejects_missing_truncated_and_empty_reference_wav_chunks() { - let temp = TempDir::new(); - let mut truncated_fmt_header = wav_fixture(vec![(*b"JUNK", vec![0; 17])]); - truncated_fmt_header.extend_from_slice(b"fmt \x10\0"); - let mut truncated_fmt_body = wav_fixture(vec![(*b"JUNK", vec![0; 8])]); - truncated_fmt_body.extend_from_slice(b"fmt \x10\0\0\0\x01\0\x01\0\0\x7d\0\0"); - let mut truncated_data_header = wav_fixture(vec![ - (*b"fmt ", pcm_format(1, 1, 32_000, 16)), - (*b"JUNK", vec![1]), - ]); - truncated_data_header.extend_from_slice(b"data\x02\0\0"); - let mut truncated_data_body = wav_fixture(vec![ - (*b"fmt ", pcm_format(1, 2, 32_000, 16)), - (*b"data", vec![0, 0]), - ]); - truncated_data_body[40..44].copy_from_slice(&4_u32.to_le_bytes()); - let mut oversized_unknown = b"RIFF\0\0\0\0WAVEJUNK".to_vec(); - oversized_unknown.extend_from_slice(&u32::MAX.to_le_bytes()); - oversized_unknown.resize(44, 0); - - let fixtures = [ - ("short.wav", b"RIFF".to_vec(), "too short"), - ("not-wave.wav", vec![b'X'; 44], "not a RIFF/WAVE file"), - ( - "missing-fmt.wav", - wav_fixture(vec![(*b"JUNK", vec![0; 20]), (*b"data", vec![0, 0])]), - "no complete fmt chunk", - ), - ( - "truncated-fmt-header.wav", - truncated_fmt_header, - "no complete fmt chunk", - ), - ( - "truncated-fmt-body.wav", - truncated_fmt_body, - "no complete fmt chunk", - ), - ( - "missing-data.wav", - wav_fixture(vec![ - (*b"fmt ", pcm_format(1, 1, 32_000, 16)), - (*b"JUNK", vec![0, 0]), - ]), - "no data chunk", - ), - ( - "truncated-data-header.wav", - truncated_data_header, - "no data chunk", - ), - ( - "data-before-fmt.wav", - wav_fixture(vec![ - (*b"JUNK", vec![0; 12]), - (*b"data", vec![0, 0]), - (*b"fmt ", pcm_format(1, 1, 32_000, 16)), - ]), - "no complete fmt chunk", - ), - ( - "empty-data.wav", - wav_fixture(vec![ - (*b"fmt ", pcm_format(1, 1, 32_000, 16)), - (*b"data", Vec::new()), - ]), - "no complete source frame", - ), - ( - "incomplete-stereo-frame.wav", - wav_fixture(vec![ - (*b"fmt ", pcm_format(1, 2, 32_000, 16)), - (*b"data", vec![0, 0]), - ]), - "no complete source frame", - ), - ( - "truncated-data-body.wav", - truncated_data_body, - "no complete source frame", - ), - ( - "oversized-unknown.wav", - oversized_unknown, - "no complete fmt chunk", - ), - ]; - for (name, bytes, expected) in fixtures { - let path = temp.0.join(name); - fs::write(&path, bytes).expect("write malformed WAV fixture"); - assert!(validate_reference_wav(&path) - .unwrap_err() - .to_string() - .contains(expected)); - } - } - - #[test] - fn invalid_reference_wav_fails_before_workdir_or_hub_side_effects() { - let _env_lock = ENV_LOCK.lock().expect("lock environment"); - let temp = TempDir::new(); - let reference = temp.0.join("invalid.wav"); - let workdir = temp.0.join("must-not-be-created"); - let isolated_hf_home = temp.0.join("hf-home"); - let _hf_home = EnvVarGuard::set("HF_HOME", &isolated_hf_home); - fs::write(&reference, b"not a wave").expect("write invalid reference"); - - let mut args = parse(&["--partition", "ref2va"]); - args.reference_audio = Some(reference); - args.workdir = workdir.clone(); - args.gguf_repo = "must/not/be/resolved".to_owned(); - let error = run(args).expect_err("invalid WAV must fail before setup"); - assert!(error.to_string().contains("invalid --reference-audio")); - assert!(!workdir.exists()); - assert!(!isolated_hf_home.exists()); - } - - #[test] - fn existing_mp4_fails_before_workdir_or_hub_side_effects() { - let _env_lock = ENV_LOCK.lock().expect("lock environment"); - let temp = TempDir::new(); - let output = temp.0.join("existing.mp4"); - let workdir = temp.0.join("must-not-be-created"); - let isolated_hf_home = temp.0.join("hf-home"); - let _hf_home = EnvVarGuard::set("HF_HOME", &isolated_hf_home); - fs::write(&output, b"keep").expect("write existing output"); - - let mut args = parse(&[]); - args.output_mp4 = Some(output.clone()); - args.workdir = workdir.clone(); - args.gguf_repo = "must/not/be/resolved".to_owned(); - let error = run(args).expect_err("existing MP4 must fail before setup"); - assert!(error.to_string().contains("already exists")); - assert_eq!(fs::read(output).expect("existing output remains"), b"keep"); - assert!(!workdir.exists()); - assert!(!isolated_hf_home.exists()); - } - - #[test] - fn accepts_a_nonexistent_mp4_in_a_resolvable_parent() { - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - let output = temp.0.join("new.mp4"); - validate_mp4_output(&output, &workdir).expect("safe new output"); - assert!(!output.exists()); - } - - #[test] - fn rejects_every_kind_of_existing_mp4_entry() { - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - - let file = temp.0.join("file.mp4"); - fs::write(&file, b"keep").expect("write existing output"); - let directory = temp.0.join("directory.mp4"); - fs::create_dir(&directory).expect("create existing output directory"); - for output in [&file, &directory] { - assert!(validate_mp4_output(output, &workdir) - .unwrap_err() - .to_string() - .contains("already exists")); - } - assert_eq!(fs::read(file).expect("existing file remains"), b"keep"); - assert!(directory.is_dir()); + #[test] + fn ref2va_requires_a_local_reference_image() { + let temp = TempDir::new(); + let mut args = temp.args(&[]); + args.reference_image = Some(args.dit.clone()); + assert!(prepare(&args).is_err()); + assert!(!args.output_dir.exists()); + args.partition = PartitionArg::Ref2va; + args.reference_image = None; + assert!(prepare(&args).is_err()); + assert!(!args.output_dir.exists()); + args.reference_image = Some(args.dit.clone()); + prepare(&args).unwrap(); } #[cfg(unix)] #[test] - fn rejects_existing_mp4_symlinks_including_dangling_links() { - use std::os::unix::fs::symlink; - - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - let target = temp.0.join("target"); - fs::write(&target, b"keep").expect("write symlink target"); - let existing_link = temp.0.join("existing.mp4"); - symlink(&target, &existing_link).expect("create output symlink"); - let dangling_link = temp.0.join("dangling.mp4"); - symlink(temp.0.join("missing"), &dangling_link).expect("create dangling output symlink"); - - for output in [&existing_link, &dangling_link] { - assert!(validate_mp4_output(output, &workdir) - .unwrap_err() - .to_string() - .contains("already exists")); - assert!(fs::symlink_metadata(output) - .expect("symlink remains") - .file_type() - .is_symlink()); - } - assert_eq!(fs::read(target).expect("target remains"), b"keep"); - } - - #[test] - fn rejects_direct_reserved_workdir_mp4_aliases() { - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - for name in [ - WORKDIR_LOCK_FILE, - "audio.wav", - "frame_000000.ppm", - "frame_1000000.ppm", - ] { - let error = validate_mp4_output(&workdir.join(name), &workdir) - .expect_err("reserved output must be rejected"); - assert!(error.to_string().contains("reserved workdir output")); - } - } - - #[cfg(unix)] - #[test] - fn rejects_reserved_mp4_aliases_through_a_symlinked_parent() { - use std::os::unix::fs::symlink; - - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - let alias = temp.0.join("workdir-alias"); - symlink(&workdir, &alias).expect("create workdir alias"); - for name in [WORKDIR_LOCK_FILE, "audio.wav", "frame_000000.ppm"] { - let error = validate_mp4_output(&alias.join(name), &workdir) - .expect_err("symlink-parent alias must be rejected"); - assert!(error.to_string().contains("reserved workdir output")); - } - } - - #[test] - fn rejects_unresolvable_mp4_parents() { - let temp = TempDir::new(); - let workdir = temp.0.join("workdir"); - fs::create_dir(&workdir).expect("create workdir"); - let output = temp.0.join("missing-parent").join("output.mp4"); - assert!(validate_mp4_output(&output, &workdir) - .unwrap_err() - .to_string() - .contains("could not safely resolve")); - assert!(!output.exists()); - } - - #[test] - fn reuses_commit_snapshot_from_normal_mutable_revision_cache() { - let temp = TempDir::new(); - let cache = Cache::new(temp.0.join("hub")); - let repo_id = "owner/model"; - let commit = "0123456789abcdef"; - let artifact_path = "nested/model.gguf"; - let repo = cache.repo(Repo::new(repo_id.to_owned(), RepoType::Model)); - repo.create_ref(commit).expect("create mutable main ref"); - let snapshot_path = repo.pointer_path(commit).join(artifact_path); - fs::create_dir_all(snapshot_path.parent().expect("snapshot parent")) - .expect("create snapshot directory"); - fs::write(&snapshot_path, b"cached artifact").expect("write snapshot artifact"); - - let commit_ref = cache - .path() - .join(Repo::new(repo_id.to_owned(), RepoType::Model).folder_name()) - .join("refs") - .join(commit); - assert!(!commit_ref.exists(), "fixture must not contain refs/"); - - let fallback_called = Cell::new(false); - let resolved = cached_commit_snapshot_or_else( - &cache, - repo_id, - commit, - artifact_path, - || -> AppResult { - fallback_called.set(true); - Err(app_error("fallback must not run for a cached snapshot")) - }, - ) - .expect("reuse commit snapshot"); - - assert_eq!(resolved, snapshot_path); - assert!(!fallback_called.get()); - } - - #[test] - fn missing_or_non_file_commit_snapshot_uses_fallback() { + fn existing_output_symlinks_are_not_followed() { let temp = TempDir::new(); - let cache = Cache::new(temp.0.join("hub")); - let repo_id = "owner/model"; - let commit = "0123456789abcdef"; - let fallback_path = temp.0.join("downloaded.gguf"); - fs::write(&fallback_path, b"downloaded artifact").expect("write fallback artifact"); - - for artifact_path in ["missing/model.gguf", "directory/model.gguf"] { - if artifact_path.starts_with("directory/") { - let directory_path = cache - .repo(Repo::new(repo_id.to_owned(), RepoType::Model)) - .pointer_path(commit) - .join(artifact_path); - fs::create_dir_all(&directory_path).expect("create non-file snapshot entry"); - } - - let fallback_called = Cell::new(false); - let resolved = cached_commit_snapshot_or_else( - &cache, - repo_id, - commit, - artifact_path, - || -> AppResult { - fallback_called.set(true); - Ok(fallback_path.clone()) - }, - ) - .expect("use fallback"); - - assert_eq!(resolved, fallback_path); - assert!(fallback_called.get()); - } - } - - #[test] - fn selects_only_nonempty_environment_hf_token() { - assert_eq!(selected_hf_token(None), None); - assert_eq!(selected_hf_token(Some(" \t".to_owned())), None); - assert_eq!( - selected_hf_token(Some(" token ".to_owned())), - Some(" token ".to_owned()) - ); - } - - #[cfg(unix)] - fn write_fake_ffmpeg(path: &Path, body: &str) { - use std::os::unix::fs::PermissionsExt; - - fs::write(path, body).expect("write fake ffmpeg"); - let mut permissions = fs::metadata(path) - .expect("fake ffmpeg metadata") - .permissions(); - permissions.set_mode(0o700); - fs::set_permissions(path, permissions).expect("make fake ffmpeg executable"); - } - - fn private_mux_paths(parent: &Path) -> Vec { - fs::read_dir(parent) - .expect("read mux parent") - .map(|entry| entry.expect("read mux-parent entry").path()) - .filter(|path| { - path.file_name() - .is_some_and(|name| name.to_string_lossy().starts_with(PRIVATE_MUX_DIR_PREFIX)) - }) - .collect() - } - - #[cfg(unix)] - #[test] - fn mux_mp4_uses_private_output_preserves_arguments_and_cleans_up() { - use std::ffi::{OsStr, OsString}; - use std::os::unix::ffi::{OsStrExt, OsStringExt}; - - let temp = TempDir::new(); - let frame_dir = temp - .0 - .join(OsString::from_vec(b"50% frames \xff %06d".to_vec())); - let audio = frame_dir.join("audio input.wav"); - let output = temp.0.join("output video.mp4"); - let executable = temp.0.join("fake-ffmpeg.sh"); - let capture = temp.0.join("fake-ffmpeg.sh.args"); - - write_fake_ffmpeg( - &executable, - "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$0.args\"\n[ \"$1\" = '-n' ] || exit 97\nfor output do :; done\n[ ! -e \"$output\" ] || exit 98\nprintf private-mp4 > \"$output\"\n", - ); - validate_mp4_output(&output, &frame_dir).expect("output passes preflight"); - mux_mp4(&frame_dir, &audio, 24, &output, &executable).expect("fake ffmpeg success"); - - let captured_bytes = fs::read(&capture).expect("read captured arguments"); - let captured = captured_bytes - .strip_suffix(b"\n") - .unwrap() - .split(|&b| b == b'\n') - .map(OsStr::from_bytes) - .collect::>(); - let private_path = PathBuf::from(*captured.last().expect("captured private output")); - let pattern = temp.0.join(OsString::from_vec( - b"50%% frames \xff %%06d/frame_%06d.ppm".to_vec(), - )); - let mut expected = compose_video_mux_argv( - &VideoMuxParams::new(pattern, &private_path) - .audio_path(&audio) - .fps(24), - ) - .expect("compose expected arguments") - .into_args() - .into_iter() - .skip(1) - .collect::>(); - assert_eq!(expected.first().and_then(|arg| arg.to_str()), Some("-y")); - expected[0] = "-n".into(); - assert_eq!( - captured, - expected - .iter() - .map(|argument| argument.as_os_str()) - .collect::>() - ); - assert!(!captured - .iter() - .any(|argument| *argument == output.as_os_str())); - assert_eq!( - private_path.file_name().and_then(|name| name.to_str()), - Some("mux.mp4") - ); - assert!(private_path - .parent() - .and_then(Path::file_name) - .is_some_and(|name| name.to_string_lossy().starts_with(PRIVATE_MUX_DIR_PREFIX))); - assert_eq!( - fs::read(&output).expect("read atomically published output"), - b"private-mp4" - ); - assert!(private_mux_paths(&temp.0).is_empty()); - } - - #[cfg(unix)] - #[test] - fn mux_mp4_refuses_destination_created_after_preflight() { - let temp = TempDir::new(); - let frame_dir = temp.0.join("frames"); - let audio = frame_dir.join("audio.wav"); - let executable = temp.0.join("fake-racing-ffmpeg.sh"); - let mut output = executable.as_os_str().to_os_string(); - output.push(".raced-output"); - let output = PathBuf::from(output); - - write_fake_ffmpeg( - &executable, - "#!/bin/sh\nfor output do :; done\nprintf private-mp4 > \"$output\"\nprintf raced-destination > \"$0.raced-output\"\n", - ); - validate_mp4_output(&output, &frame_dir).expect("output passes preflight"); - let error = mux_mp4(&frame_dir, &audio, 24, &output, &executable) - .expect_err("atomic publication must refuse raced destination"); - - assert!(error - .to_string() - .contains("appeared before atomic publication")); - assert_eq!( - fs::read(&output).expect("read preserved raced destination"), - b"raced-destination" - ); - assert!(private_mux_paths(&temp.0).is_empty()); - } - - #[cfg(unix)] - #[test] - fn mux_mp4_failure_cleans_private_output_and_leaves_no_destination() { - let temp = TempDir::new(); - let frame_dir = temp.0.join("frames"); - let audio = frame_dir.join("audio.wav"); - let output = temp.0.join("must-not-exist.mp4"); - let executable = temp.0.join("fake-failing-ffmpeg.sh"); - - write_fake_ffmpeg( - &executable, - "#!/bin/sh\nfor output do :; done\nprintf partial > \"$output\"\nexit 23\n", - ); - let error = mux_mp4(&frame_dir, &audio, 24, &output, &executable) - .expect_err("ffmpeg failure must propagate"); - - assert!(error.to_string().contains("status 23")); - assert!(!output.exists()); - assert!(private_mux_paths(&temp.0).is_empty()); - } - - #[test] - fn rejects_unsafe_hub_inputs_without_network_access() { - for (flag, value) in [ - ("--encoder-repo", "../model"), - ("--encoder-revision", "/revision"), - ("--encoder-file", "owner//encoder.gguf"), - ] { - let args = parse(&[flag, value]); - let error = validate_artifact_sources(&artifact_sources(&args)) - .expect_err("unsafe encoder source must fail before network access"); - assert!(error.to_string().contains("prompt encoder")); - } - - for (value, label) in [ - ("", "empty"), - ("../model", "parent"), - ("/model", "absolute"), - ("owner//model", "empty component"), - ("model\\file", "backslash"), - ] { - assert!( - validate_hub_path(value, label).is_err(), - "unexpectedly accepted {label}" - ); - } - validate_repo_id("owner/model", "test").expect("valid repository"); - validate_hub_path("nested/model.safetensors", "test").expect("valid artifact path"); + let args = temp.args(&[]); + let missing = temp.0.join("absent"); + std::os::unix::fs::symlink(&missing, &args.output_dir).unwrap(); + assert!(prepare(&args).is_err()); + assert!(args.output_dir.is_symlink()); + assert!(!missing.exists()); } } diff --git a/vllm-cpp/src/engine.rs b/vllm-cpp/src/engine.rs index d932770..96ee849 100644 --- a/vllm-cpp/src/engine.rs +++ b/vllm-cpp/src/engine.rs @@ -2142,7 +2142,6 @@ fn transcription_from_raw(raw: &ffi::vllm_transcription) -> Result