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..126e6d4 --- /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 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). + 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." diff --git a/sd_installer/cli.py b/sd_installer/cli.py index 9834f44..421bed6 100644 --- a/sd_installer/cli.py +++ b/sd_installer/cli.py @@ -9,19 +9,19 @@ 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 """ 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 +47,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 +85,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,15 +197,59 @@ 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 +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, KNOWN_ERRORS + from .verifier import Verifier try: base = Path(args.base_folder) if args.base_folder else find_base_folder() @@ -236,6 +280,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 +290,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 +314,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 +365,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) @@ -378,10 +429,19 @@ 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( - "-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..043412b 100644 --- a/sd_installer/installer.py +++ b/sd_installer/installer.py @@ -11,12 +11,10 @@ 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 = { @@ -26,6 +24,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) @@ -36,6 +38,17 @@ (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.1/cuda_link-1.12.1-cp311-cp311-win_amd64.whl", +} + # PyTorch configurations by CUDA version PYTORCH_CONFIGS = { "cu118": { @@ -63,9 +76,9 @@ "xformers": None, # Skip - causes conflicts }, "cu128": { - "torch": "2.7.0", - "torchvision": "0.22.0", - "torchaudio": "2.7.0", + "torch": "2.8.0", + "torchvision": "0.23.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,13 +116,14 @@ 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] + # 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.""" @@ -144,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 @@ -238,7 +253,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") @@ -260,6 +275,120 @@ 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.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, 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 + 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. + + 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), 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 + + 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") + else: + site_packages = result.stdout.strip() + self._report_progress(f"Persisting CUDALINK_LIB_PATH -> {site_packages}", 4, 8) + 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: + 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. + 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: + 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).") + + # 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) @@ -269,10 +398,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 +412,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, ) @@ -289,13 +420,22 @@ 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 @@ -304,6 +444,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. @@ -315,7 +480,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.4.0 Installation") print(" Daydream Fork with StreamV2V") print("=" * 50) print() @@ -326,16 +491,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.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) @@ -374,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.1 Installation +echo StreamDiffusionTD v0.4.0 Installation echo Daydream Fork with StreamV2V echo ======================================== @@ -386,7 +565,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/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 diff --git a/sd_installer/tensorrt.py b/sd_installer/tensorrt.py index d4b46e1..88953c5 100644 --- a/sd_installer/tensorrt.py +++ b/sd_installer/tensorrt.py @@ -3,21 +3,32 @@ Standalone module that doesn't rely on streamdiffusion package imports. """ + +import importlib +import platform import subprocess import sys -import platform +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: """Check if a package is installed""" try: - __import__(package_name.replace('-', '_')) + __import__(package_name.replace("-", "_")) return True except ImportError: return False @@ -27,11 +38,111 @@ 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 +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. + 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", "import importlib, sys; importlib.import_module(sys.argv[1])", 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. 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...") + 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.") + + +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: @@ -74,6 +185,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 +196,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,9 +210,10 @@ 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") + ensure_wrapper("tensorrt", f"tensorrt_cu12=={trt_version}", "https://pypi.nvidia.com") elif cuda_major == "12": print("Installing TensorRT for CUDA 12.x...") @@ -111,9 +226,10 @@ 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") + ensure_wrapper("tensorrt", f"tensorrt_cu12=={trt_version}", "https://pypi.nvidia.com") elif cuda_major == "11": print("Installing TensorRT for CUDA 11.x...") @@ -126,9 +242,8 @@ 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") + 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+") @@ -137,23 +252,55 @@ 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.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. + # 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)...") + # 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 onnx-graphsurgeon==0.5.8 --extra-index-url https://pypi.ngc.nvidia.com --no-cache-dir" + "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"): + + 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"): + 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") - print("TensorRT installation completed successfully!") - return True + # 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: + 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 2cc484c..e65c001 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,86 @@ 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)", + ), + ( + "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('SDTD_BASE_FOLDER_PATH'), 'SDTD_BASE_FOLDER_PATH missing'\n" + " print('OK')\n", + "cuda-link environment variables (phase4c setx)", ), ] @@ -178,7 +198,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 +231,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 +246,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 = [ @@ -247,6 +271,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: @@ -290,8 +319,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)", @@ -309,6 +338,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", + }, }