packaging: the picture reader is built per CUDA variant and checked against the package's archs (I1, I2)

build_cuda_linux.sh builds strata-vision for the same architectures as the engine (cuda12 includes sm_60/61/70) and records
vision_archs in BUILD.json; the Dockerfile records it too. packaging/build_vision_windows.bat does the same for the Windows
cuda12 and cuda13 zips (they shared one reader built before Pascal support, #3669). package_engine.py refuses a CUDA package whose
reader's vision_archs do not cover its archs and a macOS package without strata-vision (#3697); --allow-stale-vision and
--no-picture-reader are the explicit exits; --stamp-vision records a hand-built reader. ENGINE-COMPONENT.md describes the release steps.
This commit is contained in:
Niko1221 committed 2026-10-22 12:56:30 +02:00
1 parent c40a03e6dc
commit e2858b9855
6 files changed
+291 -9

No files matched your search

+2 -1
View File
@@ -56,7 +56,8 @@ RUN set -e; \
ver="${ENGINE_VERSION:-$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)}"; \
cudafull=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1); cudamajor=${cudafull%%.*}; \
if [ "$cudamajor" = 13 ]; then mindriver=580.65; else mindriver=575.51; fi; \
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true}" > /stage/BUILD.json; \
vision_archs=""; [ -f /stage/strata-vision ] && vision_archs=", \"vision_archs\": \"$CUDA_ARCHITECTURES\""; \
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true${vision_archs}}" > /stage/BUILD.json; \
python3 packaging/collect_libs.py --binary build/strata --search /usr/local/cuda/lib64 --out /stage --also 'libcublasLt.so*'; \
archs=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do archs="$archs --arch sm_$a"; done; \
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
+19 -5
View File
@@ -96,16 +96,16 @@ the manifest file (which holds every file's hash), used as the package's digest
| Package | Built on | Contents | Notes |
|---|---|---|---|
| `windows-x86_64` cuda, variant `cuda13` | the owner PC, the same cmake line as the release zips (CUDA 13.0, `75;86;89;120` real + `120` PTX, `STRATA_PORTABLE=ON`) | `strata.exe`, `strata_kq.dll`, `strata-vision.exe` (reused, unchanged since 0.1.44), `BUILD.json` | `min_driver` nvidia 580.65 |
| `windows-x86_64` cuda, variant `cuda12` | the owner PC (CUDA 12.9, `60;61;70;75;80;86;89` real + `89` PTX, `STRATA_EXPERIMENTAL_SM60=ON`) | the same four files | experimental, `min_driver` nvidia 575.51; the CUDA 13 build wins when both fit |
| `windows-x86_64` cuda, variant `cuda13` | the owner PC, the same cmake line as the release zips (CUDA 13.0, `75;86;89;120` real + `120` PTX, `STRATA_PORTABLE=ON`) | `strata.exe`, `strata_kq.dll`, `strata-vision.exe` (built for this variant's archs by `packaging/build_vision_windows.bat cuda13 <stage>`, which writes `vision_archs` into BUILD.json), `BUILD.json` | `min_driver` nvidia 580.65 |
| `windows-x86_64` cuda, variant `cuda12` | the owner PC (CUDA 12.9, `60;61;70;75;80;86;89` real + `89` PTX, `STRATA_EXPERIMENTAL_SM60=ON`) | the same four files, with its own `strata-vision.exe` (`packaging/build_vision_windows.bat cuda12 <stage>`: sm_60 .. sm_89; the CUDA 13 reader has no Pascal code, #3669) | experimental, `min_driver` nvidia 575.51; the CUDA 13 build wins when both fit |
| `windows-x86_64` hip | the owner PC, `tools/hip/build_windows.bat` (ROCm 10.2 nightly wheels) then `tools/hip/package_windows.py` | `strata.exe`, `strata-device.exe`, the ROCm DLLs and the rocBLAS/hipBLASLt kernels of the zip (`--tree`) | no Windows AMD test machine yet; with `STRATA_NO_VISION` unset it also builds the two picture readers (`strata-vision.exe` on the CPU, `strata-vision-hip.exe` on the GPU) |
| `linux-x86_64` cuda, variant `cuda13` | a Linux box with the CUDA 13 toolkit, `VARIANT=cuda13 packaging/build_cuda_linux.sh` | `strata`, `libstrata_kq.so`, `libcudart/libcublas/libcublasLt` (13.x) | the GPU driver's `libcuda` stays the PC's |
| `linux-x86_64` cuda, variant `cuda12` | a Linux box with CUDA 12.9, `VARIANT=cuda12 packaging/build_cuda_linux.sh` (`60;61;70;75;80;86;89` real + `89` PTX, `STRATA_EXPERIMENTAL_SM60=ON`) | the same files with the 12.x libraries | experimental, `min_driver` nvidia 575.51; the Pascal and Volta package (the 0.2.1 one lacked sm_60 / 61 / 70, #3503); `package_engine.py` refuses a cuda12 package without them |
| `linux-x86_64` cuda, variant `cuda13` | a Linux box with the CUDA 13 toolkit, `VARIANT=cuda13 packaging/build_cuda_linux.sh` | `strata`, `libstrata_kq.so`, `strata-vision` (built by the same script for the same archs, `BUILD_VISION=0` for a text-only package), `libcudart/libcublas/libcublasLt` (13.x) | the GPU driver's `libcuda` stays the PC's |
| `linux-x86_64` cuda, variant `cuda12` | a Linux box with CUDA 12.9, `VARIANT=cuda12 packaging/build_cuda_linux.sh` (`60;61;70;75;80;86;89` real + `89` PTX, `STRATA_EXPERIMENTAL_SM60=ON`) | the same files with the 12.x libraries (the reader has sm_60 / 61 / 70 too) | experimental, `min_driver` nvidia 575.51; the Pascal and Volta package (the 0.2.1 one lacked sm_60 / 61 / 70, #3503); `package_engine.py` refuses a cuda12 package without them |
| `linux-x86_64` hip | a box with ROCm (the Python wheels are enough) | `strata`, the HIP runtime, hipBLAS/hipBLASLt/rocBLAS, comgr + libLLVM, `rocblas/library` and `hipblaslt/library` for the listed gfx archs; `packaging/build_hip_linux.sh` builds it with both picture readers |
| `linux-x86_64` hip, variant `rx6000` (RX 6000, gfx1030 / gfx1031) | `VARIANT=rx6000 packaging/build_hip_linux.sh` in the jammy chroot, ROCm 7.13.0a20260515 `gfx103X-all` wheels | the same set as the other HIP packages, with `rocblas/library` for gfx1030 / gfx1031; no `hipblaslt/library` kernels exist for them and no `tuning/` table | built, not tested on a real RX 6000 card (the manifest notes say so). `windows-x86_64` hip, variant `rdna2`: the same archs from `tools/hip/build_windows.bat` with `STRATA_HIP_ARCHS=gfx1030;gfx1031`, packaged with `--variant rdna2`. A PC with an RX 7000 / 9000 card beside an RX 6000 one never takes it (`engine_component.hip_preference`) |
| `linux-x86_64` hip, gfx1103 (Radeon 780M / Phoenix iGPU) | `packaging/build_hip_gfx1103.sh` on the mini PC (TheRock gfx110X-all SDK) | the same set as the gfx1200/1201 package, with `rocblas/library` and `hipblaslt/library` for gfx1103 (600 MiB) | experimental, unvalidated: the iGPU resets now and then (MES REMOVE_QUEUE); never kill the engine mid-request; run configs set `STRATA_NO_ARENA_THP=1`. `engine_component.resolve` picks a HIP package only when its `archs` list a gfx target the Linux kfd topology reports |
| `linux-x86_64` sycl, `archs` `intel-arc` | a Linux box with oneAPI DPC++ 2026.1 and `patchelf`, `packaging/build_sycl_linux.sh` (JIT from SPIR-V; ocloc cannot build the Alchemist FP64 kernels ahead of time, so one JIT package serves A-series and B-series) | `strata`, libsycl, the Unified Runtime adapters, the oneMKL SYCL libraries, libimf / libsvml / libirng / libintlc (about 650 MiB); every ELF file has RUNPATH `$ORIGIN` (Intel's libraries ship without it), so `env -i ./strata --version` runs | the Level Zero loader and compute runtime are the PC's driver; the user must be in the `render` group (`strata doctor` warns). No Windows package: `BACKEND_MATRIX` allows sycl on `linux-x86_64` only |
| `macos-arm64` metal | an Apple Silicon Mac | `strata`, `metal/kernels/*.metal`, `metal/engine-msl/*.metal`, `metal/ggml-common.h`, `metal/data/*.bin` | the Metal engine compiles its kernel sources when it starts; since S1 it finds them beside the executable (`metal/src/resource_path.h`: environment variable, then `<exe dir>/metal/`, then the compiled-in source path) |
| `macos-arm64` metal | an Apple Silicon Mac | `strata`, `strata-vision` (`packaging/build_vision_mac.sh`; `package_engine.py` refuses a macOS package without it, #3697), `metal/kernels/*.metal`, `metal/engine-msl/*.metal`, `metal/ggml-common.h`, `metal/data/*.bin` | the Metal engine compiles its kernel sources when it starts; since S1 it finds them beside the executable (`metal/src/resource_path.h`: environment variable, then `<exe dir>/metal/`, then the compiled-in source path) |
`packaging/collect_libs.py` gathers the Linux libraries (an `ldd` closure with the dlopen'd extras named, kernel data filtered by
GPU architecture) into the folder `package_engine.py` packages; libraries the distribution provides are listed in
@@ -122,6 +122,20 @@ with the package). `packaging/stage_tuning.py` does it for the Linux build scrip
`STRATA_HIPBLASLT_TABLE=on|off` (or `"hipblaslt_table"` in the run config) overrides; unset it follows `AUTO_USES_TABLE`, which is off until the
owner decides, because a table changes the rounding of the prompt's GEMMs (docs/AMD_HIP.md, Tuning table). Tests: `tests/terminal/test_hipblaslt_tuning.py`.
## The picture reader must match the package
`package_engine.py` (the command line, not the library call) checks the reader before it packages:
* a CUDA package with `strata-vision` needs `vision_archs` in BUILD.json, and every `sm_NN` of the package must be in it. A reader reused from an
older build was the cause of #3669 (the cuda12 package on a Pascal card: the engine ran, the reader stopped with "device kernel image is
invalid"). `--allow-stale-vision` packages it anyway and prints which archs are missing;
* a macOS package without `strata-vision` is refused (`--no-picture-reader` ships a text-only Mac package on purpose);
* a CUDA package with no reader is allowed: it answers text only, the app says "the engine installed here does not include the picture reader
yet" and keeps Read pictures off.
`python packaging/package_engine.py --stamp-vision <stage> --vision-archs "60;61;70;75;80;86;89"` writes the record for a reader built by hand.
At run time a reader that still cannot start tells why (stderr tail, exit status, and for a kernel-image error the card hint).
## Picture readers in an AMD package
A HIP package carries two programs for pictures next to `strata` (`vision` in its BUILD.json): `strata-vision` is the CPU reader, which
+15 -1
View File
@@ -36,12 +36,26 @@ cmake -G Ninja -S . -B "$build" -DCMAKE_BUILD_TYPE=Release -DSTRATA_ENABLE_CUDA=
"-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual" -DSTRATA_GGML_DIR="$repo/third_party/llama.cpp"
cmake --build "$build" --target strata strata_kq -j "$(nproc)"
# The picture reader is built for the SAME architectures as the engine (#3669: a reader reused from an older build had no sm_60, and on
# a Pascal card the engine ran while the reader died with "device kernel image is invalid"). BUILD_VISION=0 ships a text-only package.
vision_dir=${BUILD_VISION_DIR:-$repo/build-vision-$variant}
if [ "${BUILD_VISION:-1}" = 1 ]; then
cmake -G Ninja -S tools/vision -B "$vision_dir" -DCMAKE_BUILD_TYPE=Release -DLLAMA_DIR="$repo/third_party/llama.cpp" \
-DSTRATA_VISION_CUDA=ON -DSTRATA_PORTABLE=ON -DGGML_CUDA_NCCL=OFF "-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual"
cmake --build "$vision_dir" --target strata-vision -j "$(nproc)"
fi
ver=$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)
stage=$(mktemp -d "${TMPDIR:-/tmp}/strata-engine-stage.XXXXXX")
trap 'rm -rf "$stage"' EXIT
cp "$build/strata" "$stage/"
[ -f "$build/libstrata_kq.so" ] && cp "$build/libstrata_kq.so" "$stage/"
echo "{\"version\": \"$ver\", \"source\": \"packaging/build_cuda_linux.sh\", \"archs\": \"$archs\", \"cuda\": \"$cudafull\", \"portable\": true}" > "$stage/BUILD.json"
vision_json=""
if [ -f "$vision_dir/bin/strata-vision" ]; then
cp "$vision_dir/bin/strata-vision" "$stage/"
vision_json=", \"vision_archs\": \"$archs\"" # package_engine.py refuses a reader whose archs do not cover the engine's
fi
echo "{\"version\": \"$ver\", \"source\": \"packaging/build_cuda_linux.sh\", \"archs\": \"$archs\", \"cuda\": \"$cudafull\", \"portable\": true$vision_json}" > "$stage/BUILD.json"
"$py" packaging/collect_libs.py --binary "$build/strata" --search /usr/local/cuda/lib64 --out "$stage" --also 'libcublasLt.so*'
arch_args=""; for a in $(echo "$archs" | tr ';,' ' '); do arch_args="$arch_args --arch sm_$a"; done
mkdir -p "$out"
+69
View File
@@ -0,0 +1,69 @@
@echo off
rem The Windows CUDA picture reader for an engine package: strata-vision.exe built for the SAME architectures as the package's strata.exe.
rem
rem packaging\build_vision_windows.bat cuda12 <stage folder> CUDA 12.9: 60;61;70;75;80;86;89 (+ 89 PTX) the Pascal / Volta package
rem packaging\build_vision_windows.bat cuda13 <stage folder> CUDA 13.0: 75;86;89;120 (+ 120 PTX)
rem
rem Why: the cuda12 and cuda13 packages used to share one strata-vision.exe, built before the Pascal code existed (unchanged since 0.1.44). On a
rem Tesla P100 / GTX 10 card the engine ran and the reader stopped with "IM2COL failed / device kernel image is invalid" (#3669), and the
rem app only said "the vision encoder did not start". Now each variant has its own reader and packaging\package_engine.py refuses a package
rem whose BUILD.json vision_archs do not cover its archs.
rem
rem Needs: Visual Studio 2022 Build Tools (run from a "x64 Native Tools" prompt, or let this call vcvars64), CMake + Ninja, the CUDA toolkit of
rem the variant (CUDA_PATH_V12_9 / CUDA_PATH_V13_0, or CUDA_PATH already pointing at it), and the pinned llama.cpp (python -c "import setup;
rem setup.get_llama_cpp()", or STRATA_GGML_DIR).
rem
rem Settings (all optional): STRATA_GGML_DIR the llama.cpp checkout (default third_party\llama.cpp) VISION_BUILD_DIR the build folder
rem VISION_ARCHS override the architecture list (semicolons) BUILD_JOBS the number of compile jobs (default 4: the CUDA compiler uses RAM)
rem
rem It copies strata-vision.exe into <stage folder> and writes vision_archs into <stage folder>\BUILD.json. Nothing is uploaded.
setlocal EnableDelayedExpansion
set "VARIANT=%~1"
set "STAGE=%~2"
if "%VARIANT%"=="" goto usage
if "%STAGE%"=="" goto usage
for %%I in ("%~dp0..") do set "SRC=%%~fI"
if /i "%VARIANT%"=="cuda12" (
set "ARCHS=60;61;70;75;80;86;89"
if defined CUDA_PATH_V12_9 set "CUDA_PATH=%CUDA_PATH_V12_9%"
) else if /i "%VARIANT%"=="cuda13" (
set "ARCHS=75;86;89;120"
if defined CUDA_PATH_V13_0 set "CUDA_PATH=%CUDA_PATH_V13_0%"
) else goto usage
if defined VISION_ARCHS set "ARCHS=%VISION_ARCHS%"
if not defined CUDA_PATH (echo CUDA_PATH is not set: install the %VARIANT% toolkit & exit /b 1)
if not exist "%CUDA_PATH%\bin\nvcc.exe" (echo no nvcc.exe in %CUDA_PATH%\bin & exit /b 1)
if not defined BUILD_JOBS set "BUILD_JOBS=4"
if not defined STRATA_GGML_DIR set "STRATA_GGML_DIR=%SRC%\third_party\llama.cpp"
set "LLAMA=%STRATA_GGML_DIR:\=/%"
if not exist "%LLAMA%/tools/mtmd" (echo llama.cpp with tools/mtmd not found in %LLAMA% - set STRATA_GGML_DIR & exit /b 1)
if not defined VISION_BUILD_DIR set "VISION_BUILD_DIR=%SRC%\build-vision-%VARIANT%-win"
if not exist "%STAGE%" mkdir "%STAGE%"
where cl >nul 2>nul || (
for %%V in ("%ProgramFiles%\Microsoft Visual Studio\2022\BuildTools" "%ProgramFiles%\Microsoft Visual Studio\2022\Community" "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools") do (
if exist "%%~V\VC\Auxiliary\Build\vcvars64.bat" if not defined VCVARS set "VCVARS=%%~V\VC\Auxiliary\Build\vcvars64.bat"
)
if defined VCVARS call "!VCVARS!" >nul
)
where cl >nul 2>nul || (echo cl.exe not found: run this from an x64 Native Tools prompt & exit /b 1)
rem every arch real, and PTX for the newest one, the way the engine is built
set "REAL="
for %%A in (%ARCHS:;= %) do (
set "LAST=%%A"
if defined REAL (set "REAL=!REAL!;%%A-real") else (set "REAL=%%A-real")
)
cmake -G Ninja -S "%SRC%\tools\vision" -B "%VISION_BUILD_DIR%" -DCMAKE_BUILD_TYPE=Release -DLLAMA_DIR="%LLAMA%" ^
-DSTRATA_VISION_CUDA=ON -DSTRATA_PORTABLE=ON -DGGML_CUDA_NCCL=OFF ^
"-DCMAKE_CUDA_ARCHITECTURES=!REAL!;!LAST!-virtual" -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl ^
"-DCMAKE_CUDA_COMPILER=%CUDA_PATH%/bin/nvcc.exe" "-DCUDAToolkit_ROOT=%CUDA_PATH%" || exit /b 1
cmake --build "%VISION_BUILD_DIR%" --target strata-vision -j %BUILD_JOBS% || exit /b 1
copy /y "%VISION_BUILD_DIR%\bin\strata-vision.exe" "%STAGE%\strata-vision.exe" >nul || exit /b 1
py -3 "%~dp0package_engine.py" --stamp-vision "%STAGE%" --vision-archs "%ARCHS%" || exit /b 1
echo built: %STAGE%\strata-vision.exe (%VARIANT%, archs %ARCHS%)
exit /b 0
:usage
echo usage: packaging\build_vision_windows.bat cuda12^|cuda13 ^<stage folder with strata.exe^>
exit /b 2
+92 -2
View File
@@ -27,6 +27,7 @@ import fnmatch
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
@@ -56,6 +57,78 @@ def wanted(path: Path) -> bool:
or name.startswith("license") or name in ("strata-vision", "strata-vision.exe", "strata-vision-hip", "strata-vision-hip.exe")) # the picture readers (CPU, AMD GPU) beside the engine
READER_NAMES = ("strata-vision", "strata-vision.exe")
def _arch_numbers(value) -> set[int]:
"""The CUDA architectures in a BUILD.json `archs` / `vision_archs` value ("60;61;70", [60, "sm_61", "70-real"], ...), as numbers."""
if isinstance(value, str):
value = re.split(r"[;,\s]+", value)
found: set[int] = set()
for item in value or []:
m = re.fullmatch(r"(?:sm_|compute_)?(\d+)(?:a|f)?(?:-real|-virtual)?", str(item).strip().lower())
if m:
found.add(int(m.group(1)))
return found
def check_picture_reader(src: Path, backend: str, target: str, archs: tuple[str, ...], *, variant: str = "",
allow_stale: bool = False, allow_none: bool = False) -> list[str]:
"""The picture reader of a package must run on every card and Mac the package does (tools tests: I1, I2, #3669, #3697).
* CUDA package with a reader: BUILD.json (beside the engine) must list `vision_archs`, and every `sm_NN` the package carries
code for must be there. A reader reused from an older build (the Windows zips' strata-vision.exe, unchanged since 0.1.44, had no
sm_60) stops with "device kernel image is invalid" on a Pascal card while the engine runs. Refused, unless allow_stale.
* macOS package without a reader: refused (a Mac with this package cannot read a picture), unless allow_none.
* CUDA package without a reader: allowed (text-only), reported in the returned notes so the build log says it.
Returns notes to print."""
reader = next((n for n in READER_NAMES if (src / n).is_file()), None)
notes: list[str] = []
if target.startswith("macos"):
if reader is None and not allow_none:
raise SystemExit("refusing to package the macOS engine without its picture reader: a Mac with this package could not read "
"a picture (#3697). Build it with packaging/build_vision_mac.sh and copy strata-vision beside strata "
"(or give --no-picture-reader to ship a text-only package on purpose).")
if reader is None:
notes.append("no picture reader in this macOS package: pictures are off for it")
return notes
if backend != "cuda":
return notes
if reader is None:
notes.append("no picture reader in this CUDA package: the engine answers text only (the app says pictures need the reader)")
return notes
needed = {n for n in _arch_numbers([a for a in archs if str(a).lower().startswith("sm_")])}
have: set[int] = set()
build_json = src / "BUILD.json"
try:
have = _arch_numbers(json.loads(build_json.read_text(encoding="utf-8-sig")).get("vision_archs"))
except (OSError, ValueError, AttributeError):
pass
missing = sorted(needed - have)
if missing and not allow_stale:
what = ("BUILD.json has no vision_archs: the reader's build is not recorded" if not have
else "the reader has no code for " + ", ".join(f"sm_{n}" for n in missing))
raise SystemExit(f"refusing to package the {variant or backend} engine with this picture reader: {what}. A reader reused from an "
"older build stops with 'device kernel image is invalid' on a card it was not built for (#3669). Build it for the "
"package's architectures (packaging/build_vision_windows.bat, packaging/build_cuda_linux.sh) and record them as "
"vision_archs in BUILD.json (or give --allow-stale-vision).")
if missing:
notes.append("picture reader NOT built for " + ", ".join(f"sm_{n}" for n in missing) + " (allowed by --allow-stale-vision)")
return notes
def stamp_vision_archs(stage: Path, archs: str) -> None:
"""Record the architectures the picture reader in `stage` was built for as BUILD.json `vision_archs` (the build recipes for Windows
call this after building strata-vision.exe; check_picture_reader compares it with the package's archs)."""
path = stage / "BUILD.json"
try:
data = json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {}
except (OSError, ValueError):
data = {}
data["vision_archs"] = ";".join(str(n) for n in sorted(_arch_numbers(archs)))
path.write_text(json.dumps(data, indent=1) + chr(10), encoding="utf-8")
def git_build_id(repo: Path = ROOT) -> str:
"""The source commit of the engine build, `-dirty` when the tree had changes. `unknown` outside a checkout."""
try:
@@ -90,7 +163,8 @@ def select(src: Path, entrypoint: str, *, tree: bool, include: list[str]) -> lis
def build(src: Path, backend: str, version: str, target: str, out: Path, *, entry: str | None = None,
archs: tuple[str, ...] = (), min_driver: dict | None = None, build_id: str | None = None,
min_runtime: str | None = None, notes: str = "", tree: bool = False, include: list[str] | None = None,
variant: str = "", search: list[Path] | None = None, allow_libs: list[str] | None = None) -> Path:
variant: str = "", search: list[Path] | None = None, allow_libs: list[str] | None = None,
check_reader: bool = False, allow_stale_vision: bool = False, allow_no_reader: bool = False) -> Path:
if backend not in BACKEND_MATRIX:
raise SystemExit(f"unknown backend {backend!r}")
if backend != "cpu" and not archs:
@@ -108,6 +182,10 @@ def build(src: Path, backend: str, version: str, target: str, out: Path, *, entr
entrypoint = entry or ("strata.exe" if target.startswith("windows") else "strata")
if not (src / entrypoint).is_file():
raise SystemExit(f"{src / entrypoint} does not exist")
if check_reader: # the release recipes and the CLI ask for it; the library call keeps its old behaviour
for note in check_picture_reader(src, backend, target, archs, variant=variant, allow_stale=allow_stale_vision,
allow_none=allow_no_reader):
print("note: " + note)
folder = out / (f"{version}-{target}-{backend}" + (f"-{variant}" if variant else ""))
if folder.exists():
raise SystemExit(f"{folder} already exists; remove it first (packages are immutable)")
@@ -218,6 +296,11 @@ def main(argv: list[str] | None = None) -> int:
ap.add_argument("--allow-lib", action="append", default=[], metavar="NAME",
help="an imported library that really is provided by the target system (rare; named so the check passes)")
ap.add_argument("--include", action="append", default=[], metavar="GLOB", help="more files, relative to --src")
ap.add_argument("--stamp-vision", type=Path, metavar="STAGE", help="write --vision-archs into STAGE/BUILD.json and exit")
ap.add_argument("--vision-archs", default="", help="with --stamp-vision: the architectures the reader was built for, e.g. '60;61;70;75;80;86;89'")
ap.add_argument("--allow-stale-vision", action="store_true",
help="package a CUDA picture reader whose BUILD.json vision_archs do not cover the package's archs (it will fail on those cards)")
ap.add_argument("--no-picture-reader", action="store_true", help="ship a macOS package without strata-vision on purpose (pictures stay off)")
a = ap.parse_args(argv)
if a.verify:
try:
@@ -233,6 +316,12 @@ def main(argv: list[str] | None = None) -> int:
return 1
print(f"{a.verify}: ok ({len(component.files)} files, manifest sha256 {package_sha256(a.verify)})")
return 0
if a.stamp_vision:
if not _arch_numbers(a.vision_archs):
ap.error("--stamp-vision needs --vision-archs, for example '60;61;70;75;80;86;89'")
stamp_vision_archs(a.stamp_vision, a.vision_archs)
print(f"{a.stamp_vision / 'BUILD.json'}: vision_archs {a.vision_archs}")
return 0
for needed in ("src", "backend", "version", "out"):
if getattr(a, needed) is None:
ap.error(f"--{needed} is required")
@@ -246,7 +335,8 @@ def main(argv: list[str] | None = None) -> int:
folder = build(a.src, a.backend, a.version, a.platform or host_platform(), a.out, entry=a.entrypoint,
archs=tuple(a.arch), min_driver=drivers, build_id=a.build_id, min_runtime=a.min_runtime,
notes=a.notes, tree=a.tree, include=a.include, variant=a.variant,
search=a.search, allow_libs=a.allow_lib)
search=a.search, allow_libs=a.allow_lib, check_reader=True, allow_stale_vision=a.allow_stale_vision,
allow_no_reader=a.no_picture_reader)
entries = [p for p in folder.rglob("*") if p.is_file()]
total = sum(p.stat().st_size for p in entries)
print(f"{folder} ({total / (1 << 20):.1f} MiB, {len(entries)} files, manifest sha256 {package_sha256(folder)})")
@@ -0,0 +1,94 @@
"""I1 / I2 (tools and images review, 2026-10-22): the picture reader in an engine package must run on every card and Mac the package does.
#3669: the Windows cuda12 package reused a strata-vision.exe built before Pascal support; the engine ran on a P100 and the reader stopped
with "device kernel image is invalid". #3697: a macOS package with no reader at all. These tests need no GPU and no toolkit."""
import json
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "packaging"))
import package_engine # noqa: E402
CUDA12 = ("sm_60", "sm_61", "sm_70", "sm_75", "sm_80", "sm_86", "sm_89", "compute_89")
CUDA13 = ("sm_75", "sm_86", "sm_89", "sm_120", "compute_120")
class ReaderCheck(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.src = Path(self.tmp.name)
(self.src / "strata").write_text("x")
def reader(self, vision_archs=None):
(self.src / "strata-vision").write_text("x")
data = {"version": "0.2.3"}
if vision_archs is not None:
data["vision_archs"] = vision_archs
(self.src / "BUILD.json").write_text(json.dumps(data))
def check(self, backend, target, archs, **kw):
return package_engine.check_picture_reader(self.src, backend, target, archs, **kw)
def test_a_reader_with_no_recorded_archs_is_refused_for_cuda12(self):
self.reader()
with self.assertRaises(SystemExit) as cm:
self.check("cuda", "windows-x86_64", CUDA12, variant="cuda12")
self.assertIn("vision_archs", str(cm.exception))
self.assertIn("3669", str(cm.exception))
def test_a_cuda13_reader_does_not_pass_for_the_pascal_package(self):
self.reader("75;86;89;120")
with self.assertRaises(SystemExit) as cm:
self.check("cuda", "windows-x86_64", CUDA12, variant="cuda12")
for n in ("sm_60", "sm_61", "sm_70"):
self.assertIn(n, str(cm.exception))
def test_a_reader_built_for_the_packages_archs_passes(self):
self.reader("60;61;70;75;80;86;89")
self.assertEqual(self.check("cuda", "linux-x86_64", CUDA12, variant="cuda12"), [])
self.reader(["75", "sm_86", "89-real", 120])
self.assertEqual(self.check("cuda", "windows-x86_64", CUDA13, variant="cuda13"), [])
def test_allow_stale_vision_packages_it_and_says_what_is_missing(self):
self.reader("75;86;89;120")
notes = self.check("cuda", "windows-x86_64", CUDA12, variant="cuda12", allow_stale=True)
self.assertIn("sm_60", notes[0])
def test_a_cuda_package_without_a_reader_is_text_only_not_an_error(self):
notes = self.check("cuda", "linux-x86_64", CUDA13, variant="cuda13")
self.assertIn("text only", notes[0])
def test_a_macos_package_without_a_reader_is_refused(self):
with self.assertRaises(SystemExit) as cm:
self.check("metal", "macos-arm64", ())
self.assertIn("build_vision_mac.sh", str(cm.exception))
self.assertIn("pictures are off", self.check("metal", "macos-arm64", (), allow_none=True)[0])
def test_a_macos_package_with_a_reader_passes(self):
self.reader()
self.assertEqual(self.check("metal", "macos-arm64", ()), [])
def test_build_runs_the_check_only_when_asked(self):
self.reader("75;86;89;120")
out = Path(self.tmp.name) / "out"
out.mkdir()
with self.assertRaises(SystemExit) as cm:
package_engine.build(self.src, "cuda", "0.2.3", "linux-x86_64", out, archs=CUDA12, variant="cuda12", build_id="t",
check_reader=True)
self.assertIn("picture reader", str(cm.exception))
def test_stamp_vision_writes_the_record(self):
(self.src / "BUILD.json").write_text(json.dumps({"version": "0.2.3", "archs": "60;61"}))
self.assertEqual(package_engine.main(["--stamp-vision", str(self.src), "--vision-archs", "60;61;70"]), 0)
data = json.loads((self.src / "BUILD.json").read_text())
self.assertEqual(data["vision_archs"], "60;61;70")
self.assertEqual(data["archs"], "60;61")
if __name__ == "__main__":
unittest.main()