From 8c8020ae18918c5fe3e4246311165f69674a8178 Mon Sep 17 00:00:00 2001 From: "INTER.Tech" Date: Thu, 23 Apr 2026 14:39:12 -0400 Subject: [PATCH 01/21] fix(tensorrt): TRT 10.16.1.11 + modelopt install + run_pip quote-fix - tensorrt.py: bump tensorrt_cu12 to 10.16.1.11, polygraphy 0.49.26, onnx-graphsurgeon 0.6.1; add FP8-quant block (modelopt + cupy-cuda12x + numpy re-lock); re-pin onnxruntime-gpu==1.24.4 with --no-deps after modelopt downgrade; drop shell-style quotes inside package specs (run_pip uses subprocess + .split(), quotes become literal arg chars). - installer.py: remove torchaudio from cu128 config (not needed); minor ruff format cleanup. - verifier.py: float32_to_bfloat16 diagnostic points to onnx-gs 0.6.1 instead of suggesting an onnx downgrade. - __init__.py, __main__.py, cli.py: ruff format cleanup (blank lines, unused import, raw docstring). --- sd_installer/__init__.py | 1 + sd_installer/__main__.py | 1 + sd_installer/cli.py | 39 +++++++++++-------- sd_installer/installer.py | 32 ++++++++-------- sd_installer/tensorrt.py | 44 +++++++++++++-------- sd_installer/verifier.py | 81 ++++++++++++++------------------------- 6 files changed, 96 insertions(+), 102 deletions(-) diff --git a/sd_installer/__init__.py b/sd_installer/__init__.py index 6c059c8..0cbabfc 100644 --- a/sd_installer/__init__.py +++ b/sd_installer/__init__.py @@ -16,4 +16,5 @@ from .installer import Installer from .verifier import Verifier + __all__ = ["Installer", "Verifier", "__version__"] diff --git a/sd_installer/__main__.py b/sd_installer/__main__.py index 2a0fbda..e878b7a 100644 --- a/sd_installer/__main__.py +++ b/sd_installer/__main__.py @@ -2,5 +2,6 @@ from .cli import main + if __name__ == "__main__": exit(main()) diff --git a/sd_installer/cli.py b/sd_installer/cli.py index 9834f44..3f7110c 100644 --- a/sd_installer/cli.py +++ b/sd_installer/cli.py @@ -15,13 +15,12 @@ """ import argparse -import os import sys from pathlib import Path def find_base_folder() -> Path: - """ + r""" Find the StreamDiffusion base folder (where setup.py lives). Runtime structure: @@ -47,9 +46,9 @@ def find_base_folder() -> Path: # __file__ = .../StreamDiffusion-installer/sd_installer/cli.py # We want: .../StreamDiffusion/ this_file = Path(__file__).resolve() - sd_installer_pkg = this_file.parent # sd_installer/ - installer_repo = sd_installer_pkg.parent # StreamDiffusion-installer/ - base = installer_repo.parent # StreamDiffusion/ + sd_installer_pkg = this_file.parent # sd_installer/ + installer_repo = sd_installer_pkg.parent # StreamDiffusion-installer/ + base = installer_repo.parent # StreamDiffusion/ if (base / "setup.py").exists(): return base @@ -85,7 +84,7 @@ def cmd_check(args): if venv_path.exists(): print(f"Venv: Found at {venv_path}") else: - print(f"Venv: Not found (will be created during install)") + print("Venv: Not found (will be created during install)") # Check StreamDiffusion setup.py (base folder IS StreamDiffusion) setup_py = base / "setup.py" @@ -197,7 +196,7 @@ def cmd_diagnose(args): print(f" [{status}] {check['name']}") if check["error"]: # Print just the last line of the error - error_line = check["error"].split('\n')[-1][:60] + error_line = check["error"].split("\n")[-1][:60] print(f" {error_line}") return 0 @@ -205,7 +204,7 @@ def cmd_diagnose(args): def cmd_repair(args): """Auto-fix known issues.""" - from .verifier import Verifier, KNOWN_ERRORS + from .verifier import Verifier try: base = Path(args.base_folder) if args.base_folder else find_base_folder() @@ -236,6 +235,7 @@ def cmd_repair(args): for check in info["checks"]: if not check["passed"] and check["error"]: from .verifier import match_known_error + fix = match_known_error(check["error"]) if fix: fixes_needed.append((check["name"], fix)) @@ -245,10 +245,15 @@ def cmd_repair(args): # numpy 2.x numpy_ver = info["versions"].get("numpy", "") if numpy_ver.startswith("2."): - fixes_needed.append(("numpy version", { - "cause": f"numpy {numpy_ver} detected (2.x breaks things)", - "fix": "pip install numpy==1.26.4 --force-reinstall" - })) + fixes_needed.append( + ( + "numpy version", + { + "cause": f"numpy {numpy_ver} detected (2.x breaks things)", + "fix": "pip install numpy==1.26.4 --force-reinstall", + }, + ) + ) if not fixes_needed: print("No known issues detected that can be auto-fixed.") @@ -264,18 +269,19 @@ def cmd_repair(args): if not args.yes: response = input("Apply fixes? [y/N]: ") - if response.lower() != 'y': + if response.lower() != "y": print("Aborted.") return 0 # Apply fixes import subprocess + for name, fix in fixes_needed: print(f"Applying fix for {name}...") cmd = [str(python_exe), "-m", "pip"] + fix["fix"].replace("pip ", "").split() result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: - print(f" OK") + print(" OK") else: print(f" FAILED: {result.stderr}") @@ -314,7 +320,7 @@ def cmd_generate_bat(args): def cmd_install_tensorrt(args): """Install TensorRT packages.""" - from .tensorrt import install, get_cuda_version_from_torch + from .tensorrt import get_cuda_version_from_torch, install print("StreamDiffusionTD TensorRT Installation") print("=" * 40) @@ -381,7 +387,8 @@ def main(): # repair command repair_parser = subparsers.add_parser("repair", help="Auto-fix known issues") repair_parser.add_argument( - "-y", "--yes", + "-y", + "--yes", action="store_true", help="Apply fixes without prompting", ) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 11214f0..d982d87 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -11,12 +11,11 @@ 5. Verify imports - Catch failures immediately """ -import os -import sys import subprocess -import shutil +import sys from pathlib import Path -from typing import Optional, Callable +from typing import Callable, Optional + # Version pins - packages NOT in setup.py that must be manually pinned MANUAL_PINS = { @@ -65,7 +64,7 @@ "cu128": { "torch": "2.7.0", "torchvision": "0.22.0", - "torchaudio": "2.7.0", + "torchaudio": None, "index_url": "https://download.pytorch.org/whl/cu128", "cuda_python": "12.9.0", "xformers": None, # Not needed - PyTorch 2.7+ has native SDPA @@ -103,10 +102,7 @@ def __init__( # Validate CUDA version if cuda_version not in PYTORCH_CONFIGS: - raise ValueError( - f"Unsupported CUDA version: {cuda_version}. " - f"Supported: {list(PYTORCH_CONFIGS.keys())}" - ) + raise ValueError(f"Unsupported CUDA version: {cuda_version}. Supported: {list(PYTORCH_CONFIGS.keys())}") self.pytorch_config = PYTORCH_CONFIGS[cuda_version] @@ -238,7 +234,7 @@ def phase3b_insightface(self): version_str = result.stdout.strip() try: - major, minor = map(int, version_str.split('.')) + major, minor = map(int, version_str.split(".")) py_version = (major, minor) except ValueError: print(f" WARNING: Could not parse Python version '{version_str}', skipping insightface pre-install") @@ -269,10 +265,13 @@ def phase5_missing_pins(self): # Force reinstall varshith15 diffusers (other deps may have overwritten it) self._report_progress("Ensuring varshith15 diffusers fork with kvo_cache support...", 5, 8) - self._run_pip([ - "--force-reinstall", "--no-deps", - "diffusers @ git+https://github.com/varshith15/diffusers.git@3e3b72f557e91546894340edabc845e894f00922" - ]) + self._run_pip( + [ + "--force-reinstall", + "--no-deps", + "diffusers @ git+https://github.com/varshith15/diffusers.git@3e3b72f557e91546894340edabc845e894f00922", + ] + ) def phase6_conflict_prone(self): """Phase 6: Fix conflict-prone packages with --no-deps.""" @@ -280,8 +279,7 @@ def phase6_conflict_prone(self): # Remove conflicting opencv variants subprocess.run( - [str(self.python_exe), "-m", "pip", "uninstall", "-y", - "opencv-python-headless", "opencv-contrib-python"], + [str(self.python_exe), "-m", "pip", "uninstall", "-y", "opencv-python-headless", "opencv-contrib-python"], capture_output=True, ) @@ -386,7 +384,7 @@ def generate_batch_file(self, output_path: Optional[str] = None, python_exe: Opt pause ''' - with open(output_path, 'w', encoding='utf-8') as f: + with open(output_path, "w", encoding="utf-8") as f: f.write(content) print(f"Generated batch file: {output_path}") diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index d4b46e1..a048f67 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -3,9 +3,10 @@ Standalone module that doesn't rely on streamdiffusion package imports. """ + +import platform import subprocess import sys -import platform from typing import Optional @@ -17,7 +18,7 @@ def run_pip(command: str): def is_installed(package_name: str) -> bool: """Check if a package is installed""" try: - __import__(package_name.replace('-', '_')) + __import__(package_name.replace("-", "_")) return True except ImportError: return False @@ -27,6 +28,7 @@ def version(package_name: str) -> Optional[str]: """Get version of installed package""" try: import importlib.metadata + return importlib.metadata.version(package_name) except Exception: return None @@ -74,6 +76,7 @@ def install(cu: Optional[str] = None): if current_version_str: try: from packaging.version import Version + current_version = Version(current_version_str) if current_version < Version("10.8.0"): print("Uninstalling old TensorRT version...") @@ -84,9 +87,11 @@ def install(cu: Optional[str] = None): print("Uninstalling old TensorRT version...") run_pip("uninstall -y tensorrt") - # For CUDA 12.8+ (RTX 5090/Blackwell support), use TensorRT 10.12+ + # For CUDA 12.8+ (RTX 5090/Blackwell support), use TensorRT 10.16+ + # 10.16.1.11 is the first Blackwell-Windows-production release and fixes + # the 78% FP8 perf regression that shipped in 10.12–10.13 on SM_120. if cuda_version_float >= 12.8: - print("Installing TensorRT 10.12+ for CUDA 12.8+ (Blackwell GPU support)...") + print("Installing TensorRT 10.16+ for CUDA 12.8+ (Blackwell GPU support)...") # Install cuDNN 9 for CUDA 12 cudnn_name = "nvidia-cudnn-cu12==9.7.1.26" @@ -96,7 +101,7 @@ def install(cu: Optional[str] = None): # tensorrt_cu12 is the CUDA 12 wrapper that owns tensorrt/__init__.py # and depends on tensorrt_cu12_libs + tensorrt_cu12_bindings. # All three are normal wheels with Requires-Dist (no pip-inside-pip). - trt_version = "10.12.0.36" + trt_version = "10.16.1.11" print(f"Installing TensorRT {trt_version} for CUDA {cu}...") run_pip(f"install --extra-index-url https://pypi.nvidia.com tensorrt_cu12=={trt_version} --no-cache-dir") @@ -111,7 +116,7 @@ def install(cu: Optional[str] = None): # tensorrt_cu12 is the CUDA 12 wrapper that owns tensorrt/__init__.py # and depends on tensorrt_cu12_libs + tensorrt_cu12_bindings. # All three are normal wheels with Requires-Dist (no pip-inside-pip). - trt_version = "10.12.0.36" + trt_version = "10.16.1.11" print(f"Installing TensorRT {trt_version} for CUDA {cu}...") run_pip(f"install --extra-index-url https://pypi.nvidia.com tensorrt_cu12=={trt_version} --no-cache-dir") @@ -126,9 +131,7 @@ def install(cu: Optional[str] = None): # Install TensorRT for CUDA 11 tensorrt_version = "tensorrt==9.0.1.post11.dev4" print(f"Installing TensorRT for CUDA {cu}: {tensorrt_version}") - run_pip( - f"install --extra-index-url https://pypi.nvidia.com {tensorrt_version} --no-cache-dir" - ) + run_pip(f"install --extra-index-url https://pypi.nvidia.com {tensorrt_version} --no-cache-dir") else: print(f"Unsupported CUDA version: {cu}") print("Supported versions: CUDA 11.x, 12.x, 12.8+") @@ -137,18 +140,25 @@ def install(cu: Optional[str] = None): # Install additional TensorRT tools if not is_installed("polygraphy"): print("Installing polygraphy...") - run_pip( - "install polygraphy==0.49.24 --extra-index-url https://pypi.ngc.nvidia.com --no-cache-dir" - ) + run_pip("install polygraphy==0.49.26 --extra-index-url https://pypi.ngc.nvidia.com --no-cache-dir") if not is_installed("onnx_graphsurgeon"): print("Installing onnx-graphsurgeon...") - run_pip( - "install onnx-graphsurgeon==0.5.8 --extra-index-url https://pypi.ngc.nvidia.com --no-cache-dir" - ) - if platform.system() == 'Windows' and not is_installed("pywin32"): + run_pip("install onnx-graphsurgeon==0.6.1 --extra-index-url https://pypi.ngc.nvidia.com --no-cache-dir") + + # FP8 quantization dependencies (CUDA 12 only). + # Previously missing — caused ImportError in fp8_quantize.py when users enabled FP8. + # Aligns with FLUX pyproject.toml (nvidia-modelopt >= 0.19.0). + if cuda_major == "12": + print("Installing FP8 quantization dependencies (modelopt, cupy)...") + run_pip("install nvidia-modelopt[onnx]>=0.19.0 cupy-cuda12x==13.6.0 numpy==1.26.4 --no-cache-dir") + # modelopt's resolver downgrades onnxruntime-gpu to 1.22.0; re-assert 1.24.4. + # --no-deps avoids triggering a conflicting re-solve. + run_pip("install onnxruntime-gpu==1.24.4 --no-deps --no-cache-dir") + + if platform.system() == "Windows" and not is_installed("pywin32"): print("Installing pywin32...") run_pip("install pywin32==306 --no-cache-dir") - if platform.system() == 'Windows' and not is_installed("triton"): + if platform.system() == "Windows" and not is_installed("triton"): print("Installing triton-windows...") run_pip("install triton-windows==3.4.0.post21 --no-cache-dir") diff --git a/sd_installer/verifier.py b/sd_installer/verifier.py index 2cc484c..fa23d60 100644 --- a/sd_installer/verifier.py +++ b/sd_installer/verifier.py @@ -12,6 +12,7 @@ @dataclass class VerificationResult: """Result of a single verification check.""" + name: str passed: bool message: str @@ -23,67 +24,39 @@ class VerificationResult: ( "torch CUDA", "import torch; assert torch.cuda.is_available(), 'CUDA not available'; print(f'{torch.__version__}+cu{torch.version.cuda} | {torch.cuda.get_device_name(0)}')", - "PyTorch with CUDA" - ), - ( - "StreamDiffusion", - "from streamdiffusion.config import load_config; print('OK')", - "StreamDiffusion core" - ), - ( - "timm RotaryEmbedding", - "from timm.layers import RotaryEmbedding; print('OK')", - "timm (>=1.0.24 required)" - ), - ( - "mediapipe", - "import mediapipe as mp; mp.solutions.drawing_utils; print('OK')", - "mediapipe solutions" - ), - ( - "transformers MT5", - "from transformers import MT5Tokenizer; print('OK')", - "transformers (MT5Tokenizer)" - ), - ( - "huggingface_hub", - "from huggingface_hub import hf_hub_download; print('OK')", - "huggingface_hub" + "PyTorch with CUDA", ), + ("StreamDiffusion", "from streamdiffusion.config import load_config; print('OK')", "StreamDiffusion core"), + ("timm RotaryEmbedding", "from timm.layers import RotaryEmbedding; print('OK')", "timm (>=1.0.24 required)"), + ("mediapipe", "import mediapipe as mp; mp.solutions.drawing_utils; print('OK')", "mediapipe solutions"), + ("transformers MT5", "from transformers import MT5Tokenizer; print('OK')", "transformers (MT5Tokenizer)"), + ("huggingface_hub", "from huggingface_hub import hf_hub_download; print('OK')", "huggingface_hub"), ( "numpy version", "import numpy; v = numpy.__version__; assert v.startswith('1.'), f'numpy 2.x detected: {v}'; print(v)", - "numpy (<2.0.0 required)" + "numpy (<2.0.0 required)", ), ( "diffusers fork", "import inspect; from diffusers.models.attention_processor import Attention; assert 'kvo_cache' in inspect.signature(Attention.forward).parameters, 'Missing kvo_cache'; print('OK')", - "diffusers (varshith15 fork with kvo_cache)" - ), - ( - "accelerate", - "from accelerate import Accelerator; print('OK')", - "accelerate" - ), - ( - "controlnet_aux", - "from controlnet_aux import OpenposeDetector; print('OK')", - "controlnet_aux" + "diffusers (varshith15 fork with kvo_cache)", ), + ("accelerate", "from accelerate import Accelerator; print('OK')", "accelerate"), + ("controlnet_aux", "from controlnet_aux import OpenposeDetector; print('OK')", "controlnet_aux"), ( "peft (USE_PEFT_BACKEND)", "from diffusers.utils import USE_PEFT_BACKEND; assert USE_PEFT_BACKEND, 'peft not detected'; print('OK')", - "peft (required for Cached Attention/StreamV2V)" + "peft (required for Cached Attention/StreamV2V)", ), ( "protobuf version", "import google.protobuf; v = google.protobuf.__version__; major = int(v.split('.')[0]); assert major < 5, f'protobuf {v} (>=5.x breaks TRT engine builds)'; print(v)", - "protobuf (<5.0 required for TRT)" + "protobuf (<5.0 required for TRT)", ), ( "onnx version", "import onnx; v = onnx.__version__; parts = [int(x) for x in v.split('.')[:2]]; assert parts[0] == 1 and parts[1] < 20, f'onnx {v} (>=1.20 removes float32_to_bfloat16)'; print(v)", - "onnx (<1.20 required for TRT)" + "onnx (<1.20 required for TRT)", ), ] @@ -178,7 +151,7 @@ def run_all(self, verbose: bool = True) -> bool: print(f"FAIL: {result.message}") if result.error: # Print first line of error - error_line = result.error.split('\n')[-1] + error_line = result.error.split("\n")[-1] print(f" {error_line}") if verbose: @@ -211,10 +184,12 @@ def diagnose(self) -> dict: try: result = subprocess.run( [self.python_exe, "-c", gpu_code], - capture_output=True, text=True, timeout=30, + capture_output=True, + text=True, + timeout=30, ) if result.returncode == 0: - lines = result.stdout.strip().split('\n') + lines = result.stdout.strip().split("\n") info["gpu"]["name"] = lines[0] info["gpu"]["vram_mb"] = int(lines[1]) info["gpu"]["compute_capability"] = lines[2] @@ -224,12 +199,14 @@ def diagnose(self) -> dict: # Run all checks and collect detailed info for name, code, description in VERIFICATION_CHECKS: result = self.check(name, code, description) - info["checks"].append({ - "name": name, - "passed": result.passed, - "message": result.message, - "error": result.error, - }) + info["checks"].append( + { + "name": name, + "passed": result.passed, + "message": result.message, + "error": result.error, + } + ) # Get version information for key packages version_checks = [ @@ -290,8 +267,8 @@ def diagnose(self) -> dict: "fix": "pip install accelerate==1.10.0", }, "'onnx.helper' has no attribute 'float32_to_bfloat16'": { - "cause": "onnx version too new", - "fix": "pip install onnx==1.18.0", + "cause": "onnx-graphsurgeon too old for onnx>=1.19 (float32_to_bfloat16 was removed)", + "fix": "pip install onnx-graphsurgeon==0.6.1 --extra-index-url https://pypi.ngc.nvidia.com", }, "Missing kvo_cache": { "cause": "Wrong diffusers installed (vanilla instead of varshith15 fork)", From 4b23a5688de3fa6aacf10c59bb55d02ce8503c0d Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 23 May 2026 21:03:10 -0400 Subject: [PATCH 02/21] chore(deps): add security floor pins for idna, Mako, urllib3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes 6 CVEs patched in deps audit 2026-05-23: - idna >=3.16 (CVE-2026-45409: punycode resource exhaustion) - Mako >=1.3.12 (CVE-2026-44307: Windows backslash path traversal) - urllib3 >=2.7.0 (CVE-2026-44432/44431: over-decompression, cross-origin redirect) Added to MANUAL_PINS and installed in phase7_numpy_lock so upgrade runs on both fresh and existing installs. Fresh pip resolves already satisfy these floors; this ensures the minimum on partial updates. pip and onnxruntime-gpu CVEs are handled separately: - pip: phase1_foundation already runs --upgrade pip (gets latest) - onnx 1.19.1: 6 CVEs deferred — 1.21.0 breaks FP8 quantization Co-Authored-By: Claude Opus 4.7 --- sd_installer/installer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index d982d87..a61b2fa 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -25,6 +25,10 @@ "python-osc": "", # Required for TouchDesigner OSC communication "peft": "0.17.1", # Required for Cached Attention (StreamV2V) - enables USE_PEFT_BACKEND "protobuf": "4.25.8", # Required by mediapipe, onnx/TensorRT - protobuf 6.x breaks serialization, setup.py requires >=4.25.8 + # Security floor pins (transitive deps — pip resolves these on fresh install, but floor ensures upgrade on update) + "idna": ">=3.16", # CVE-2026-45409: punycode resource exhaustion + "Mako": ">=1.3.12", # CVE-2026-44307: Windows backslash path traversal + "urllib3": ">=2.7.0", # CVE-2026-44432/44431: response over-decompression; cross-origin redirect } # Pre-built insightface wheels for Windows (PyPI has no Windows wheels, requires C++ build tools) @@ -287,13 +291,20 @@ def phase6_conflict_prone(self): self._run_pip(["--no-deps", f"opencv-python=={MANUAL_PINS['opencv-python']}"]) def phase7_numpy_lock(self): - """Phase 7: Final numpy and protobuf lock (other packages may have upgraded them).""" + """Phase 7: Final numpy/protobuf lock + security floor pins.""" self._report_progress(f"Final numpy lock (numpy=={MANUAL_PINS['numpy']})...", 7, 8) self._run_pip([f"numpy=={MANUAL_PINS['numpy']}", "--force-reinstall"]) self._report_progress(f"Final protobuf lock (protobuf=={MANUAL_PINS['protobuf']})...", 7, 8) self._run_pip([f"protobuf=={MANUAL_PINS['protobuf']}", "--force-reinstall"]) + self._report_progress("Applying security floor pins (idna, Mako, urllib3)...", 7, 8) + self._run_pip([ + f"idna{MANUAL_PINS['idna']}", + f"Mako{MANUAL_PINS['Mako']}", + f"urllib3{MANUAL_PINS['urllib3']}", + ]) + def phase8_verify(self) -> bool: """Phase 8: Verify installation with import tests.""" from .verifier import Verifier From d60be9a6dbc82a7df2d705c7dcc05687556db80c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Jul 2026 12:07:57 -0400 Subject: [PATCH 03/21] fix(installer): install cuda-link 1.12.0 wheel + pywin32 311 + bump v0.3.2 cuda-link (CUDA-IPC zero-copy transport) was never installed on a clean install: setup.py only exposes it via the optional cuda_ipc extra (a git ref to a compiled cp311 extension), which installer.py's phase4 never requests to avoid forcing an MSVC/nvcc source build. Add phase4b_cuda_link, mirroring phase3b_insightface's Python-version-gated prebuilt-wheel install (--no-deps, non-fatal fallback to the mirror-DAT transport). Also fixes pywin32 306->311 in tensorrt.py to match setup.py's authoritative pin (8c8020a fixed every other TensorRT pin but missed this one). Co-Authored-By: Claude Opus 4.8 --- sd_installer/installer.py | 41 +++++++++++++++++++++++++++++++++++++-- sd_installer/tensorrt.py | 2 +- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index a61b2fa..3481a98 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -39,6 +39,14 @@ (3, 12): "https://github.com/Gourieff/Assets/raw/main/Insightface/insightface-0.7.3-cp312-cp312-win_amd64.whl", } +# Pre-built cuda-link wheel (CUDA-IPC zero-copy transport). setup.py's cuda-link pin lives only in +# the optional cuda_ipc extra as a git ref (compiled cp311 extension) — installing that extra would +# force an MSVC/nvcc source build. Install the prebuilt wheel directly instead, --no-deps, so this +# extra is never triggered. Only a cp311 wheel is published. +CUDA_LINK_WHEELS = { + (3, 11): "https://github.com/forkni/cuda-link/releases/download/v1.12.0/cuda_link-1.12.0-cp311-cp311-win_amd64.whl", +} + # PyTorch configurations by CUDA version PYTORCH_CONFIGS = { "cu118": { @@ -260,6 +268,34 @@ def phase4_streamdiffusion(self): # The -e flag makes it editable, setup.py handles all pinned versions self._run_pip(["-e", ".[tensorrt,controlnet,ipadapter]"], check=True, cwd=self.streamdiffusion_path) + def phase4b_cuda_link(self): + """Phase 4b: Install cuda-link from pre-built wheel (CUDA-IPC zero-copy transport). + + Not covered by any setup.py extra actually installed above (cuda_ipc is intentionally + skipped to avoid a source build) — install the compiled wheel directly. Non-fatal: if no + wheel exists for this venv's Python, CUDA-IPC falls back to the mirror-DAT transport. + """ + result = self._run_python("import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") + if result.returncode != 0: + print(" WARNING: Could not detect venv Python version, skipping cuda-link pre-install") + return + + version_str = result.stdout.strip() + try: + major, minor = map(int, version_str.split(".")) + py_version = (major, minor) + except ValueError: + print(f" WARNING: Could not parse Python version '{version_str}', skipping cuda-link pre-install") + return + + wheel_url = CUDA_LINK_WHEELS.get(py_version) + if wheel_url: + self._report_progress(f"Installing cuda-link 1.12.0 from pre-built wheel (Python {version_str})...", 4, 8) + self._run_pip(["--no-deps", wheel_url], check=False) + else: + print(f" WARNING: No pre-built cuda-link wheel for Python {version_str}") + print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") + def phase5_missing_pins(self): """Phase 5: Install packages not pinned in setup.py and fix diffusers.""" self._report_progress("Installing packages not in setup.py (timm, python-osc, peft)...", 5, 8) @@ -324,7 +360,7 @@ def install(self, python_exe: Optional[str] = None) -> bool: True if installation and verification succeeded. """ print("=" * 50) - print(" StreamDiffusionTD v0.3.1 Installation") + print(" StreamDiffusionTD v0.3.2 Installation") print(" Daydream Fork with StreamV2V") print("=" * 50) print() @@ -341,6 +377,7 @@ def install(self, python_exe: Optional[str] = None) -> bool: self.phase3_xformers() self.phase3b_insightface() # Pre-install insightface from wheel (Windows) self.phase4_streamdiffusion() + self.phase4b_cuda_link() # Pre-install cuda-link from wheel (CUDA-IPC transport) self.phase5_missing_pins() self.phase6_conflict_prone() self.phase7_numpy_lock() @@ -383,7 +420,7 @@ def generate_batch_file(self, output_path: Optional[str] = None, python_exe: Opt content = f'''@echo off echo ======================================== -echo StreamDiffusionTD v0.3.1 Installation +echo StreamDiffusionTD v0.3.2 Installation echo Daydream Fork with StreamV2V echo ======================================== diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index a048f67..d46530d 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -157,7 +157,7 @@ def install(cu: Optional[str] = None): if platform.system() == "Windows" and not is_installed("pywin32"): print("Installing pywin32...") - run_pip("install pywin32==306 --no-cache-dir") + run_pip("install pywin32==311 --no-cache-dir") if platform.system() == "Windows" and not is_installed("triton"): print("Installing triton-windows...") run_pip("install triton-windows==3.4.0.post21 --no-cache-dir") From b1712e8329dea28365408595d1d43c9b5751c27f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Jul 2026 12:19:37 -0400 Subject: [PATCH 04/21] fix(installer): bump torch to 2.8.0+cu128 (matches repo's proven target) PYTORCH_CONFIGS["cu128"] pinned torch 2.7.0 / torchvision 0.22.0, but setup.py doesn't constrain torch at all -- this dict was the sole source of truth, and the repo's own README, PKG-INFO, and tests/quality/manifest.json all target torch 2.8.0+cu128 / torchvision 0.23.0 (the golden-image regression harness aborts on any divergence). Bump to match, pairing correctly with the already maintained tensorrt_cu12==10.16.1.11. --- sd_installer/installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 3481a98..6cb0452 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -74,8 +74,8 @@ "xformers": None, # Skip - causes conflicts }, "cu128": { - "torch": "2.7.0", - "torchvision": "0.22.0", + "torch": "2.8.0", + "torchvision": "0.23.0", "torchaudio": None, "index_url": "https://download.pytorch.org/whl/cu128", "cuda_python": "12.9.0", From a61e225bd8a8d277d8af7d46c93f5c71edb712ab Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Jul 2026 12:52:17 -0400 Subject: [PATCH 05/21] fix(tensorrt): pin modelopt 0.43.0 + re-assert onnx 1.19.1 in FP8 block Unbounded nvidia-modelopt[onnx]>=0.19.0 floats to 0.45.0, whose [onnx] extra force-upgrades onnx past the immovable setup.py pin (1.19.1), breaking FP8 quant (negative QDQ scale on external-data loading). Confirmed live: a fresh Step 3 install left onnx 1.21.0 / modelopt 0.45.0 in the venv. Pin modelopt to the proven 0.43.0 (matches tests/quality/manifest.json) and re-assert onnx==1.19.1 alongside the existing onnxruntime-gpu re-assert. --- sd_installer/tensorrt.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index d46530d..8e885b6 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -147,13 +147,15 @@ def install(cu: Optional[str] = None): # FP8 quantization dependencies (CUDA 12 only). # Previously missing — caused ImportError in fp8_quantize.py when users enabled FP8. - # Aligns with FLUX pyproject.toml (nvidia-modelopt >= 0.19.0). + # modelopt is pinned to 0.43.0 (the proven pin in tests/quality/manifest.json); an unbounded + # ">=0.19.0" spec floats to 0.45.0, whose [onnx] extra force-upgrades onnx to 1.21.0, which + # breaks FP8 quant (external-data loading -> negative QDQ scale). setup.py pins onnx==1.19.1. if cuda_major == "12": print("Installing FP8 quantization dependencies (modelopt, cupy)...") - run_pip("install nvidia-modelopt[onnx]>=0.19.0 cupy-cuda12x==13.6.0 numpy==1.26.4 --no-cache-dir") - # modelopt's resolver downgrades onnxruntime-gpu to 1.22.0; re-assert 1.24.4. - # --no-deps avoids triggering a conflicting re-solve. - run_pip("install onnxruntime-gpu==1.24.4 --no-deps --no-cache-dir") + run_pip("install nvidia-modelopt[onnx]==0.43.0 cupy-cuda12x==13.6.0 numpy==1.26.4 --no-cache-dir") + # Re-assert the setup.py-authoritative pins the modelopt resolver perturbs: it downgrades + # onnxruntime-gpu to 1.22.0 and upgrades onnx past 1.19.1. --no-deps avoids a re-solve. + run_pip("install onnx==1.19.1 onnxruntime-gpu==1.24.4 --no-deps --no-cache-dir") if platform.system() == "Windows" and not is_installed("pywin32"): print("Installing pywin32...") From bb9c8d4a9e796bce1cf4b1500e3b07d0c61601ec Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jul 2026 16:11:00 -0400 Subject: [PATCH 06/21] feat: auto-persist CUDALINK_LIB_PATH + bump cuda-link wheel to 1.12.1 Add Installer.phase4c_cuda_link_env(): after the cuda-link wheel is installed, persist CUDALINK_LIB_PATH to the user environment via setx (Windows-only, non-fatal) so TouchDesigner library mode resolves the venv site-packages with no manual env-var step. Wired into install() right after phase4b_cuda_link(). Bump the wheel URL and phase progress string from 1.12.0 to 1.12.1. Co-Authored-By: Claude Sonnet 5 --- sd_installer/installer.py | 40 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 6cb0452..d62b5e0 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -44,7 +44,7 @@ # force an MSVC/nvcc source build. Install the prebuilt wheel directly instead, --no-deps, so this # extra is never triggered. Only a cp311 wheel is published. CUDA_LINK_WHEELS = { - (3, 11): "https://github.com/forkni/cuda-link/releases/download/v1.12.0/cuda_link-1.12.0-cp311-cp311-win_amd64.whl", + (3, 11): "https://github.com/forkni/cuda-link/releases/download/v1.12.1/cuda_link-1.12.1-cp311-cp311-win_amd64.whl", } # PyTorch configurations by CUDA version @@ -290,12 +290,47 @@ def phase4b_cuda_link(self): wheel_url = CUDA_LINK_WHEELS.get(py_version) if wheel_url: - self._report_progress(f"Installing cuda-link 1.12.0 from pre-built wheel (Python {version_str})...", 4, 8) + self._report_progress(f"Installing cuda-link 1.12.1 from pre-built wheel (Python {version_str})...", 4, 8) self._run_pip(["--no-deps", wheel_url], check=False) else: print(f" WARNING: No pre-built cuda-link wheel for Python {version_str}") print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") + def phase4c_cuda_link_env(self): + """Phase 4c: Persist CUDALINK_LIB_PATH -> this venv's site-packages (Windows only). + + TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to + enable "library mode" (sys.path injection of the installed cuda_link package, aliasing the + 14 mirror DAT names). Persisting it here via `setx` means every TD process launched after + this install inherits it automatically -- no manual env-var step. + + setx writes to HKCU\\Environment (user scope) and only affects processes started + *after* it runs, so TD must be (re)started after installation to pick it up. This + intentionally overwrites any prior manual value (e.g. an older cuda_link_lib\\ target). + Non-fatal: if setx fails or this isn't Windows, TD simply falls back to the mirror-DAT + classic mode. + """ + if sys.platform != "win32": + return # setx is a Windows-only mechanism; non-Windows TD launches are unaffected + + result = self._run_python("import sysconfig; print(sysconfig.get_paths()['purelib'])") + if result.returncode != 0 or not result.stdout.strip(): + print(" WARNING: Could not resolve venv site-packages path, skipping CUDALINK_LIB_PATH setup") + return + + site_packages = result.stdout.strip() + self._report_progress(f"Persisting CUDALINK_LIB_PATH -> {site_packages}", 4, 8) + setx_result = subprocess.run( + ["setx", "CUDALINK_LIB_PATH", site_packages], + capture_output=True, + text=True, + ) + if setx_result.returncode != 0: + print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_result.stderr.strip()}") + else: + print(" CUDALINK_LIB_PATH persisted for this user account.") + print(" Restart TouchDesigner (and any open shells) to pick up the new environment variable.") + def phase5_missing_pins(self): """Phase 5: Install packages not pinned in setup.py and fix diffusers.""" self._report_progress("Installing packages not in setup.py (timm, python-osc, peft)...", 5, 8) @@ -378,6 +413,7 @@ def install(self, python_exe: Optional[str] = None) -> bool: self.phase3b_insightface() # Pre-install insightface from wheel (Windows) self.phase4_streamdiffusion() self.phase4b_cuda_link() # Pre-install cuda-link from wheel (CUDA-IPC transport) + self.phase4c_cuda_link_env() # Persist CUDALINK_LIB_PATH -> venv (TD library mode) self.phase5_missing_pins() self.phase6_conflict_prone() self.phase7_numpy_lock() From 790ef42c9dfcc4752f98f2cb426a13fd476fdf5e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jul 2026 22:52:39 -0400 Subject: [PATCH 07/21] fix: persist CUDALINK_DOORBELL=1 in phase4c for doorbell fast path phase4c_cuda_link_env only declared CUDALINK_LIB_PATH, so a fresh install silently degraded the native-wait IPC path to poll-sleep. SD's TD topology is bidirectional (TD Sender + SD Exporter are each a producer on their own IPC leg), and TD's Sender runs in a separate bundled-Python process that a runtime os.environ.setdefault can't reach -- the doorbell var must be setx-persisted like CUDALINK_LIB_PATH. Found via a clean-slate reproducibility test of PR #2's cuda-link handling (stripped venv package + all cuda-link env vars, then re-ran phase4b/phase4c to confirm they're self-sufficient). --- sd_installer/installer.py | 71 +++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index d62b5e0..dd7284e 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -16,7 +16,6 @@ from pathlib import Path from typing import Callable, Optional - # Version pins - packages NOT in setup.py that must be manually pinned MANUAL_PINS = { "numpy": "1.26.4", @@ -44,7 +43,10 @@ # force an MSVC/nvcc source build. Install the prebuilt wheel directly instead, --no-deps, so this # extra is never triggered. Only a cp311 wheel is published. CUDA_LINK_WHEELS = { - (3, 11): "https://github.com/forkni/cuda-link/releases/download/v1.12.1/cuda_link-1.12.1-cp311-cp311-win_amd64.whl", + ( + 3, + 11, + ): "https://github.com/forkni/cuda-link/releases/download/v1.12.1/cuda_link-1.12.1-cp311-cp311-win_amd64.whl", } # PyTorch configurations by CUDA version @@ -297,18 +299,30 @@ def phase4b_cuda_link(self): print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") def phase4c_cuda_link_env(self): - """Phase 4c: Persist CUDALINK_LIB_PATH -> this venv's site-packages (Windows only). + """Phase 4c: Persist CUDALINK_LIB_PATH and CUDALINK_DOORBELL (Windows only). + CUDALINK_LIB_PATH -> this venv's site-packages: TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to enable "library mode" (sys.path injection of the installed cuda_link package, aliasing the 14 mirror DAT names). Persisting it here via `setx` means every TD process launched after this install inherits it automatically -- no manual env-var step. + CUDALINK_DOORBELL=1: + Enables the Win32 named-event doorbell so the cuda-link native wait backend reaches its + low-latency target instead of silently falling back to poll-sleep. The SD<->TD topology is + bidirectional -- TD's Sender and SD's Exporter are each a producer on their own IPC leg -- + and the doorbell event is only created by a producer whose CUDALINK_DOORBELL=1. TD's Sender + runs inside TD's own bundled-Python *process*, which reads its environment from user/system + scope only; a runtime `os.environ.setdefault` (as used for + CUDALINK_TORCH_GPU_WAIT_ADAPTIVE in td_manager.py) cannot reach a separate process, so this + must be persisted here instead. CUDALINK_WAIT_BACKEND is deliberately left unset -- its + default "auto" already selects the native path. + setx writes to HKCU\\Environment (user scope) and only affects processes started *after* it runs, so TD must be (re)started after installation to pick it up. This intentionally overwrites any prior manual value (e.g. an older cuda_link_lib\\ target). Non-fatal: if setx fails or this isn't Windows, TD simply falls back to the mirror-DAT - classic mode. + classic mode (for CUDALINK_LIB_PATH) or the poll-sleep wait backend (for CUDALINK_DOORBELL). """ if sys.platform != "win32": return # setx is a Windows-only mechanism; non-Windows TD launches are unaffected @@ -316,20 +330,31 @@ def phase4c_cuda_link_env(self): result = self._run_python("import sysconfig; print(sysconfig.get_paths()['purelib'])") if result.returncode != 0 or not result.stdout.strip(): print(" WARNING: Could not resolve venv site-packages path, skipping CUDALINK_LIB_PATH setup") - return - - site_packages = result.stdout.strip() - self._report_progress(f"Persisting CUDALINK_LIB_PATH -> {site_packages}", 4, 8) - setx_result = subprocess.run( - ["setx", "CUDALINK_LIB_PATH", site_packages], - capture_output=True, - text=True, - ) - if setx_result.returncode != 0: - print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_result.stderr.strip()}") else: - print(" CUDALINK_LIB_PATH persisted for this user account.") - print(" Restart TouchDesigner (and any open shells) to pick up the new environment variable.") + site_packages = result.stdout.strip() + self._report_progress(f"Persisting CUDALINK_LIB_PATH -> {site_packages}", 4, 8) + setx_result = subprocess.run( + ["setx", "CUDALINK_LIB_PATH", site_packages], + capture_output=True, + text=True, + ) + if setx_result.returncode != 0: + print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_result.stderr.strip()}") + else: + print(" CUDALINK_LIB_PATH persisted for this user account.") + print(" Restart TouchDesigner (and any open shells) to pick up the new environment variable.") + + # CUDALINK_DOORBELL=1 enables the Win32 named-event doorbell so the cuda-link native wait + # backend reaches its low-latency target. Must be set on the *producer* side, and SD's TD + # topology is bidirectional (TD Sender + SD Exporter are both producers). TD's Sender runs + # in TD's own bundled-Python *process*, which reads env from user/system scope only -- a + # runtime os.environ.setdefault in td_manager.py can't reach it, so it must be persisted + # here. Independent of the site-packages resolution above, so it runs even if that warned. + db_result = subprocess.run(["setx", "CUDALINK_DOORBELL", "1"], capture_output=True, text=True) + if db_result.returncode != 0: + print(f" WARNING: setx failed to persist CUDALINK_DOORBELL: {db_result.stderr.strip()}") + else: + print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") def phase5_missing_pins(self): """Phase 5: Install packages not pinned in setup.py and fix diffusers.""" @@ -370,11 +395,13 @@ def phase7_numpy_lock(self): self._run_pip([f"protobuf=={MANUAL_PINS['protobuf']}", "--force-reinstall"]) self._report_progress("Applying security floor pins (idna, Mako, urllib3)...", 7, 8) - self._run_pip([ - f"idna{MANUAL_PINS['idna']}", - f"Mako{MANUAL_PINS['Mako']}", - f"urllib3{MANUAL_PINS['urllib3']}", - ]) + self._run_pip( + [ + f"idna{MANUAL_PINS['idna']}", + f"Mako{MANUAL_PINS['Mako']}", + f"urllib3{MANUAL_PINS['urllib3']}", + ] + ) def phase8_verify(self) -> bool: """Phase 8: Verify installation with import tests.""" From 06788dc353cfc18e474edf3164335cda6f92be25 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Jul 2026 23:03:22 -0400 Subject: [PATCH 08/21] style: sort import blocks (ruff I001) in package __init__/__main__ --- sd_installer/__init__.py | 1 - sd_installer/__main__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/sd_installer/__init__.py b/sd_installer/__init__.py index 0cbabfc..6c059c8 100644 --- a/sd_installer/__init__.py +++ b/sd_installer/__init__.py @@ -16,5 +16,4 @@ from .installer import Installer from .verifier import Verifier - __all__ = ["Installer", "Verifier", "__version__"] diff --git a/sd_installer/__main__.py b/sd_installer/__main__.py index e878b7a..2a0fbda 100644 --- a/sd_installer/__main__.py +++ b/sd_installer/__main__.py @@ -2,6 +2,5 @@ from .cli import main - if __name__ == "__main__": exit(main()) From 6b082dddefb29ceb9c8c06a69ade2050bab0a841 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:08:23 -0400 Subject: [PATCH 09/21] chore: add Claude Code automated PR review harness --- .github/instructions/python.instructions.md | 39 +++++++++++++++++ .github/workflows/claude-code-review.yml | 47 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .github/instructions/python.instructions.md create mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md new file mode 100644 index 0000000..76d437e --- /dev/null +++ b/.github/instructions/python.instructions.md @@ -0,0 +1,39 @@ +--- +applyTo: "**/*.py" +--- + +# Python review guidance — StreamDiffusion-installer (Windows CLI installer) + +This is a Windows CLI (`sd-installer`) that drives a multi-phase install of CUDA/torch/ModelOpt/ +ONNX/TensorRT dependencies by shelling out to `pip`, `nvidia-smi`, and related tools. There is no +ruff/pyrefly config in this repo (unlike the sibling `StreamDiffusion` library repo) — don't assume +its style conventions apply here. + +## Flag these + +- **`subprocess` calls with `shell=True`** where any part of the command is not a fixed literal — + string-built commands incorporating a version string, path, or other variable input are a command + injection risk. Prefer an argument list with `shell=False` (the existing pattern in + `sd_installer/report.py`'s `_nvidia_smi_driver()`). +- **Secret-shaped values leaking into logs or error reports.** `sd_installer/report.py` defines + `ENV_ALLOWLIST_PREFIXES` (`CUDALINK_`, `HF_`, `SD_`, `SDTD_`) and + `ENV_DENYLIST_SUBSTRINGS` (`TOKEN`, `KEY`, `SECRET`, `PASSWORD`, `PASSWD`, `CRED`) as the one + intentional pattern for env-var capture — new code that dumps environment/config state should + reuse or extend this, not add a parallel allowlist/denylist. +- **Install phases that leave the environment partially modified without being resumable or + clearly reported.** `installer.py`'s `install()` runs a `phases` list wrapped in try/finally — + a new phase should fail loudly (write an error report, re-raise) rather than silently continuing + past a failed step. +- **Error-reporting code that can raise.** `report.py` / `installer.py`'s + `_write_install_error_report()` must stay best-effort — any exception inside report generation + must be caught internally so it never masks or replaces the real installation error being + reported. +- **Windows-path assumptions that break on other separators/drives** — this installer targets + Windows specifically (see `pyproject.toml` classifiers), but avoid hardcoded backslash-joined + paths where `pathlib.Path` composition already works portably. + +## Do NOT flag + +- Missing type hints beyond what's already present — this repo doesn't enforce a strict typing + policy like the sibling library repo. +- Style nits (import order, line length) — there's no ruff/black config in this repo to violate. diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..2cb25e0 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,47 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # base-branch filter: PRs *into* main, or into the deps stack that + # feat/installer-error-report chains on, so Claude fires on both stacked PRs. + branches: [main, deps/cuda-link-1.12-torch-modelopt-onnx] + +jobs: + review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + track_progress: true # forces tag mode -> sticky comment updates in place + use_sticky_comment: true # one review comment across pushes, not a new one each time + show_full_output: true # surfaces full tool output/denials in the run log for debugging + plugin_marketplaces: "https://github.com/anthropics/claude-code.git" + plugins: "code-review@claude-code-plugins" + prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" + claude_args: >- + --max-turns 60 + --allowedTools "Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git fetch:*),Bash(git merge-base:*),Bash(git rev-parse:*),Bash(git remote:*),Bash(git branch:*),Bash(git show:*),Bash(echo:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh release view:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Skill,Write(/tmp/*)" + --append-system-prompt "Repo-specific focus: this is a Windows CLI installer that shells out to + pip/nvidia-smi/conda and drives multi-phase installs (cuda-link, torch, modelopt, onnx, tensorrt). + Flag subprocess calls that are shell=True with unsanitized/user-influenced input, install phases + that leave the environment partially-modified on failure without a clear rollback or resumable + state, and any place a secret-shaped env var (TOKEN/KEY/SECRET/PASSWORD/CRED) could leak into a + log or error report -- see sd_installer/report.py's ENV_ALLOWLIST_PREFIXES/ENV_DENYLIST_SUBSTRINGS + pattern, which is the intentional convention to follow, not duplicate ad hoc. Error-reporting code + (report.py, installer.py's _write_install_error_report) must stay best-effort: it must never raise + past its own try/except, since a reporting bug must not mask the real install failure. Skip generic + style nits. + Tool usage: never run filesystem-wide search (find /, grep -r /), stay inside the repo checkout. + Never redirect Bash output to a file with > or >>, it is blocked; pipe to head/tail or just read + the direct stdout instead." From 316d50b260c44adaa815fb159cc8cf4d533dbe9b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:09:00 -0400 Subject: [PATCH 10/21] chore: add Claude Code automated PR review harness --- .github/instructions/python.instructions.md | 39 +++++++++++++++++ .github/workflows/claude-code-review.yml | 47 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .github/instructions/python.instructions.md create mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md new file mode 100644 index 0000000..76d437e --- /dev/null +++ b/.github/instructions/python.instructions.md @@ -0,0 +1,39 @@ +--- +applyTo: "**/*.py" +--- + +# Python review guidance — StreamDiffusion-installer (Windows CLI installer) + +This is a Windows CLI (`sd-installer`) that drives a multi-phase install of CUDA/torch/ModelOpt/ +ONNX/TensorRT dependencies by shelling out to `pip`, `nvidia-smi`, and related tools. There is no +ruff/pyrefly config in this repo (unlike the sibling `StreamDiffusion` library repo) — don't assume +its style conventions apply here. + +## Flag these + +- **`subprocess` calls with `shell=True`** where any part of the command is not a fixed literal — + string-built commands incorporating a version string, path, or other variable input are a command + injection risk. Prefer an argument list with `shell=False` (the existing pattern in + `sd_installer/report.py`'s `_nvidia_smi_driver()`). +- **Secret-shaped values leaking into logs or error reports.** `sd_installer/report.py` defines + `ENV_ALLOWLIST_PREFIXES` (`CUDALINK_`, `HF_`, `SD_`, `SDTD_`) and + `ENV_DENYLIST_SUBSTRINGS` (`TOKEN`, `KEY`, `SECRET`, `PASSWORD`, `PASSWD`, `CRED`) as the one + intentional pattern for env-var capture — new code that dumps environment/config state should + reuse or extend this, not add a parallel allowlist/denylist. +- **Install phases that leave the environment partially modified without being resumable or + clearly reported.** `installer.py`'s `install()` runs a `phases` list wrapped in try/finally — + a new phase should fail loudly (write an error report, re-raise) rather than silently continuing + past a failed step. +- **Error-reporting code that can raise.** `report.py` / `installer.py`'s + `_write_install_error_report()` must stay best-effort — any exception inside report generation + must be caught internally so it never masks or replaces the real installation error being + reported. +- **Windows-path assumptions that break on other separators/drives** — this installer targets + Windows specifically (see `pyproject.toml` classifiers), but avoid hardcoded backslash-joined + paths where `pathlib.Path` composition already works portably. + +## Do NOT flag + +- Missing type hints beyond what's already present — this repo doesn't enforce a strict typing + policy like the sibling library repo. +- Style nits (import order, line length) — there's no ruff/black config in this repo to violate. diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 0000000..2cb25e0 --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,47 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # base-branch filter: PRs *into* main, or into the deps stack that + # feat/installer-error-report chains on, so Claude fires on both stacked PRs. + branches: [main, deps/cuda-link-1.12-torch-modelopt-onnx] + +jobs: + review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + id-token: write + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + track_progress: true # forces tag mode -> sticky comment updates in place + use_sticky_comment: true # one review comment across pushes, not a new one each time + show_full_output: true # surfaces full tool output/denials in the run log for debugging + plugin_marketplaces: "https://github.com/anthropics/claude-code.git" + plugins: "code-review@claude-code-plugins" + prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" + claude_args: >- + --max-turns 60 + --allowedTools "Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git fetch:*),Bash(git merge-base:*),Bash(git rev-parse:*),Bash(git remote:*),Bash(git branch:*),Bash(git show:*),Bash(echo:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh release view:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Skill,Write(/tmp/*)" + --append-system-prompt "Repo-specific focus: this is a Windows CLI installer that shells out to + pip/nvidia-smi/conda and drives multi-phase installs (cuda-link, torch, modelopt, onnx, tensorrt). + Flag subprocess calls that are shell=True with unsanitized/user-influenced input, install phases + that leave the environment partially-modified on failure without a clear rollback or resumable + state, and any place a secret-shaped env var (TOKEN/KEY/SECRET/PASSWORD/CRED) could leak into a + log or error report -- see sd_installer/report.py's ENV_ALLOWLIST_PREFIXES/ENV_DENYLIST_SUBSTRINGS + pattern, which is the intentional convention to follow, not duplicate ad hoc. Error-reporting code + (report.py, installer.py's _write_install_error_report) must stay best-effort: it must never raise + past its own try/except, since a reporting bug must not mask the real install failure. Skip generic + style nits. + Tool usage: never run filesystem-wide search (find /, grep -r /), stay inside the repo checkout. + Never redirect Bash output to a file with > or >>, it is blocked; pipe to head/tail or just read + the direct stdout instead." From 8d538cff823dbd75ed50191b169d89796f174a51 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:11:26 -0400 Subject: [PATCH 11/21] feat: add install error-report generation (schema v1) --- sd_installer/cli.py | 53 +++++++++++ sd_installer/installer.py | 90 ++++++++++++++++--- sd_installer/report.py | 184 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 14 deletions(-) create mode 100644 sd_installer/report.py diff --git a/sd_installer/cli.py b/sd_installer/cli.py index 3f7110c..421bed6 100644 --- a/sd_installer/cli.py +++ b/sd_installer/cli.py @@ -9,6 +9,7 @@ python -m sd_installer install --cuda cu128 # Install with specific CUDA python -m sd_installer verify # Verify existing installation python -m sd_installer diagnose # Detailed diagnostics + python -m sd_installer report # Write a diagnostic report on demand python -m sd_installer repair # Auto-fix known issues python -m sd_installer generate-bat # Generate standalone batch file python -m sd_installer install-tensorrt # Install TensorRT packages @@ -202,6 +203,50 @@ def cmd_diagnose(args): return 0 +def cmd_report(args): + """Generate a diagnostic report on demand (no failure required).""" + from .report import write_error_report + + try: + base = Path(args.base_folder) if args.base_folder else find_base_folder() + except RuntimeError as e: + print(f"ERROR: {e}") + return 1 + + # Find Python executable in venv + venv_path = base / "venv" + if sys.platform == "win32": + python_exe = venv_path / "Scripts" / "python.exe" + else: + python_exe = venv_path / "bin" / "python" + + if not python_exe.exists(): + print(f"ERROR: Virtual environment not found at {venv_path}") + return 1 + + print("Generating StreamDiffusionTD Diagnostic Report") + print("=" * 40) + + out_dir = Path(args.output) if args.output else base / "error_reports" + report_path = write_error_report( + out_dir, + { + "stage": "installation", + "phase": "manual", + "python_exe": str(python_exe), + "base_folder": str(base), + "venv_path": str(venv_path), + }, + ) + + if report_path: + print(f"\nReport written to: {report_path}") + return 0 + + print("ERROR: Failed to write report.") + return 1 + + def cmd_repair(args): """Auto-fix known issues.""" from .verifier import Verifier @@ -384,6 +429,14 @@ def main(): diagnose_parser = subparsers.add_parser("diagnose", help="Run detailed diagnostics") diagnose_parser.set_defaults(func=cmd_diagnose) + # report command + report_parser = subparsers.add_parser("report", help="Generate a diagnostic report on demand") + report_parser.add_argument( + "--output", + help="Directory to write the report into (default: base folder/error_reports)", + ) + report_parser.set_defaults(func=cmd_report) + # repair command repair_parser = subparsers.add_parser("repair", help="Auto-fix known issues") repair_parser.add_argument( diff --git a/sd_installer/installer.py b/sd_installer/installer.py index dd7284e..a2d55a7 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -120,6 +120,10 @@ def __init__( self.pytorch_config = PYTORCH_CONFIGS[cuda_version] + # Populated during install() for the on-failure diagnostic report (see report.py). + self.current_phase: Optional[str] = None + self._last_pip_stderr: Optional[str] = None + @property def python_exe(self) -> Path: """Path to Python executable in venv.""" @@ -154,6 +158,7 @@ def _run_pip(self, args: list, check: bool = True, cwd: Optional[Path] = None) - ) if check and result.returncode != 0: print(f" STDERR: {result.stderr}") + self._last_pip_stderr = result.stderr raise RuntimeError(f"pip failed: {result.stderr}") return result @@ -299,7 +304,8 @@ def phase4b_cuda_link(self): print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") def phase4c_cuda_link_env(self): - """Phase 4c: Persist CUDALINK_LIB_PATH and CUDALINK_DOORBELL (Windows only). + """Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, and SDTD_BASE_FOLDER_PATH + (Windows only). CUDALINK_LIB_PATH -> this venv's site-packages: TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to @@ -318,11 +324,19 @@ def phase4c_cuda_link_env(self): must be persisted here instead. CUDALINK_WAIT_BACKEND is deliberately left unset -- its default "auto" already selects the native path. + SDTD_BASE_FOLDER_PATH -> this install's base_folder (StreamDiffusion repo root): + Same cross-process problem as CUDALINK_DOORBELL -- TD's Python process needs a reliable + anchor to the repo root for the inference-side error-report dump + (streamdiffusion.utils.diagnostics.write_error_report), and a runtime os.environ.setdefault + in td_manager.py can't reach that separate process. Persisting it here means every + TD-launched Python inherits it without a manual env-var step. + setx writes to HKCU\\Environment (user scope) and only affects processes started *after* it runs, so TD must be (re)started after installation to pick it up. This intentionally overwrites any prior manual value (e.g. an older cuda_link_lib\\ target). Non-fatal: if setx fails or this isn't Windows, TD simply falls back to the mirror-DAT - classic mode (for CUDALINK_LIB_PATH) or the poll-sleep wait backend (for CUDALINK_DOORBELL). + classic mode (for CUDALINK_LIB_PATH), the poll-sleep wait backend (for CUDALINK_DOORBELL), + or the diagnostics module's own __file__-relative fallback (for SDTD_BASE_FOLDER_PATH). """ if sys.platform != "win32": return # setx is a Windows-only mechanism; non-Windows TD launches are unaffected @@ -356,6 +370,17 @@ def phase4c_cuda_link_env(self): else: print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") + # SDTD_BASE_FOLDER_PATH -> repo root, so TD's Python process can locate error_reports/ + # without a manual env-var step. Independent of the blocks above, so it runs even if + # either warned. + base_result = subprocess.run( + ["setx", "SDTD_BASE_FOLDER_PATH", str(self.base_folder)], capture_output=True, text=True + ) + if base_result.returncode != 0: + print(f" WARNING: setx failed to persist SDTD_BASE_FOLDER_PATH: {base_result.stderr.strip()}") + else: + print(f" SDTD_BASE_FOLDER_PATH={self.base_folder} persisted (anchors error-report dumps).") + def phase5_missing_pins(self): """Phase 5: Install packages not pinned in setup.py and fix diffusers.""" self._report_progress("Installing packages not in setup.py (timm, python-osc, peft)...", 5, 8) @@ -411,6 +436,31 @@ def phase8_verify(self) -> bool: verifier = Verifier(str(self.python_exe)) return verifier.run_all() + def _write_install_error_report(self, exc: BaseException) -> None: + """Best-effort diagnostic dump on install failure. Never raises -- a bug here must + not mask the real installation error, which the caller re-raises regardless.""" + try: + from .report import write_error_report + + report_path = write_error_report( + self.base_folder / "error_reports", + { + "stage": "installation", + "exc": exc, + "phase": self.current_phase, + "python_exe": str(self.python_exe), + "base_folder": str(self.base_folder), + "cuda_version": self.cuda_version, + "pytorch_config": self.pytorch_config, + "venv_path": str(self.venv_path), + "pip_stderr": self._last_pip_stderr, + }, + ) + if report_path: + print(f"\n Error report written to: {report_path}") + except Exception as report_exc: + print(f" WARNING: Failed to generate error report: {report_exc}") + def install(self, python_exe: Optional[str] = None) -> bool: """ Run full installation. @@ -433,18 +483,30 @@ def install(self, python_exe: Optional[str] = None) -> bool: # Create venv if needed self.create_venv(python_exe) - # Run installation phases - self.phase1_foundation() - self.phase2_pytorch() - self.phase3_xformers() - self.phase3b_insightface() # Pre-install insightface from wheel (Windows) - self.phase4_streamdiffusion() - self.phase4b_cuda_link() # Pre-install cuda-link from wheel (CUDA-IPC transport) - self.phase4c_cuda_link_env() # Persist CUDALINK_LIB_PATH -> venv (TD library mode) - self.phase5_missing_pins() - self.phase6_conflict_prone() - self.phase7_numpy_lock() - success = self.phase8_verify() + # Run installation phases. current_phase is tracked so a failure report (see + # _write_install_error_report) can name the phase that was running when it broke. + phases = [ + ("phase1_foundation", self.phase1_foundation), + ("phase2_pytorch", self.phase2_pytorch), + ("phase3_xformers", self.phase3_xformers), + ("phase3b_insightface", self.phase3b_insightface), # insightface from wheel (Windows) + ("phase4_streamdiffusion", self.phase4_streamdiffusion), + ("phase4b_cuda_link", self.phase4b_cuda_link), # cuda-link from wheel (CUDA-IPC transport) + ("phase4c_cuda_link_env", self.phase4c_cuda_link_env), # CUDALINK_LIB_PATH -> venv (TD library mode) + ("phase5_missing_pins", self.phase5_missing_pins), + ("phase6_conflict_prone", self.phase6_conflict_prone), + ("phase7_numpy_lock", self.phase7_numpy_lock), + ] + + try: + for name, phase_fn in phases: + self.current_phase = name + phase_fn() + self.current_phase = "phase8_verify" + success = self.phase8_verify() + except Exception as exc: + self._write_install_error_report(exc) + raise print() print("=" * 50) diff --git a/sd_installer/report.py b/sd_installer/report.py new file mode 100644 index 0000000..97a27c1 --- /dev/null +++ b/sd_installer/report.py @@ -0,0 +1,184 @@ +""" +StreamDiffusionTD Installer Error Report + +Builds a self-contained, human-readable diagnostic report (schema v1) when an +installation phase fails, and backs the manual `report` CLI subcommand. + +Best-effort by design: report generation must never raise past write_error_report's +own try/except, so a bug in the reporter never masks the real installation error. +""" + +import datetime +import os +import platform +import subprocess +import sys +import traceback +from pathlib import Path +from typing import Optional + +from .verifier import Verifier, match_known_error + +SCHEMA_VERSION = "v1" +# Only these env-var prefixes are dumped -- never the full os.environ (avoids leaking secrets). +ENV_ALLOWLIST_PREFIXES = ("CUDALINK_", "HF_", "SD_", "SDTD_") +# Even an allowlisted-prefix var is dropped if its name contains one of these substrings -- +# a prefix match alone isn't enough (e.g. HF_TOKEN matches "HF_" but must never be dumped). +ENV_DENYLIST_SUBSTRINGS = ("TOKEN", "KEY", "SECRET", "PASSWORD", "PASSWD", "CRED") + + +def _utc_now() -> datetime.datetime: + return datetime.datetime.now(datetime.timezone.utc) + + +def _nvidia_smi_driver() -> str: + """Query the NVIDIA driver version via nvidia-smi. Best-effort.""" + try: + result = subprocess.run( + ["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip().splitlines()[0] + except Exception: + pass + return "unknown" + + +def _collect_env_allowlist() -> dict: + """Collect only allow-listed env vars -- never dump full os.environ (secrets).""" + return { + k: v + for k, v in sorted(os.environ.items()) + if k.startswith(ENV_ALLOWLIST_PREFIXES) and not any(bad in k.upper() for bad in ENV_DENYLIST_SUBSTRINGS) + } + + +def _format_section(title: str, lines: list) -> str: + body = "\n".join(lines) if lines else "(none)" + return f"== {title} ==\n{body}\n" + + +def build_report_text(context: dict) -> str: + """ + Build the schema-v1 diagnostic report text. + + Expected context keys (all optional unless noted): + stage (str, required): "installation" + exc (BaseException): the caught exception -- feeds SUMMARY + TRACEBACK + exc_text (str): pre-formatted traceback text, used when `exc` is absent + phase (str): name of the failing (or current, for manual reports) phase + python_exe (str | Path): venv python -- used to run Verifier.diagnose() + base_folder, cuda_version, pytorch_config, venv_path: installer config + pip_stderr (str): captured stderr tail from the failing pip invocation + + Returns: + Full report text, ready to write to disk. + """ + stage = context.get("stage", "installation") + lines = [ + "StreamDiffusionTD Error Report (schema v1)", + f"Generated: {_utc_now().isoformat()}", + f"Stage: {stage}", + "-" * 50, + "", + ] + + # == SUMMARY == + exc = context.get("exc") + if exc is not None: + error_line = f"{type(exc).__name__}: {exc}" + elif context.get("exc_text"): + error_line = context["exc_text"].strip().splitlines()[-1] + else: + error_line = "unknown" + summary_lines = [f"Error: {error_line}", f"Context: {context.get('phase', 'unknown')}"] + + pip_stderr = context.get("pip_stderr") + match_text = pip_stderr or (str(exc) if exc is not None else "") + known = None + if match_text: + try: + known = match_known_error(match_text) + except Exception: + known = None + if known: + summary_lines.append(f"Known-cause match: {known['cause']} -- fix: {known['fix']}") + lines.append(_format_section("SUMMARY", summary_lines)) + + # == TRACEBACK == + if exc is not None: + tb_text = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + else: + tb_text = context.get("exc_text") or "(no traceback available)" + lines.append(_format_section("TRACEBACK", [tb_text.rstrip("\n")])) + + # == SYSTEM == / == VERSIONS == + system_lines = [ + f"OS: {platform.platform()}", + f"Python (host): {sys.version.replace(chr(10), ' ')}", + f"NVIDIA driver: {_nvidia_smi_driver()}", + ] + versions_lines = [] + python_exe = context.get("python_exe") + if python_exe: + try: + info = Verifier(str(python_exe)).diagnose() + gpu = info.get("gpu", {}) + if gpu: + system_lines.append(f"GPU: {gpu.get('name', 'unknown')}") + if "vram_mb" in gpu: + system_lines.append(f"VRAM total: {gpu['vram_mb']} MB") + if "compute_capability" in gpu: + system_lines.append(f"Compute capability: {gpu['compute_capability']}") + for pkg, version in info.get("versions", {}).items(): + versions_lines.append(f"{pkg}: {version}") + except Exception as diag_exc: + versions_lines.append(f"(Verifier.diagnose() failed: {diag_exc})") + else: + versions_lines.append("(no python_exe provided -- venv package versions unavailable)") + lines.append(_format_section("SYSTEM", system_lines)) + lines.append(_format_section("VERSIONS", versions_lines)) + + # == CONFIG == + config_keys = ("base_folder", "cuda_version", "pytorch_config", "venv_path") + config_lines = [f"{key}: {context[key]}" for key in config_keys if key in context] + lines.append(_format_section("CONFIG", config_lines)) + + # == ENV == + env_lines = [f"{k}={v}" for k, v in _collect_env_allowlist().items()] + lines.append(_format_section("ENV", env_lines)) + + # == LOG TAIL == + log_lines = pip_stderr.strip().splitlines()[-50:] if pip_stderr else [] + lines.append(_format_section("LOG TAIL", log_lines)) + + return "\n".join(lines) + + +def write_error_report(out_dir, context: dict) -> Optional[Path]: + """ + Build and write a diagnostic report to disk. + + Best-effort -- any failure here is caught, printed, and swallowed rather than + raised, so a reporting bug never masks the original installation error. + + Args: + out_dir: Directory to write the report into (created if missing). + context: See build_report_text() for expected keys. + + Returns: + Path to the written report, or None if writing failed. + """ + try: + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + timestamp = _utc_now().strftime("%Y%m%d_%H%M%S") + report_path = out_dir / f"install_error_report_{timestamp}.txt" + report_path.write_text(build_report_text(context), encoding="utf-8") + return report_path + except Exception as write_exc: + print(f" WARNING: Failed to write error report: {write_exc}") + return None From eb35ff33c3cbb39d6eef5be96f409eaf7208d98a Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:33:26 -0400 Subject: [PATCH 12/21] chore: raise review harness max-turns 60 -> 120 The multi-agent code-review plugin exhausted 60 turns on PR #2 (feat/installer-error-report, a stacked diff with no CLAUDE.md to shortcut convention discovery) before posting a verdict. Bumping headroom rather than trusting a partial/incomplete review. --- .github/workflows/claude-code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2cb25e0..126e6d4 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -30,7 +30,7 @@ jobs: plugins: "code-review@claude-code-plugins" prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" claude_args: >- - --max-turns 60 + --max-turns 120 --allowedTools "Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git fetch:*),Bash(git merge-base:*),Bash(git rev-parse:*),Bash(git remote:*),Bash(git branch:*),Bash(git show:*),Bash(echo:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh release view:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Skill,Write(/tmp/*)" --append-system-prompt "Repo-specific focus: this is a Windows CLI installer that shells out to pip/nvidia-smi/conda and drives multi-phase installs (cuda-link, torch, modelopt, onnx, tensorrt). From d414d603fe52d247c8cc90fb47077781068dcf23 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:34:08 -0400 Subject: [PATCH 13/21] chore: raise review harness max-turns 60 -> 120 The multi-agent code-review plugin exhausted 60 turns on PR #2 (feat/installer-error-report, a stacked diff with no CLAUDE.md to shortcut convention discovery) before posting a verdict. Bumping headroom rather than trusting a partial/incomplete review. --- .github/workflows/claude-code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 2cb25e0..126e6d4 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -30,7 +30,7 @@ jobs: plugins: "code-review@claude-code-plugins" prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}" claude_args: >- - --max-turns 60 + --max-turns 120 --allowedTools "Bash(git diff:*),Bash(git status:*),Bash(git log:*),Bash(git fetch:*),Bash(git merge-base:*),Bash(git rev-parse:*),Bash(git remote:*),Bash(git branch:*),Bash(git show:*),Bash(echo:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh release view:*),Bash(python:*),Bash(python3:*),Bash(pytest:*),Skill,Write(/tmp/*)" --append-system-prompt "Repo-specific focus: this is a Windows CLI installer that shells out to pip/nvidia-smi/conda and drives multi-phase installs (cuda-link, torch, modelopt, onnx, tensorrt). From 587ac3e85ded0930feec079f310db67a63ed2c9c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 22:53:03 -0400 Subject: [PATCH 14/21] fix: guard setx calls in phase4c_cuda_link_env against OSError subprocess.run(['setx', ...]) can raise FileNotFoundError/OSError if setx isn't resolvable, which propagated unhandled and aborted the install before phases 5-8 ran -- contradicting the method's own 'non-fatal' docstring. Wrap both calls in try/except OSError, matching the existing returncode!=0 warning path. Flagged independently by both the Claude and Copilot review passes on PR #1. --- sd_installer/installer.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index dd7284e..1b95032 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -333,16 +333,20 @@ def phase4c_cuda_link_env(self): else: site_packages = result.stdout.strip() self._report_progress(f"Persisting CUDALINK_LIB_PATH -> {site_packages}", 4, 8) - setx_result = subprocess.run( - ["setx", "CUDALINK_LIB_PATH", site_packages], - capture_output=True, - text=True, - ) - if setx_result.returncode != 0: - print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_result.stderr.strip()}") + try: + setx_result = subprocess.run( + ["setx", "CUDALINK_LIB_PATH", site_packages], + capture_output=True, + text=True, + ) + except OSError as setx_exc: + print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_exc}") else: - print(" CUDALINK_LIB_PATH persisted for this user account.") - print(" Restart TouchDesigner (and any open shells) to pick up the new environment variable.") + if setx_result.returncode != 0: + print(f" WARNING: setx failed to persist CUDALINK_LIB_PATH: {setx_result.stderr.strip()}") + else: + print(" CUDALINK_LIB_PATH persisted for this user account.") + print(" Restart TouchDesigner (and any open shells) to pick up the new environment variable.") # CUDALINK_DOORBELL=1 enables the Win32 named-event doorbell so the cuda-link native wait # backend reaches its low-latency target. Must be set on the *producer* side, and SD's TD @@ -350,11 +354,15 @@ def phase4c_cuda_link_env(self): # in TD's own bundled-Python *process*, which reads env from user/system scope only -- a # runtime os.environ.setdefault in td_manager.py can't reach it, so it must be persisted # here. Independent of the site-packages resolution above, so it runs even if that warned. - db_result = subprocess.run(["setx", "CUDALINK_DOORBELL", "1"], capture_output=True, text=True) - if db_result.returncode != 0: - print(f" WARNING: setx failed to persist CUDALINK_DOORBELL: {db_result.stderr.strip()}") + try: + db_result = subprocess.run(["setx", "CUDALINK_DOORBELL", "1"], capture_output=True, text=True) + except OSError as setx_exc: + print(f" WARNING: setx failed to persist CUDALINK_DOORBELL: {setx_exc}") else: - print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") + if db_result.returncode != 0: + print(f" WARNING: setx failed to persist CUDALINK_DOORBELL: {db_result.stderr.strip()}") + else: + print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") def phase5_missing_pins(self): """Phase 5: Install packages not pinned in setup.py and fix diffusers.""" From 9f38ea7abd84bb3fde72330289f4731737fce20c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Jul 2026 23:13:26 -0400 Subject: [PATCH 15/21] chore: bump install banner to v0.4.0 --- sd_installer/installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 0504865..043412b 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -480,7 +480,7 @@ def install(self, python_exe: Optional[str] = None) -> bool: True if installation and verification succeeded. """ print("=" * 50) - print(" StreamDiffusionTD v0.3.2 Installation") + print(" StreamDiffusionTD v0.4.0 Installation") print(" Daydream Fork with StreamV2V") print("=" * 50) print() @@ -553,7 +553,7 @@ def generate_batch_file(self, output_path: Optional[str] = None, python_exe: Opt content = f'''@echo off echo ======================================== -echo StreamDiffusionTD v0.3.2 Installation +echo StreamDiffusionTD v0.4.0 Installation echo Daydream Fork with StreamV2V echo ======================================== From 2ea99e07d5d14d803db6608e11f74eaef595451e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 14 Jul 2026 18:39:16 -0400 Subject: [PATCH 16/21] fix: consolidate TORCH_CUDNN_V8_API_ENABLED to single setx site in phase4c The TD launcher (StreamDiffusionExt.Startstream) set this env var in its own stream subprocess on every launch, duplicated across three launch-path variants (windowless Popen, PowerShell, cmd batch). It also duplicated three now-removed no-op/harmful vars: CUDA_LAUNCH_BLOCKING (torch default), TORCHINDUCTOR_FX_GRAPH_CACHE (inert -- no torch.compile usage in streamdiffusion), and SDTD_L2_PERSIST/_MB (forced L2 persistence on regardless of backend, masking streamdiffusion's new mode-aware default that keeps it off under TensorRT). Persist TORCH_CUDNN_V8_API_ENABLED here via setx instead, next to CUDALINK_DOORBELL, so there's one declaration site inherited by every TD-launched process. Co-Authored-By: Claude Sonnet 5 --- sd_installer/installer.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 043412b..41e5388 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -304,8 +304,8 @@ def phase4b_cuda_link(self): print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") def phase4c_cuda_link_env(self): - """Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, and SDTD_BASE_FOLDER_PATH - (Windows only). + """Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, TORCH_CUDNN_V8_API_ENABLED, + and SDTD_BASE_FOLDER_PATH (Windows only). CUDALINK_LIB_PATH -> this venv's site-packages: TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to @@ -324,6 +324,14 @@ def phase4c_cuda_link_env(self): must be persisted here instead. CUDALINK_WAIT_BACKEND is deliberately left unset -- its default "auto" already selects the native path. + TORCH_CUDNN_V8_API_ENABLED=1: + Opts torch into the cuDNN v8 API. Persisted here (rather than set per-launch by the TD + component) so there's a single declaration site, matching CUDALINK_DOORBELL above -- + previously the TD launcher (StreamDiffusionExt.Startstream) set this in its own subprocess + env on every stream start, duplicated across three launch-path variants (windowless Popen, + PowerShell, cmd batch). It's likely a no-op on current torch (the v8 API is default-on), but + is kept as belt-and-suspenders now that it lives in one place instead of three. + SDTD_BASE_FOLDER_PATH -> this install's base_folder (StreamDiffusion repo root): Same cross-process problem as CUDALINK_DOORBELL -- TD's Python process needs a reliable anchor to the repo root for the inference-side error-report dump @@ -378,6 +386,20 @@ def phase4c_cuda_link_env(self): else: print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") + # TORCH_CUDNN_V8_API_ENABLED=1: single declaration site for this perf flag, replacing the + # three duplicated per-launch-path `env`/`set` assignments previously in + # StreamDiffusionExt.Startstream. Independent of the blocks above, so it runs even if + # either warned. + try: + cudnn_result = subprocess.run(["setx", "TORCH_CUDNN_V8_API_ENABLED", "1"], capture_output=True, text=True) + except OSError as setx_exc: + print(f" WARNING: setx failed to persist TORCH_CUDNN_V8_API_ENABLED: {setx_exc}") + else: + if cudnn_result.returncode != 0: + print(f" WARNING: setx failed to persist TORCH_CUDNN_V8_API_ENABLED: {cudnn_result.stderr.strip()}") + else: + print(" TORCH_CUDNN_V8_API_ENABLED=1 persisted.") + # SDTD_BASE_FOLDER_PATH -> repo root, so TD's Python process can locate error_reports/ # without a manual env-var step. Independent of the blocks above, so it runs even if # either warned. From 36844262a49aefcd7de26a6d674ae9d4aaa8b15f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jul 2026 08:41:44 -0400 Subject: [PATCH 17/21] feat: verify cuda-link + phase4c env vars + gap deps; add TensorRT step verification The main [8/8] verifier checked 13 deps but missed several that the installer actually installs: cuda-link (phase4b), its phase4c env vars (CUDALINK_LIB_PATH, CUDALINK_DOORBELL, TORCH_CUDNN_V8_API_ENABLED, SDTD_BASE_FOLDER_PATH), cuda-python, opencv-python, python-osc, and insightface. A broken cuda-link install or a failed setx would previously pass silently. - verifier.py: add 6 VERIFICATION_CHECKS entries (env-var check reads HKCU via winreg since setx doesn't update the current process's os.environ); cuda-link/ insightface self-SKIP off cp311/Windows so they never false-fail elsewhere. Also extend diagnose() version_checks and KNOWN_ERRORS. - tensorrt.py: add verify() covering tensorrt/polygraphy/onnx-graphsurgeon/triton (import) and cudnn/modelopt/cupy/pywin32 (distribution presence), gated by CUDA major version and platform. install() now calls verify() at its tail and returns the verification result instead of an unconditional True, so the separate "Install TensorRT" UI-button step is finally checked (previously it just printed a success message with no verification at all). Verified: extended `sd_installer verify` against the known-good SDTD_040_beta venv (RTX 4090, cu128) - 19/19 checks pass, including the new cuda-link and env-var checks. --- sd_installer/tensorrt.py | 72 ++++++++++++++++++++++++++++++++++++++-- sd_installer/verifier.py | 57 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index 8e885b6..ffd0af4 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -34,6 +34,70 @@ def version(package_name: str) -> Optional[str]: return None +def verify(cu: Optional[str] = None) -> bool: + """ + Verify the TensorRT install by importing/checking every package installed + by install(). Unlike the main sd_installer verifier ([8/8] VERIFICATION_CHECKS + in verifier.py), this covers the TensorRT step specifically, since TensorRT is + a separate, optional UI-button install and previously had no verification at all. + + Args: + cu: CUDA version string used for the install (e.g. "12.8"). Auto-detected if None. + + Returns: + True if all applicable checks passed, False otherwise. + """ + if cu is None: + cu = get_cuda_version_from_torch() + cuda_major = cu.split(".")[0] if cu else "12" + + print() + print("TensorRT Verification") + print("=" * 40) + + checks = [] # (name, passed, detail) + + def try_import(mod_name: str, label: Optional[str] = None): + label = label or mod_name + try: + mod = __import__(mod_name) + checks.append((label, True, getattr(mod, "__version__", "OK"))) + except Exception as e: + checks.append((label, False, str(e))) + + def check_dist(dist_name: str): + v = version(dist_name) + checks.append((dist_name, v is not None, v or "not installed")) + + # Functional checks - these actually load the native libs. + try_import("tensorrt") + try_import("polygraphy") + try_import("onnx_graphsurgeon", "onnx-graphsurgeon") + + # Distribution-presence checks - avoid heavy/CUDA-init imports for these. + if cuda_major == "12": + check_dist("nvidia-cudnn-cu12") + check_dist("nvidia-modelopt") + check_dist("cupy-cuda12x") + elif cuda_major == "11": + check_dist("nvidia-cudnn-cu11") + + if platform.system() == "Windows": + check_dist("pywin32") + try_import("triton") + + passed = sum(1 for _, ok, _ in checks if ok) + failed = len(checks) - passed + + for name, ok, detail in checks: + print(f"{' OK' if ok else 'FAIL'}: {name}: {detail}") + + print() + print(f"Results: {passed} passed, {failed} failed") + + return failed == 0 + + def get_cuda_version_from_torch() -> Optional[str]: """Get CUDA version from installed PyTorch""" try: @@ -164,8 +228,12 @@ def install(cu: Optional[str] = None): print("Installing triton-windows...") run_pip("install triton-windows==3.4.0.post21 --no-cache-dir") - print("TensorRT installation completed successfully!") - return True + ok = verify(cu) + if ok: + print("TensorRT installation completed successfully!") + else: + print("TensorRT installation completed, but verification found issues (see above).") + return ok if __name__ == "__main__": diff --git a/sd_installer/verifier.py b/sd_installer/verifier.py index fa23d60..4073c86 100644 --- a/sd_installer/verifier.py +++ b/sd_installer/verifier.py @@ -58,6 +58,54 @@ class VerificationResult: "import onnx; v = onnx.__version__; parts = [int(x) for x in v.split('.')[:2]]; assert parts[0] == 1 and parts[1] < 20, f'onnx {v} (>=1.20 removes float32_to_bfloat16)'; print(v)", "onnx (<1.20 required for TRT)", ), + ( + "cuda-python (bindings)", + "import cuda.bindings; print('OK')", + "cuda-python (CUDA bindings, phase2)", + ), + ("opencv (cv2)", "import cv2; print(cv2.__version__)", "opencv-python (phase6)"), + ("python-osc", "import pythonosc; print('OK')", "python-osc (TD OSC comms, phase5)"), + ( + "cuda-link", + "import sys\n" + "if not (sys.platform == 'win32' and sys.version_info[:2] == (3, 11)):\n" + " print('SKIP (cp311 Windows wheel only)')\n" + "else:\n" + " import cuda_link\n" + " print(getattr(cuda_link, '__version__', 'OK'))\n", + "cuda-link (CUDA-IPC zero-copy transport, phase4b)", + ), + ( + "insightface", + "import sys\n" + "if sys.platform != 'win32':\n" + " print('SKIP')\n" + "else:\n" + " import insightface\n" + " print(insightface.__version__)\n", + "insightface (IPAdapter face, phase3b)", + ), + ( + "cuda-link environment variables", + "import sys\n" + "if sys.platform != 'win32':\n" + " print('SKIP')\n" + "else:\n" + " import os, winreg\n" + " key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment')\n" + " def get(name):\n" + " try:\n" + " return winreg.QueryValueEx(key, name)[0]\n" + " except FileNotFoundError:\n" + " return None\n" + " lib_path = get('CUDALINK_LIB_PATH')\n" + " assert lib_path and os.path.isdir(lib_path), f'CUDALINK_LIB_PATH missing or not a dir: {lib_path}'\n" + " assert get('CUDALINK_DOORBELL') == '1', 'CUDALINK_DOORBELL != 1'\n" + " assert get('TORCH_CUDNN_V8_API_ENABLED') == '1', 'TORCH_CUDNN_V8_API_ENABLED != 1'\n" + " assert get('SDTD_BASE_FOLDER_PATH'), 'SDTD_BASE_FOLDER_PATH missing'\n" + " print('OK')\n", + "cuda-link environment variables (phase4c setx)", + ), ] @@ -224,6 +272,11 @@ def diagnose(self) -> dict: ("peft", "import peft; print(peft.__version__)"), ("protobuf", "import google.protobuf; print(google.protobuf.__version__)"), ("tensorrt", "import tensorrt; print(tensorrt.__version__)"), + ("cuda_link", "import cuda_link; print(getattr(cuda_link, '__version__', 'OK'))"), + ("cuda-python", "import cuda.bindings; print('OK')"), + ("insightface", "import insightface; print(insightface.__version__)"), + ("python-osc", "import pythonosc; print('OK')"), + ("opencv-python", "import cv2; print(cv2.__version__)"), ] for pkg, code in version_checks: @@ -286,6 +339,10 @@ def diagnose(self) -> dict: "cause": "protobuf version too new (6.x) - breaks ONNX model serialization during TensorRT engine build", "fix": "pip install protobuf==4.25.3 --force-reinstall", }, + "No module named 'cuda_link'": { + "cause": "cuda-link wheel not installed (phase4b) - only ships a cp311 Windows wheel", + "fix": "Re-run install; on cp311/Windows this installs the forkni cuda_link wheel via --no-deps", + }, } From 1dd05e42e398088d0195479161faacd28b47f94b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 15 Jul 2026 16:46:13 -0400 Subject: [PATCH 18/21] chore: retire inert TORCH_CUDNN_V8_API_ENABLED setx + verifier assert No-op on torch 2.8 (cuDNN v8 API is default-on; only the inverse TORCH_CUDNN_V8_API_DISABLED opt-out is read). Traced while debugging why SDTD_L2_PERSIST appeared set unrequested -- both vars came from the same forced per-launch perf-env bundle in StreamDiffusionExt.Startstream. L2 was already fixed via the config-driven l2_persist override (a520fe6); this retires the last inert member: the phase4c setx and its matching verifier assertion. CUDALINK_LIB_PATH/DOORBELL/SDTD_BASE_FOLDER_PATH setx + asserts are untouched. --- sd_installer/installer.py | 26 ++------------------------ sd_installer/verifier.py | 1 - 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/sd_installer/installer.py b/sd_installer/installer.py index 41e5388..043412b 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -304,8 +304,8 @@ def phase4b_cuda_link(self): print(" CUDA-IPC zero-copy export will fall back to the mirror-DAT transport") def phase4c_cuda_link_env(self): - """Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, TORCH_CUDNN_V8_API_ENABLED, - and SDTD_BASE_FOLDER_PATH (Windows only). + """Phase 4c: Persist CUDALINK_LIB_PATH, CUDALINK_DOORBELL, and SDTD_BASE_FOLDER_PATH + (Windows only). CUDALINK_LIB_PATH -> this venv's site-packages: TouchDesigner's CUDALinkBootstrap.py reads CUDALINK_LIB_PATH at Text DAT import time to @@ -324,14 +324,6 @@ def phase4c_cuda_link_env(self): must be persisted here instead. CUDALINK_WAIT_BACKEND is deliberately left unset -- its default "auto" already selects the native path. - TORCH_CUDNN_V8_API_ENABLED=1: - Opts torch into the cuDNN v8 API. Persisted here (rather than set per-launch by the TD - component) so there's a single declaration site, matching CUDALINK_DOORBELL above -- - previously the TD launcher (StreamDiffusionExt.Startstream) set this in its own subprocess - env on every stream start, duplicated across three launch-path variants (windowless Popen, - PowerShell, cmd batch). It's likely a no-op on current torch (the v8 API is default-on), but - is kept as belt-and-suspenders now that it lives in one place instead of three. - SDTD_BASE_FOLDER_PATH -> this install's base_folder (StreamDiffusion repo root): Same cross-process problem as CUDALINK_DOORBELL -- TD's Python process needs a reliable anchor to the repo root for the inference-side error-report dump @@ -386,20 +378,6 @@ def phase4c_cuda_link_env(self): else: print(" CUDALINK_DOORBELL=1 persisted (enables doorbell/native-wait IPC fast path).") - # TORCH_CUDNN_V8_API_ENABLED=1: single declaration site for this perf flag, replacing the - # three duplicated per-launch-path `env`/`set` assignments previously in - # StreamDiffusionExt.Startstream. Independent of the blocks above, so it runs even if - # either warned. - try: - cudnn_result = subprocess.run(["setx", "TORCH_CUDNN_V8_API_ENABLED", "1"], capture_output=True, text=True) - except OSError as setx_exc: - print(f" WARNING: setx failed to persist TORCH_CUDNN_V8_API_ENABLED: {setx_exc}") - else: - if cudnn_result.returncode != 0: - print(f" WARNING: setx failed to persist TORCH_CUDNN_V8_API_ENABLED: {cudnn_result.stderr.strip()}") - else: - print(" TORCH_CUDNN_V8_API_ENABLED=1 persisted.") - # SDTD_BASE_FOLDER_PATH -> repo root, so TD's Python process can locate error_reports/ # without a manual env-var step. Independent of the blocks above, so it runs even if # either warned. diff --git a/sd_installer/verifier.py b/sd_installer/verifier.py index 4073c86..e65c001 100644 --- a/sd_installer/verifier.py +++ b/sd_installer/verifier.py @@ -101,7 +101,6 @@ class VerificationResult: " lib_path = get('CUDALINK_LIB_PATH')\n" " assert lib_path and os.path.isdir(lib_path), f'CUDALINK_LIB_PATH missing or not a dir: {lib_path}'\n" " assert get('CUDALINK_DOORBELL') == '1', 'CUDALINK_DOORBELL != 1'\n" - " assert get('TORCH_CUDNN_V8_API_ENABLED') == '1', 'TORCH_CUDNN_V8_API_ENABLED != 1'\n" " assert get('SDTD_BASE_FOLDER_PATH'), 'SDTD_BASE_FOLDER_PATH missing'\n" " print('OK')\n", "cuda-link environment variables (phase4c setx)", From 17445bdecb0fbd6c4434f5c393b16b65d76f8efb Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 14:08:01 -0400 Subject: [PATCH 19/21] fix: self-heal empty tensorrt_cu12 wrapper + retry flaky pip installs tensorrt_cu12 ships as an sdist that builds the top-level tensorrt/ wrapper package. On at least one install, the wrapper ended up in a metadata-only state: dist-info present, tensorrt/ package dir missing. `import tensorrt` then failed with ModuleNotFoundError even though tensorrt_cu12_bindings and tensorrt_cu12_libs were both intact, and re-running install() couldn't self-repair: plain `pip install tensorrt_cu12==...` reports "already satisfied" and does nothing, so the broken wrapper was stuck; is_installed("tensorrt") also returned False, skipping the old-version-uninstall path; verify() detected the failure but only printed a warning. Add ensure_wrapper(), called right after each TensorRT install branch: probe `import ` in a fresh subprocess, and if it fails, repair with `pip install --force-reinstall --no-deps` (rebuilds the wrapper from sdist without touching the already-good bindings/libs wheels) -- the exact fix that was applied manually to recover a broken install. Also add a couple of retries to run_pip() to ride out a flaky pypi.nvidia.com resolve, and invalidate_caches() before verify() so the in-process import sees a wrapper ensure_wrapper() just rebuilt on disk. Verified against the real module: simulated the empty-wrapper state with a throwaway package (present -> broken -> ensure_wrapper repairs -> present), confirmed no-op on an already-healthy import, and confirmed run_pip retries transient failures but still raises once retries are exhausted. --- sd_installer/tensorrt.py | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index ffd0af4..c9ae98d 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -4,15 +4,25 @@ Standalone module that doesn't rely on streamdiffusion package imports. """ +import importlib import platform import subprocess import sys +import time from typing import Optional -def run_pip(command: str): - """Run pip command with proper error handling""" - return subprocess.check_call([sys.executable, "-m", "pip"] + command.split()) +def run_pip(command: str, retries: int = 2): + """Run pip command with proper error handling; retry a couple times for flaky indexes.""" + args = [sys.executable, "-m", "pip"] + command.split() + for attempt in range(retries + 1): + try: + return subprocess.check_call(args) + except subprocess.CalledProcessError: + if attempt >= retries: + raise + print(f" pip failed (attempt {attempt + 1}/{retries + 1}), retrying: {command}") + time.sleep(3) def is_installed(package_name: str) -> bool: @@ -34,6 +44,29 @@ def version(package_name: str) -> Optional[str]: return None +def _import_ok(module: str) -> bool: + """True if `module` imports in a fresh interpreter. A subprocess avoids this + process's stale import caches after a pip install ran in another subprocess.""" + return ( + subprocess.run([sys.executable, "-c", f"import {module}"], capture_output=True).returncode + == 0 + ) + + +def ensure_wrapper(module: str, spec: str, index_url: str): + """Repair the empty-wrapper state: pip reports the dist already satisfied + (dist-info present) but the top-level package dir is missing, so `import + ` fails. --force-reinstall rebuilds the wrapper from sdist; --no-deps + leaves the intact bindings/libs wheels untouched. + """ + if _import_ok(module): + return + print(f"'{module}' import failed after install; repairing wrapper package...") + run_pip(f"install --force-reinstall --no-deps --no-cache-dir --extra-index-url {index_url} {spec}") + if not _import_ok(module): + print(f"WARNING: '{module}' still not importable after repair.") + + def verify(cu: Optional[str] = None) -> bool: """ Verify the TensorRT install by importing/checking every package installed @@ -168,6 +201,7 @@ def install(cu: Optional[str] = None): trt_version = "10.16.1.11" print(f"Installing TensorRT {trt_version} for CUDA {cu}...") run_pip(f"install --extra-index-url https://pypi.nvidia.com tensorrt_cu12=={trt_version} --no-cache-dir") + ensure_wrapper("tensorrt", f"tensorrt_cu12=={trt_version}", "https://pypi.nvidia.com") elif cuda_major == "12": print("Installing TensorRT for CUDA 12.x...") @@ -183,6 +217,7 @@ def install(cu: Optional[str] = None): trt_version = "10.16.1.11" print(f"Installing TensorRT {trt_version} for CUDA {cu}...") run_pip(f"install --extra-index-url https://pypi.nvidia.com tensorrt_cu12=={trt_version} --no-cache-dir") + ensure_wrapper("tensorrt", f"tensorrt_cu12=={trt_version}", "https://pypi.nvidia.com") elif cuda_major == "11": print("Installing TensorRT for CUDA 11.x...") @@ -196,6 +231,7 @@ def install(cu: Optional[str] = None): tensorrt_version = "tensorrt==9.0.1.post11.dev4" print(f"Installing TensorRT for CUDA {cu}: {tensorrt_version}") run_pip(f"install --extra-index-url https://pypi.nvidia.com {tensorrt_version} --no-cache-dir") + ensure_wrapper("tensorrt", tensorrt_version, "https://pypi.nvidia.com") else: print(f"Unsupported CUDA version: {cu}") print("Supported versions: CUDA 11.x, 12.x, 12.8+") @@ -228,6 +264,9 @@ def install(cu: Optional[str] = None): print("Installing triton-windows...") run_pip("install triton-windows==3.4.0.post21 --no-cache-dir") + # verify() runs in-process; without this it can still see the stale (pre-repair) + # import cache for a module that ensure_wrapper() just rebuilt on disk. + importlib.invalidate_caches() ok = verify(cu) if ok: print("TensorRT installation completed successfully!") From e1a9c921cd842d54c4edde246d059baab196fbfe Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 14:24:30 -0400 Subject: [PATCH 20/21] fix: address CI review findings on tensorrt self-heal - ensure_wrapper() no longer lets a failed repair pip call raise past install(): cli.py has no try/except around install(), so an exhausted retry would abort before the remaining install steps and verify(), worse than the pre-PR "FAIL: tensorrt" report this PR was fixing. - _import_ok() passes the module name as an argv value instead of interpolating it into `python -c`, closing the injection-shaped f-string Copilot flagged (no live risk today, but the helper is reusable). - Drop tensorrt/polygraphy/onnx_graphsurgeon from sys.modules before verify(), since invalidate_caches() alone doesn't clear already in-process module objects that is_installed() may have imported. Co-Authored-By: Claude Sonnet 5 --- sd_installer/tensorrt.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index c9ae98d..d05ba25 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -46,9 +46,14 @@ def version(package_name: str) -> Optional[str]: def _import_ok(module: str) -> bool: """True if `module` imports in a fresh interpreter. A subprocess avoids this - process's stale import caches after a pip install ran in another subprocess.""" + process's stale import caches after a pip install ran in another subprocess. + The module name is passed as an argv value (not interpolated into the -c + string) so it can't be abused to inject arbitrary code.""" return ( - subprocess.run([sys.executable, "-c", f"import {module}"], capture_output=True).returncode + subprocess.run( + [sys.executable, "-c", "import importlib, sys; importlib.import_module(sys.argv[1])", module], + capture_output=True, + ).returncode == 0 ) @@ -57,12 +62,19 @@ def ensure_wrapper(module: str, spec: str, index_url: str): """Repair the empty-wrapper state: pip reports the dist already satisfied (dist-info present) but the top-level package dir is missing, so `import ` fails. --force-reinstall rebuilds the wrapper from sdist; --no-deps - leaves the intact bindings/libs wheels untouched. + leaves the intact bindings/libs wheels untouched. Best-effort: if the repair + pip call itself fails (e.g. retries exhausted on a flaky index), warn and + return rather than raising, so install() still reaches the remaining steps + and verify() instead of aborting with an unhandled traceback. """ if _import_ok(module): return print(f"'{module}' import failed after install; repairing wrapper package...") - run_pip(f"install --force-reinstall --no-deps --no-cache-dir --extra-index-url {index_url} {spec}") + try: + run_pip(f"install --force-reinstall --no-deps --no-cache-dir --extra-index-url {index_url} {spec}") + except subprocess.CalledProcessError: + print(f"WARNING: repair install for '{module}' failed; continuing without it.") + return if not _import_ok(module): print(f"WARNING: '{module}' still not importable after repair.") @@ -264,8 +276,11 @@ def install(cu: Optional[str] = None): print("Installing triton-windows...") run_pip("install triton-windows==3.4.0.post21 --no-cache-dir") - # verify() runs in-process; without this it can still see the stale (pre-repair) - # import cache for a module that ensure_wrapper() just rebuilt on disk. + # verify() runs in-process; drop any modules install() may have already imported + # (via is_installed()) and invalidate the finder caches, so verify() picks up a + # module ensure_wrapper() just rebuilt on disk rather than a stale sys.modules entry. + for _m in ("tensorrt", "polygraphy", "onnx_graphsurgeon"): + sys.modules.pop(_m, None) importlib.invalidate_caches() ok = verify(cu) if ok: From 2edf5c5e015f144d40b6e35cdf4341a3c054d15a Mon Sep 17 00:00:00 2001 From: Alex Korneev <49823108+forkni@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:53:46 -0400 Subject: [PATCH 21/21] Fix onnxruntime-gpu churn in TensorRT FP8 install step (#4) nvidia-modelopt[onnx]==0.43.0 hard-pins onnxruntime-gpu==1.22.0 on Windows, forcing a downgrade from the setup.py-authoritative 1.24.4 followed immediately by a --no-deps re-pin back to 1.24.4 (~420 MB of wasted download on every fresh TensorRT install). Drop the [onnx] extra and enumerate its (deterministic, version-pinned) deps explicitly with our own onnx/onnxruntime-gpu pins, so 1.22.0 never enters the resolve. Verified via pip --dry-run: all packages "already satisfied" at existing pins, zero onnxruntime-gpu churn. Co-authored-by: Claude Opus 5 --- sd_installer/tensorrt.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index d05ba25..88953c5 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -264,10 +264,23 @@ def install(cu: Optional[str] = None): # breaks FP8 quant (external-data loading -> negative QDQ scale). setup.py pins onnx==1.19.1. if cuda_major == "12": print("Installing FP8 quantization dependencies (modelopt, cupy)...") - run_pip("install nvidia-modelopt[onnx]==0.43.0 cupy-cuda12x==13.6.0 numpy==1.26.4 --no-cache-dir") - # Re-assert the setup.py-authoritative pins the modelopt resolver perturbs: it downgrades - # onnxruntime-gpu to 1.22.0 and upgrades onnx past 1.19.1. --no-deps avoids a re-solve. - run_pip("install onnx==1.19.1 onnxruntime-gpu==1.24.4 --no-deps --no-cache-dir") + # nvidia-modelopt[onnx]==0.43.0 hard-pins onnxruntime-gpu==1.22.0 on Windows (its METADATA: + # `Requires-Dist: onnxruntime-gpu==1.22.0; platform_system == "Windows" and extra == "onnx"`), + # so requesting the [onnx] extra force-downgrades our setup.py-authoritative + # onnxruntime-gpu==1.24.4 to 1.22.0 (~215 MB) only to re-install 1.24.4 (~207 MB) seconds + # later. Install modelopt WITHOUT the extra and enumerate the extra's deps explicitly, + # substituting our own onnx/onnxruntime-gpu pins — modelopt core has no onnx requirement, so + # 1.22.0 never enters the resolve. modelopt is version-pinned, so this dep list is + # deterministic; re-check it against nvidia_modelopt-.dist-info/METADATA (the + # `extra == "onnx"` Requires-Dist lines) if the modelopt pin is ever bumped. onnx-graphsurgeon + # and polygraphy (also [onnx] deps) are installed above from the NVIDIA index — already + # satisfied, intentionally not re-listed. + run_pip( + "install nvidia-modelopt==0.43.0 " + "cppimport lief ml_dtypes onnxconverter-common~=1.16.0 onnxscript onnxslim>=0.1.76 " + "onnx==1.19.1 onnxruntime-gpu==1.24.4 " + "cupy-cuda12x==13.6.0 numpy==1.26.4 --no-cache-dir" + ) if platform.system() == "Windows" and not is_installed("pywin32"): print("Installing pywin32...")