diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8bc65eaf..cbc46326 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,6 +26,63 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
+ ai-static:
+ name: AI plans and unit tests
+ runs-on: windows-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Parse PowerShell and run hardware-independent tests
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ Get-ChildItem ./src/Workloads, ./src/tests, ./src/tools -Recurse -Filter *.ps1 |
+ ForEach-Object {
+ $tokens = $null
+ $errors = $null
+ [void][System.Management.Automation.Language.Parser]::ParseFile(
+ $_.FullName, [ref]$tokens, [ref]$errors)
+ if ($errors) { throw "$($_.FullName): $($errors -join '; ')" }
+ }
+ foreach ($id in 'ai-common','cuda','rocm','intel-ai','foundry','pytorch','local-ai','llama.cpp','ollama') {
+ & "./src/tests/$id/unit.ps1"
+ }
+ & "./src/tests/ai-common/all-signed.ps1"
+
+ - name: Validate report schema and manifest
+ shell: pwsh
+ run: |
+ python -m pip install --disable-pip-version-check --quiet pyyaml jsonschema
+ python -c "import json,yaml; json.load(open('src/docs/ai-workload-report.schema.json', encoding='utf-8')); yaml.safe_load(open('src/manifest.yml', encoding='utf-8-sig')); print('AI data files valid')"
+
+ - name: Run AI unit tests in Windows PowerShell
+ shell: powershell
+ run: |
+ $ErrorActionPreference = 'Stop'
+ foreach ($id in 'ai-common','cuda','rocm','intel-ai','foundry','pytorch','local-ai','llama.cpp','ollama') {
+ & "./src/tests/$id/unit.ps1"
+ }
+
+ - name: Exercise non-mutating AI plans and validate reports
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ New-Item -ItemType Directory -Path ai-reports -Force | Out-Null
+ foreach ($id in 'cuda','rocm','intel-ai','foundry','pytorch','llama.cpp','ollama') {
+ & "./src/Workloads/$id/install.ps1" -PlanOnly -ReportPath "./ai-reports/$id.json"
+ }
+ & "./src/Workloads/local-ai/install.ps1" -PlanOnly -ReportRoot "./ai-reports/local-ai"
+ @'
+ import glob, json, jsonschema
+ schema = json.load(open("src/docs/ai-workload-report.schema.json", encoding="utf-8"))
+ for path in glob.glob("ai-reports/**/*.json", recursive=True):
+ with open(path, encoding="utf-8-sig") as handle:
+ jsonschema.validate(json.load(handle), schema)
+ print("OK:", path)
+ '@ | python -
+
# ---------------------------------------------------------------------------
# Job A: parse manifest.yml → matrices for per-OS flow jobs.
# ---------------------------------------------------------------------------
diff --git a/README.md b/README.md
index 75b3f18e..a93d2bcd 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
·
WSL Comfort
·
- Workloads
+ Workloads
·
Troubleshooting
@@ -30,7 +30,7 @@ Three developer setups live in this repo. Pick the one that matches what you wan
| --- | --- |
| A complete dev workstation: tools, OS settings, WSL, and terminal. One command, restarts once. | [Windows Dev Config](#%EF%B8%8F-windows-dev-config) |
| A polished WSL shell: zsh/bash, Starship, CLI tools, and a themed terminal profile. Interactive or unattended. | [WSL Comfort](#-wsl-comfort) |
-| A single language toolchain: Node, Python, SQL, PowerShell, .NET, Rust, Go, Java, PHP, WinForms, or WinUI 3. One command each. | [Workloads](#-single-language-workloads) |
+| A focused language or Windows AI toolchain. One command each. | [Workloads](#-focused-workloads) |
## 🖥️ Windows Dev Config
@@ -100,10 +100,14 @@ Full details: [`wsl-comfort/readme.md`](./wsl-comfort/readme.md).
-## 🧪 Single-language workloads
+## 🧪 Focused workloads
Just want one toolchain? Pick a row. Each workload ships a `configuration.winget` file plus a matching `install.ps1` shim that applies it and refreshes PATH in the current session.
+AI workloads instead use resumable PowerShell entry points that directly check,
+install or upgrade, refresh PATH, verify a real workload, and write a
+machine-readable report.
+
Most of the single-language workloads use [`winget configure`](https://learn.microsoft.com/en-us/windows/package-manager/winget/configure). If you've never used it before, enable it once:
```powershell
@@ -139,6 +143,14 @@ If that fails or `winget configure` is still not recognized, see [Troubleshootin
| WinForms | .NET SDK 10 + Windows Forms desktop workload | `winget configure -f .\Workloads\winforms\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| WinAppCLI | Developer Mode + .NET SDK 10 + Windows App Development CLI | `winget configure -f .\Workloads\winappcli\configuration.winget --accept-configuration-agreements --disable-interactivity` |
| WinUI 3 | .NET SDK 10 + Visual Studio Community + Windows App SDK / WinUI 3 + WinAppCLI | `winget configure -f .\Workloads\winui\configuration.winget --accept-configuration-agreements --disable-interactivity` |
+| NVIDIA CUDA | CUDA Toolkit + MSVC; compiles and executes a minimal GPU kernel | `.\Workloads\cuda\install.ps1` |
+| AMD ROCm / HIP | ROCm Core SDK 10.0 for supported AMD GPUs; compiles and executes a HIP kernel | `.\Workloads\rocm\install.ps1` |
+| Intel AI | OpenVINO device inference; optional oneAPI/SYCL GPU toolkit and kernel | `.\Workloads\intel-ai\install.ps1` |
+| Foundry Local | Architecture-native WinML package + Qwen3-0.6B model inference | `.\Workloads\foundry\install.ps1` |
+| PyTorch | CPython 3.13 + contained CPU/CUDA/ROCm/XPU environment; vendor-appropriate Triton where supported | `.\Workloads\pytorch\install.ps1` |
+| Local AI development | Hardware inventory + contained PyTorch/Triton + optional one local-model runtime | `.\Workloads\local-ai\install.ps1` |
+| llama.cpp | Hardware-selected official rolling CUDA/ROCm/SYCL/OpenVINO/Vulkan/OpenCL/CPU runtime + pinned GGUF inference | `.\Workloads\llama.cpp\install.ps1` |
+| Ollama | Installed x64 application or Dev Config-managed native ARM64 archive + official qwen3:0.6b inference | `.\Workloads\ollama\install.ps1` |
Want the PATH refresh in your current shell? Use the matching shim instead of calling `winget configure` directly:
@@ -148,6 +160,507 @@ Want the PATH refresh in your current shell? Use the matching shim instead of ca
> **Heads up:** WinForms and WinUI 3 pull down several gigabytes of Visual Studio components. Fine on a real workstation, painful on a small VM.
+### Windows AI workload support
+
+The primary deliverable is a runnable **local AI development scenario**, not a
+replacement for PyPI/Conda and not an instruction to install every vendor SDK
+or model runtime. The supported product-level dispatcher uses the same protected
+download/signature/elevation architecture as Windows Dev Config:
+
+```powershell
+$url = 'https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/main/src/windows-dev-config/bootstrap.ps1'
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai
+```
+
+For this PR before its signed release copy exists:
+
+```powershell
+# Follow the unsigned-development instructions below first: temporarily set
+# the test user's CurrentUser policy to Bypass and restore it afterward.
+$prHead = gh pr view 104 --repo microsoft/WindowsDeveloperConfig `
+ --json headRefOid --jq .headRefOid
+$prUrl = "https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/$prHead/src/windows-dev-config/bootstrap.ps1"
+& ([scriptblock]::Create((irm $prUrl))) `
+ -Ref $prHead -Scenario local-ai -AllowUnsigned
+```
+
+The dispatcher downloads the full multi-file workload tree, verifies signed
+PowerShell files plus the signed hash manifest for catalog/Python/C++/CUDA
+content, copies them with shared helper steps to a protected scenario directory,
+reverifies after copy, elevates the apply run, and launches only `local-ai`—not
+the full Calm OS workstation setup. Explicit `-AllowUnsigned` branch tests use
+the isolated `%ProgramData%\CalmOS-Development` root so they cannot contaminate
+the production signed `%ProgramData%\CalmOS` payload.
+
+The repository-level equivalent detects hardware, installs a contained PyTorch
+backend, adds compatible Triton when published, and proves both a tensor
+operation and a minimal neural-network forward pass:
+
+```powershell
+.\Workloads\local-ai\install.ps1
+# Expected:
+# PYTORCH_SMOKE=... "model_forward_verified": true ...
+# PYTORCH_READY: backend=, ...
+# LOCAL_AI_SCENARIO_READY: backend=Auto, runtime=None, ...
+```
+
+Product-level plan and optional runtime examples:
+
+```powershell
+& ([scriptblock]::Create((irm $url))) `
+ -Scenario local-ai -PlanOnly `
+ -ReportRoot "$env:TEMP\local-ai-plan"
+
+& ([scriptblock]::Create((irm $url))) `
+ -Scenario local-ai -AiRuntime LlamaCpp
+```
+
+Choose one optional local-model runtime only when the scenario needs it:
+
+```powershell
+.\Workloads\local-ai\install.ps1 -Runtime LlamaCpp
+.\Workloads\local-ai\install.ps1 -Runtime Ollama
+.\Workloads\local-ai\install.ps1 -Runtime Foundry
+```
+
+### What each entry point installs transitively
+
+GPU drivers are prerequisites and are never replaced.
+
+| Entry point | Transitive acquisition and explicit non-acquisition |
+| --- | --- |
+| `local-ai` | Hardware inventory → PyTorch `Auto`. NVIDIA installs CUDA-enabled torch; if Triton is selected, it also ensures architecture-native MSVC and standalone CUDA Toolkit for JIT. AMD installs the exact ROCm device runtime tuple inside the PyTorch venv, **not** native ROCm SDK/`hipcc`. Intel installs the XPU tuple + `triton-xpu`, **not** full oneAPI. CPU installs only the CPU tuple. |
+| `local-ai -Runtime LlamaCpp` | The PyTorch stack above + one backend-specific llama.cpp runtime + pinned quick validation GGUF. NVIDIA llama assets carry paired `cudart`; llama.cpp itself does not require the full standalone CUDA Toolkit. |
+| `local-ai -Runtime Ollama` | The PyTorch stack above + Ollama. Ollama selects/manages its own inference backend and model runtime; the report records actual CPU/GPU allocation. |
+| `local-ai -Runtime Foundry` | The PyTorch stack above + Foundry Local. Foundry controls EP discovery/acquisition; the report records the actual EP/device or truthful CPU fallback. |
+| `pytorch` | Exact backend behavior from the first row. CUDA/MSVC are conditional on supported Triton JIT, not ordinary tensor/model inference. ROCm and XPU runtimes remain contained in the venv. |
+| `cuda` | Native CUDA Toolkit + architecture-native MSVC; compiles/runs a CUDA kernel. Does not install PyTorch or a model runtime. |
+| `rocm` | Native ROCm Core SDK/`hipcc` in a contained environment; compiles/runs a HIP kernel. Does not install PyTorch or a model runtime. |
+| `intel-ai` | OpenVINO CPU/GPU/NPU inference; `SYCL`/`Full` conditionally adds full oneAPI for native GPU development. Does not install PyTorch XPU. |
+| `llama.cpp` | One selected backend runtime, paired runtime assets where required, and the quick validation model. Does not install PyTorch; NVIDIA assets include their required `cudart`. |
+| `ollama` | Ollama + verified quick model. Backend selection is source-managed and reported, not forced by Dev Config. |
+| `foundry` | Foundry Local + quick catalog model. EP acquisition/selection is source-managed and reported, not forced by Dev Config. |
+
+The standalone vendor flows remain available for native kernel/toolchain work.
+The AI flows are independent and install only the selected hardware stack.
+CPU architecture and GPU vendor are separate axes: Windows ARM64 can have an
+NVIDIA GPU (RTX Spark), while AMD and Intel native Windows toolkits currently
+publish x64 artifacts only. There is no generic "ARM GPU" toolkit. Foundry
+Local/Windows ML is the cross-vendor layer for DirectML and dynamically acquired
+NVIDIA, AMD, Intel, and Qualcomm execution providers.
+
+| Workload | Windows x64 | Windows ARM64 | Prerequisites and selected path |
+| --- | --- | --- | --- |
+| Local AI development | PyTorch Auto + optional selected runtime | PyTorch Auto + optional selected runtime | Recommended scenario entry point. Does not universally install CUDA, ROCm, oneAPI, Foundry, llama.cpp, and Ollama. |
+| CUDA | WinGet CUDA 13 stable; GPU readiness requires driver 580+ and CC7.5+ | NVIDIA CUDA 13.4 Developer Preview | NVIDIA GPU + qualified driver by default. Installs MSVC, compiles with `nvcc -arch=native`, and executes a kernel. `-ToolkitOnly` permits compiler-only setup. |
+| AMD ROCm / HIP | ROCm Core SDK 10.0 on supported Radeon/Ryzen AI GPUs | Not published | Uses AMD's stable Windows x64 feed and executes a compiled HIP kernel. Native Windows Triton is unsupported. |
+| Intel AI | OpenVINO CPU/GPU/NPU; optional oneAPI/SYCL | Not published | OpenVINO performs generated-model inference on the requested device. `-Profile Full` also executes a SYCL GPU kernel. |
+| Foundry Local | Supported | Supported | Windows 11 24H2/build 26100+. Uses WinML and does **not** require CUDA. Downloads `qwen3-0.6b` and runs a marker completion. |
+| PyTorch CPU | Stable official CPU wheel | Stable official CPU wheel | Contained CPU runtime, tensor operation, and minimal neural-model forward pass. |
+| PyTorch CUDA | Stable official CUDA wheel chosen from driver/device capability | Pinned NVIDIA CUDA 13.4 qualified interim wheel on RTX Spark | Self-contained wheel runtime; standalone `cuda` is not required for ordinary tensor/model use. Triton JIT acquires its compiler/toolchain automatically. |
+| PyTorch ROCm | AMD stable Windows x64 feed with exact `device-` runtime tuple | Unsupported/unpublished | Self-contained AMD runtime tuple inside the PyTorch venv; does not require the standalone `rocm` SDK flow. |
+| PyTorch XPU | Official PyTorch XPU index with `torch`, `torchvision`, and `triton-xpu` | Unsupported/unpublished | Self-contained Intel XPU runtime tuple; does not install full oneAPI. |
+| Triton Windows CUDA | Community `triton-windows` on qualified NVIDIA CUDA stacks | NVIDIA CUDA 13.4 preview stack | Executes a real vector-add GPU kernel. |
+| Triton XPU / `torch.compile` | Official `triton-xpu` through the PyTorch XPU index | Unsupported/unpublished | Executes a cold `torch.compile` workload on the Intel GPU. |
+| llama.cpp CUDA x64 | Official rolling CUDA 13.3 or 12.4 app + paired cudart assets | Unsupported | Auto selects the newest compatible CUDA runtime from driver and compute capability, then proves NVIDIA device offload and GPU layers. |
+| llama.cpp CUDA ARM64 | Unsupported | Qualified CUDA 13.4 Developer Preview app + paired cudart assets | Retains the N1X path and requires RTX Spark-class hardware, driver 616+, backend/device evidence, GPU layers, and real inference. |
+| llama.cpp ROCm x64 | Official rolling ROCm 10.0 asset | Unsupported | Requires an AMD GPU in the Windows ROCm matrix and proves ROCm/AMD offload. |
+| llama.cpp SYCL / OpenVINO x64 | Official rolling SYCL and OpenVINO 2026.3.1 assets | Unsupported | Auto prefers SYCL for a supported Intel GPU because it directly proves Intel GPU execution. OpenVINO is explicit/general x64 inference; no NPU claim is made. |
+| llama.cpp OpenCL Adreno ARM64 | Unsupported | Policy-approved, physically qualified `b10917` Qualcomm Adreno OpenCL asset | Requires a detected Qualcomm/Adreno GPU plus the Windows OpenCL loader and proves OpenCL/Adreno offload. |
+| llama.cpp Vulkan x64 fallback | Official rolling Vulkan asset | Unsupported | Used by Auto only after no supported vendor-native backend is available and a Vulkan loader/device exists. Reports Vulkan explicitly. |
+| llama.cpp CPU fallback | Official rolling CPU asset | Official rolling CPU asset | Used when no qualified accelerator exists or explicitly requested; benchmark must show no GPU layers. |
+| Ollama | Official WinGet installer | Official native ARM64 archive installed as a managed per-user application | ARM64 installs atomically under `%LOCALAPPDATA%\Programs\Ollama`, registers user startup/PATH, verifies native architecture/model/backend, and supports model-preserving uninstall. |
+
+### Exact quick-run and coding-demo commands
+
+The default model checks are intentionally small enough for setup validation:
+
+```powershell
+.\Workloads\llama.cpp\install.ps1
+# Expected: LLAMA_CPP_READY: ... and INSTALL_OK: llama.cpp
+
+.\Workloads\ollama\install.ps1
+# Expected: OLLAMA_READY: ... and INSTALL_OK: ollama
+
+.\Workloads\foundry\install.ps1
+# Expected: FOUNDRY_READY: ... provider= and INSTALL_OK: foundry
+```
+
+On x64, Ollama remains a normally registered WinGet application. On ARM64,
+Dev Config verifies the official native `ollama-windows-arm64.zip`, installs it
+atomically under `%LOCALAPPDATA%\Programs\Ollama`, prepends that directory to
+the user PATH, and registers `ollama serve` in the current user's Run key.
+The `.devconfig-install.json` marker records the release tag, asset digest,
+installed files, architecture, and source. Upgrade stops only processes whose
+executable is inside that managed directory. To remove the managed ARM64
+application while retaining downloaded models:
+
+```powershell
+.\Workloads\ollama\install.ps1 -Uninstall
+# Add -RemoveModels only when model data should also be deleted.
+```
+
+For a more useful coding demonstration, after the llama.cpp flow succeeds:
+
+```powershell
+.\Workloads\llama.cpp\coding-demo.ps1 `
+ -ReportPath "$env:TEMP\llama-coding-demo.json"
+# Downloads the optional ~1.04 GB Qwen2.5-Coder-1.5B-Instruct Q4_K_M model.
+# Expected: generated Python `def group_anagrams(...)` and CODING_DEMO_READY.
+```
+
+The coding model is opt-in and does not enlarge the default setup. Its immutable
+Qwen revision, exact 1,117,320,768-byte size, SHA-256, and Apache-2.0 license
+are verified before execution.
+
+### Advanced component-level usage
+
+Run an individual flow from PowerShell:
+
+```powershell
+.\Workloads\cuda\install.ps1
+.\Workloads\rocm\install.ps1
+.\Workloads\intel-ai\install.ps1
+.\Workloads\foundry\install.ps1
+.\Workloads\pytorch\install.ps1
+.\Workloads\llama.cpp\install.ps1
+.\Workloads\ollama\install.ps1
+```
+
+PyTorch accepts explicit backend and Triton policy switches:
+
+```powershell
+.\Workloads\pytorch\install.ps1 -Backend CPU
+.\Workloads\pytorch\install.ps1 -Backend CUDA -RequireTriton
+.\Workloads\pytorch\install.ps1 -Backend ROCm
+.\Workloads\pytorch\install.ps1 -Backend XPU -RequireTriton
+.\Workloads\llama.cpp\install.ps1 -Backend CUDA
+.\Workloads\llama.cpp\install.ps1 -Backend ROCm
+.\Workloads\llama.cpp\install.ps1 -Backend SYCL
+.\Workloads\llama.cpp\install.ps1 -Backend OpenVINO
+```
+
+`-Backend Auto` uses the deterministic priority **supported NVIDIA CUDA →
+supported AMD ROCm → supported Intel XPU → CPU**. Explicit `ROCm` or `XPU`
+can select a supported secondary adapter on mixed-GPU systems.
+
+### Vendor setup layers
+
+| Vendor | Native developer flow | PyTorch flow |
+| --- | --- | --- |
+| NVIDIA | `cuda` installs the CUDA compiler/toolkit and proves a native kernel. | `pytorch -Backend CUDA` installs its own wheel runtime. Standalone CUDA is not universally required; compatible Triton JIT/toolchain dependencies are acquired automatically. |
+| AMD | `rocm` installs the ROCm Core SDK/HIP compiler and proves a native HIP kernel. | `pytorch -Backend ROCm` installs the official AMD device-specific runtime package tuple inside its own venv; the separate `rocm` flow is not a prerequisite for tensor inference. |
+| Intel | `intel-ai -Profile OpenVINO` is CPU/GPU/NPU inference; `-Profile SYCL` or `Full` installs full oneAPI for native SYCL development. | `pytorch -Backend XPU` installs the official XPU wheel tuple and `triton-xpu` inside its own venv; it does not install full oneAPI. |
+
+Every standalone AI workload accepts `-PlanOnly` and `-ReportPath`. The
+`local-ai` scenario accepts `-PlanOnly` and groups its child reports under
+`-ReportRoot`. Plan mode performs hardware/support resolution without installing
+software. Applied runs write JSON to
+`%LOCALAPPDATA%\DevConfig\reports\-latest.json`; the reusable hardware
+inventory command is:
+
+```powershell
+.\src\tools\collect-ai-hardware.ps1
+.\src\tools\get-ai-capabilities.ps1 -OutputPath "$env:TEMP\ai-capabilities.json"
+```
+
+Default acceptance proves each workload is usable, not merely installed:
+CUDA executes a compiled GPU kernel; PyTorch performs a tensor operation on the
+selected backend and, when supported, Triton runs a GPU kernel; and each local
+model runtime downloads a small Apache-2.0 Qwen model and performs deterministic
+text inference.
+
+llama.cpp `-Backend Auto` prefers **supported NVIDIA CUDA → supported AMD ROCm
+→ supported Intel SYCL → Qualcomm Adreno OpenCL → x64 Vulkan → CPU**. Explicit
+`OpenVINO` is available on x64 for its official general inference backend. The
+resolver takes every archive for a selection from one `bNNNNN` release, requires
+GitHub's SHA-256 digest for each asset, caches the verified archives under
+`%LOCALAPPDATA%\DevConfig\llama.cpp\asset-cache`, and atomically replaces the
+runtime. Qualcomm ARM64 is pinned to qualified release `b10917` because managed
+Defender ransomware protection blocks the unsigned `b10919` Adreno executable.
+`llama-bench -o json` must identify the selected backend/device and
+diagnostics must report an actual nonzero `offloaded X/Y layers` result for
+every accelerator path before the flow is ready; requested `-ngl` is not treated
+as proof. Physical hardware comes from the official `gpu_info` field; `devices`
+is retained only as the requested selector and must agree with explicit
+`-Device`.
+
+| Flow | Default model download | Cache |
+| --- | ---: | --- |
+| Foundry Local | `qwen3-0.6b`, about 593 MB | Reported by `foundry cache location` |
+| llama.cpp | `Qwen3-0.6B-Q4_K_M.gguf`, 396,704,416 bytes | `%LOCALAPPDATA%\DevConfig\llama.cpp\models` |
+| llama.cpp coding demo (opt-in) | `Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf`, 1,117,320,768 bytes | `%LOCALAPPDATA%\DevConfig\llama.cpp\models` |
+| Ollama | `qwen3:0.6b`, about 522 MB | `%USERPROFILE%\.ollama\models` or `OLLAMA_MODELS` |
+
+Use `-SkipModelSmoke` with Foundry Local, llama.cpp, or Ollama to opt out
+of the model download and inference. Use CUDA's `-SkipWorkloadSmoke` to opt out
+of kernel compilation/execution. Opted-out runs verify installation only and do
+not report full workload readiness.
+
+For physical partner validation, run the assigned llama.cpp backend from an
+elevated PowerShell. Run the plan first, then the full flow twice without skip
+switches; retain every report and console log.
+
+```powershell
+$ReportRoot = Join-Path $env:TEMP "devconfig-ai-$env:COMPUTERNAME"
+New-Item -ItemType Directory -Path $ReportRoot -Force | Out-Null
+
+.\src\Workloads\llama.cpp\install.ps1 -Backend CUDA -PlanOnly -ReportPath "$ReportRoot\llama-cuda-plan.json"
+.\src\Workloads\llama.cpp\install.ps1 -Backend ROCm -PlanOnly -ReportPath "$ReportRoot\llama-rocm-plan.json"
+.\src\Workloads\llama.cpp\install.ps1 -Backend SYCL -PlanOnly -ReportPath "$ReportRoot\llama-sycl-plan.json"
+.\src\Workloads\llama.cpp\install.ps1 -Backend OpenCL -PlanOnly -ReportPath "$ReportRoot\llama-adreno-plan.json"
+```
+
+Use the same command without `-PlanOnly` for the full and idempotence runs,
+writing distinct `*-final.json` and `*-rerun-final.json` reports. A pass requires
+`result.ready=true`, no blockers, matching `backends` and `gpu_info`, actual
+offloaded layers, and the pinned model marker.
+
+On ARM64, CUDA downloads NVIDIA's checksum- and Authenticode-verified 13.4
+Developer Preview installer (about 3.8 GB) under the NVIDIA CUDA EULA. The
+native RTX Spark PyTorch CUDA wheel is also a pinned developer preview (about
+1.85 GB). The flows clearly label both previews and never silently claim an
+ARM64 NVIDIA system is GPU-ready after falling back to CPU.
+
+PyTorch caches that verified ARM64 wheel under
+`%LOCALAPPDATA%\DevConfig\pytorch\wheel-cache`. A matching rerun validates the
+recorded plan and exact installed torch, NumPy, and Triton versions, skips all
+package downloads/installation, and still reruns the CUDA tensor and Triton
+kernel acceptance tests.
+
+**Hardware validation status:** Windows ARM64 on NVIDIA RTX Spark N1X is
+validated end-to-end for CUDA, PyTorch CUDA, Triton, Foundry Local, llama.cpp,
+and Ollama. The managed Ollama ARM64 acceptance installed native runtime
+0.34.4 under `%LOCALAPPDATA%\Programs\Ollama`, migrated the prior Dev Config
+runtime, registered PATH/startup, verified the official release digest and
+`qwen3:0.6b` model digest, performed real inference at `/api/ps` 100% GPU, and
+left the persistent endpoint ready on `127.0.0.1:11434`. An idempotent rerun
+reported `already-current`; model-preserving uninstall and reinstall also
+passed. The optional Qwen2.5-Coder-1.5B demo generated the requested
+`group_anagrams` Python implementation through llama.cpp CUDA at 101.7
+generation tokens/s. AMD ROCm/HIP, Intel OpenVINO/oneAPI/XPU, NVIDIA x64
+llama.cpp CUDA, and Qualcomm ARM64 llama.cpp OpenCL are hardware-gated and
+ready for partner execution. Their current gap is physical partner hardware
+coverage, not static planning, asset discovery, or unit coverage.
+
+### Known vendor gaps and boundaries
+
+| Vendor | GPU coverage | NPU coverage | Windows CPU architecture | Maturity / live status | Known boundaries |
+| --- | --- | --- | --- | --- | --- |
+| NVIDIA | `cuda`, PyTorch CUDA/Triton, llama.cpp CUDA | None in these vendor SDK flows; Foundry/WinML provider behavior is separate | x64 designed/partner pending; ARM64 N1X validated | x64 stable channels need live acceptance; ARM64 CUDA/PyTorch are developer previews | Foundry on N1X currently uses `CPUExecutionProvider`, not CUDA. Installers qualify but do not replace GPU drivers. |
+| AMD | Native ROCm/HIP, PyTorch ROCm, llama.cpp ROCm on AMD's exact Windows GPU/gfx matrix | Not implemented; ROCm is GPU/HIP, not Ryzen AI NPU | x64 only | Resolver/static acceptance complete; hardware pending | Native Windows AMD Triton is unavailable. Foundry/WinML AMD EP and Ollama AMD acceleration remain unvalidated and are not claimed. |
+| Intel | OpenVINO GPU, oneAPI/SYCL, PyTorch XPU/`triton-xpu`, llama.cpp SYCL/OpenVINO | OpenVINO NPU only when the requested device actually executes; no PyTorch XPU or llama SYCL NPU claim | x64 only | Resolver/static acceptance complete; hardware pending | Full oneAPI is only for native SYCL. XPU and SYCL target Intel GPUs, not NPUs. |
+| Qualcomm/Adreno | llama.cpp OpenCL ARM64; Foundry/WinML is the vendor-neutral path | Only through a validated runtime/provider such as WinML/Foundry; no standalone toolkit here | ARM64 | Resolver/static acceptance complete; hardware pending | No native PyTorch accelerator backend. Ollama ARM64 is reported as CPU/NVIDIA capability unless actual Adreno evidence becomes available. |
+| Other / fallback | Vulkan x64 compatibility fallback; CPU x64/ARM64 | None | x64/ARM64 as listed | Fallback paths | Vulkan/CPU are never labeled vendor-native. Mali and other stacks are unimplemented/unpublished without official Windows artifacts. |
+
+Auto selects a vendor/backend deterministically, and explicit backends can target
+a supported secondary vendor. Same-vendor targeting is available through
+`cuda`/`rocm` and explicit-backend PyTorch `-DeviceIndex`, llama.cpp `-Device`, OpenVINO
+`-OpenVinoDeviceId`, and SYCL `-SyclDeviceSelector`. Foundry and Ollama manage
+their own device selection; those flows report the actual provider/allocation
+rather than claiming control they do not expose. Reports retain the selected
+device and installed driver as preconditions. These flows do not update GPU
+drivers; unsupported versions fail with remediation.
+
+The executable supported-cell source of truth is
+[`CapabilityMatrix`](./src/Workloads/_common/ai-catalog.psd1). Repository tests
+resolve every implemented/source-managed cell and validate its acquisition
+metadata, probe, report contract, and partner command; only cataloged
+`upstream-unavailable` cells may remain unimplemented.
+
+### Preview and rolling acquisition promotion
+
+Acquisition metadata is centralized in
+[`Workloads/_common/ai-catalog.psd1`](./src/Workloads/_common/ai-catalog.psd1).
+Changing from a preview/rolling artifact to a normal channel is a resolver-data
+change after the stated detection rule and real hardware acceptance pass.
+
+#### Stable-channel status
+
+| Component / tuple | Current acquisition | Current maturity/support | Stable available? | Why not selected / promotion trigger | Qualification required | Tracking |
+| --- | --- | --- | --- | --- | --- | --- |
+| CUDA x64 | WinGet `Nvidia.CUDA` | Stable | Yes | Selected | Native kernel on target GPU | Current |
+| CUDA ARM64 | Pinned NVIDIA 13.4.0 prerelease installer | Qualified interim developer preview | **Candidate:** official direct 13.4.1, SHA-256 `39af79e5…2442` | 13.4.1 is authoritative and signed but has not yet passed the N1X workload suite | `nvcc` compile/kernel plus PyTorch Triton JIT on N1X | Tracked |
+| PyTorch CUDA x64 | Official PyTorch `cu126`/`cu130` index | Stable | Yes | Selected by driver/capability | CUDA tensor + Triton kernel | Current |
+| PyTorch CUDA ARM64 | Pinned NVIDIA `2.15.0.dev...+cu134` wheel | Qualified interim nightly | **Candidate:** NVIDIA stable out-of-tree `nvtorch_oot` 2.14.0 trio | Artifact publication alone is insufficient; the exact torch/vision/audio tuple is not yet N1X-qualified | Trio imports, CUDA tensor, idempotent rerun, Triton vector-add | Tracked |
+| Triton Windows CUDA | PyPI `triton-windows==3.8.0.post28` | Community-stable, not upstream-official | No upstream Windows package | Keep exact qualified community build until an official Windows package passes | Vector-add JIT on each supported CUDA tuple | Current / monitor upstream |
+| Triton XPU | Official PyTorch XPU index `triton-xpu==3.8.0` | Stable integrated | Yes | Selected | Cold `torch.compile` on Intel GPU | Current |
+| Foundry Local | WinGet `Microsoft.FoundryLocal` 0.10.3 | Qualified preview | **Candidate:** official non-prerelease v2.0.1; Python metadata still labels the SDK alpha | v2 changes the CLI/SDK contract and has not passed x64/ARM64 provider, inference, and cached-rerun acceptance | Install/migration, EP registration, real inference, truthful fallback on both architectures | Tracked |
+| llama.cpp backends | Official rolling `bNNNNN` assets; Qualcomm pinned to policy-approved `b10917` | Rolling | No stable backend-specific Windows channel | WinGet exposes only x64 Vulkan and cannot represent the required backend matrix | Backend/device, actual offloaded layers, inference, policy acceptance | Tracked |
+| Ollama x64 | WinGet `Ollama.Ollama` | Stable | Yes | Selected | API/model/backend evidence | Current |
+| Ollama ARM64 | Latest official `ollama-windows-arm64.zip`, converted to a managed Dev Config application | Official native ARM64 archive; managed install semantics supplied by Dev Config | No official ARM64 setup EXE/current non-portable WinGet payload | Promote when an upstream managed ARM64 installer/package passes acceptance | Native PE, managed endpoint, version, digest, inference, allocation, upgrade/uninstall | Tracked |
+| ROCm/HIP x64 | AMD stable ROCm feed, exact device/gfx tuple | Stable | Yes | Selected; WinGet identity unconfirmed | Compiled HIP kernel on supported AMD GPU | Current |
+| Intel OpenVINO / oneAPI x64 | Official PyPI OpenVINO tuple / WinGet oneAPI | Stable | Yes | Selected | Requested-device inference / SYCL kernel | Current |
+
+| Component | Vendor / architecture | Current channel and identity | Integrity | Why normal channel is insufficient | Expected stable channel | Promotion trigger |
+| --- | --- | --- | --- | --- | --- | --- |
+| CUDA ARM64 | NVIDIA / ARM64 | Qualified interim `cuda_13.4.0_windows_arm64.exe`; stable 13.4.1 tracked | Pinned SHA-256 + NVIDIA Authenticode | `Nvidia.CUDA` has no ARM64 payload; 13.4.1 awaits N1X qualification | NVIDIA stable direct / `Nvidia.CUDA` ARM64 if published | 13.4.1 N1X kernel + Triton pass |
+| PyTorch CUDA x64 | NVIDIA / x64 | Stable `torch==2.14.0+cu126` or `+cu130` from official PyTorch index | Official index hashes + wheel RECORD | None | Official PyTorch CUDA index | New tuple passes tensor and Triton kernel |
+| PyTorch CUDA ARM64 | NVIDIA / ARM64 | Qualified interim nightly `torch-2.15.0.dev20260904+cu134-cp313-win_arm64.whl`; stable NVIDIA trio tracked | Pinned SHA-256 | Stable out-of-tree tuple has not passed N1X trio/tensor/Triton acceptance | NVIDIA `nvtorch_oot` | Stable trio passes N1X acceptance |
+| PyTorch ROCm x64 | AMD / x64 | Stable `torch[device-]==2.13.0+rocm10.0.0`, matching torchvision and torchaudio from AMD feed | AMD HTTPS feed + wheel RECORD | Default PyPI has no AMD ROCm Windows build | AMD stable ROCm feed | New exact tuple lists GPU and tensor acceptance passes |
+| PyTorch XPU x64 | Intel / x64 | Stable `torch==2.14.0+xpu`, `torchvision==0.29.0+xpu` from official XPU index | Official index hashes + wheel RECORD | Default PyPI has no Intel XPU build | Official PyTorch XPU index | New tuple passes XPU tensor and `torch.compile` |
+| Triton Windows CUDA | NVIDIA x64/ARM64 | Community `triton-windows==3.8.0.post28` | Package-index TLS + wheel RECORD | Upstream Triton has no general stable Windows package | Official PyTorch/Triton Windows feed, unconfirmed | Official package appears and kernel passes |
+| Triton XPU / `torch.compile` | Intel / x64 | Stable `triton-xpu==3.8.0` from official PyTorch XPU index | Official index hashes + wheel RECORD | Standalone Intel Triton documents Linux; Windows support is integrated with PyTorch XPU | Official PyTorch XPU index | New tuple passes cold `torch.compile` |
+| llama.cpp CUDA x64 | NVIDIA / x64 | Latest complete CUDA 13.3 or 12.4 app + cudart pair from one `bNNNNN` release | GitHub asset SHA-256 digests | WinGet maps only to Vulkan | Backend-specific `ggml.llamacpp` CUDA variant, unconfirmed | Package variant appears and NVIDIA benchmark/inference pass |
+| llama.cpp CUDA ARM64 | NVIDIA / ARM64 | Latest complete CUDA 13.4 app + cudart pair; developer-preview stack | GitHub asset SHA-256 digests | WinGet has no ARM64 CUDA variant | Backend-specific `ggml.llamacpp` CUDA ARM64 variant, unconfirmed | Package variant appears and N1X benchmark/inference pass |
+| llama.cpp ROCm x64 | AMD / x64 | Latest ROCm 10.0 asset from a complete `bNNNNN` release | GitHub asset SHA-256 digest | WinGet maps only to Vulkan | Backend-specific `ggml.llamacpp` ROCm variant, unconfirmed | Package variant appears and AMD benchmark/inference pass |
+| llama.cpp SYCL / OpenVINO x64 | Intel/general / x64 | Latest SYCL or OpenVINO 2026.3.1 asset | GitHub asset SHA-256 digest | WinGet maps only to Vulkan | Backend-specific `ggml.llamacpp` SYCL/OpenVINO variants, unconfirmed | Package variant appears and selected-device benchmark/inference pass |
+| llama.cpp OpenCL Adreno ARM64 | Qualcomm / ARM64 | Policy-approved and physically qualified `b10917` Adreno OpenCL asset | GitHub asset SHA-256 digest | WinGet has no ARM64 Adreno variant; newer unsigned b10919 is blocked by managed Defender policy | Backend-specific `ggml.llamacpp` OpenCL ARM64 variant, unconfirmed | New candidate passes policy and Adreno benchmark/inference |
+| llama.cpp Vulkan x64 | Cross-vendor / x64 | Latest official rolling Vulkan asset | GitHub asset SHA-256 digest | Current WinGet package cannot coexist as explicit backend variants | `ggml.llamacpp` Vulkan with reliable backend identity | Package backend/version evidence and Vulkan inference pass |
+| llama.cpp CPU x64/ARM64 | CPU / x64, ARM64 | Latest official rolling CPU asset | GitHub asset SHA-256 digest | WinGet lacks ARM64 and backend-selectable CPU variants | Backend-specific `ggml.llamacpp` CPU variants, unconfirmed | Package variants appear and CPU inference passes |
+| Foundry Local | Cross-vendor / x64, ARM64 | Qualified WinGet 0.10.3 preview; official v2.0.1 tracked | WinGet signature; v2 release hashes recorded | v2 CLI/SDK migration and target-hardware qualification pending | Official v2 release / current stable WinGet | x64+ARM64 provider/inference/cached rerun pass |
+| Ollama ARM64 | CPU/NVIDIA / ARM64 | Latest stable official `ollama-windows-arm64.zip`, installed at `%LOCALAPPDATA%\Programs\Ollama` with manifest/startup/PATH lifecycle | GitHub asset SHA-256 | Upstream has no ARM64 setup EXE; x64 setup is never emulated and portable WinGet is not used | Official ARM64 installer or architecture-correct managed WinGet payload | Managed package passes native PE/API/model/backend/upgrade/uninstall acceptance |
+
+### Partner validation commands
+
+Use the exact PR head that was statically qualified:
+
+```powershell
+git fetch https://github.com/Kixantrix/WindowsDeveloperConfig.git `
+ mihippel-microsoft-windows-ai-setup-workloads
+git switch --detach FETCH_HEAD
+$ExpectedHead = (
+ git ls-remote https://github.com/Kixantrix/WindowsDeveloperConfig.git `
+ refs/heads/mihippel-microsoft-windows-ai-setup-workloads
+).Split("`t")[0]
+if ((git rev-parse HEAD).Trim() -ne $ExpectedHead) {
+ throw "Checkout does not match published branch head $ExpectedHead."
+}
+```
+
+Always run from the live published branch head selected above; the replacement
+PR targets `main` after the former dependent PR #98 closed when its base branch
+was deleted.
+
+Open **elevated PowerShell** in the repository root, then use this harness. It
+always inventories first, runs a non-mutating plan, stops on blockers, applies
+the same arguments, and requires `result.ready=true`.
+
+PR/source validation uses unsigned files under `src/`. Before opening the
+elevated shell, follow the repository's
+[unsigned-development policy](./src/windows-dev-config/README.md#running-it-other-ways)
+and temporarily set the test user's `CurrentUser` execution policy to `Bypass`;
+restore the prior policy afterward. Release copies under top-level `Workloads/`
+are Authenticode-signed. The protected bootstrap explicitly verifies Microsoft
+signatures and requests process-scoped `RemoteSigned`; CI additionally validates
+that the scripts remain compatible with organization-enforced `AllSigned`,
+where a first run may prompt to trust the Microsoft publisher.
+
+```powershell
+$ErrorActionPreference = 'Stop'
+$ReportRoot = Join-Path $env:TEMP "devconfig-ai-$env:COMPUTERNAME"
+New-Item -ItemType Directory -Path $ReportRoot -Force | Out-Null
+
+.\src\tools\collect-ai-hardware.ps1 `
+ -OutputPath "$ReportRoot\hardware.json" *>&1 |
+ Tee-Object "$ReportRoot\hardware.console.log"
+
+function Invoke-PartnerFlow {
+ param(
+ [Parameter(Mandatory)] [string] $Name,
+ [Parameter(Mandatory)] [string] $Script,
+ [hashtable] $Parameters = @{}
+ )
+ $planPath = Join-Path $ReportRoot "$Name-plan.json"
+ $finalPath = Join-Path $ReportRoot "$Name-report.json"
+ $planParameters = @{} + $Parameters
+ $planParameters.PlanOnly = $true
+ $planParameters.ReportPath = $planPath
+ & $Script @planParameters *>&1 |
+ Tee-Object (Join-Path $ReportRoot "$Name-plan.console.log")
+ $plan = Get-Content $planPath -Raw | ConvertFrom-Json
+ if ($plan.result.blockers.Count) {
+ throw "$Name blocked: $($plan.result.blockers -join '; ')"
+ }
+ $finalParameters = @{} + $Parameters
+ $finalParameters.ReportPath = $finalPath
+ & $Script @finalParameters *>&1 |
+ Tee-Object (Join-Path $ReportRoot "$Name-report.console.log")
+ $final = Get-Content $finalPath -Raw | ConvertFrom-Json
+ if (-not $final.result.ready) {
+ throw "$Name did not produce result.ready=true."
+ }
+}
+```
+
+Run the assigned device group:
+
+```powershell
+# NVIDIA Windows x64
+Invoke-PartnerFlow nvidia-cuda .\src\Workloads\cuda\install.ps1
+Invoke-PartnerFlow nvidia-pytorch .\src\Workloads\pytorch\install.ps1 `
+ @{ Backend = 'CUDA'; RequireTriton = $true }
+Invoke-PartnerFlow nvidia-llama .\src\Workloads\llama.cpp\install.ps1 `
+ @{ Backend = 'CUDA' }
+
+# AMD Windows x64
+Invoke-PartnerFlow pytorch-rocm .\src\Workloads\pytorch\install.ps1 `
+ @{ Backend = 'ROCm' }
+Invoke-PartnerFlow amd-hip .\src\Workloads\rocm\install.ps1
+Invoke-PartnerFlow amd-llama .\src\Workloads\llama.cpp\install.ps1 `
+ @{ Backend = 'ROCm' }
+
+# Intel Windows x64 GPU
+Invoke-PartnerFlow pytorch-xpu .\src\Workloads\pytorch\install.ps1 `
+ @{ Backend = 'XPU'; RequireTriton = $true }
+Invoke-PartnerFlow intel-openvino-gpu .\src\Workloads\intel-ai\install.ps1 `
+ @{ Device = 'GPU'; Profile = 'OpenVINO' }
+Invoke-PartnerFlow intel-full-gpu .\src\Workloads\intel-ai\install.ps1 `
+ @{ Device = 'GPU'; Profile = 'Full' }
+Invoke-PartnerFlow intel-llama-sycl .\src\Workloads\llama.cpp\install.ps1 `
+ @{ Backend = 'SYCL' }
+Invoke-PartnerFlow intel-llama-openvino .\src\Workloads\llama.cpp\install.ps1 `
+ @{ Backend = 'OpenVINO' }
+
+# Intel Windows x64 NPU (separate from XPU/SYCL GPU paths)
+Invoke-PartnerFlow intel-openvino-npu .\src\Workloads\intel-ai\install.ps1 `
+ @{ Device = 'NPU'; Profile = 'OpenVINO' }
+
+# Qualcomm/Adreno Windows ARM64
+Invoke-PartnerFlow qualcomm-llama .\src\Workloads\llama.cpp\install.ps1 `
+ @{ Backend = 'OpenCL' }
+Invoke-PartnerFlow qualcomm-foundry .\src\Workloads\foundry\install.ps1
+```
+
+For example, the AMD and Intel PyTorch commands above write
+`$ReportRoot\pytorch-rocm-report.json` and
+`$ReportRoot\pytorch-xpu-report.json`, respectively.
+
+Foundry acceleration is source-managed: its Qualcomm run succeeds with any
+truthfully reported provider, including CPU fallback. For same-vendor secondary
+adapters, use `-DeviceIndex`, llama.cpp `-Device`, OpenVINO
+`-OpenVinoDeviceId`, or oneAPI `-SyclDeviceSelector` as documented above.
+
+Return the entire `$ReportRoot` directory and state whether any installer
+requested or performed a reboot. Success is **not** the presence of
+`INSTALL_OK`; the final JSON must have `result.ready=true`, no blockers, and
+acceptance evidence for the intended backend/device.
+
+| Evidence | Required fields or proof |
+| --- | --- |
+| Host inventory | `host.architecture`, GPU vendor/model/driver in `host.gpus`, and relevant `host.npus` |
+| Acquisition | Each `acquisitions[]` action, source/package or artifact identity, version/requirement, integrity metadata, cache/install path |
+| Selection | Requested and selected backend/device/profile; explicit adapter selector when used |
+| CUDA / HIP | Compiler/runtime version, actual device, compute capability or gfx target, compiled/executed kernel marker |
+| PyTorch / Triton | Exact package tuple, runtime (`torch.version.cuda` or `torch.version.hip`), actual device, tensor marker, Triton vector-add or `torch.compile` evidence |
+| Intel AI | OpenVINO requested/actual CPU/GPU/NPU device and provider; SYCL actual GPU and kernel marker |
+| llama.cpp | Release tag/assets/digests, `backends`, `gpu_info`, actual `offloaded X/Y layers`, model hash and inference marker |
+| Foundry | Selected source-managed device/EP, inference marker, `fallbackUsed`; CPU fallback is valid when reported |
+| Ollama | Model digest, inference marker, actual backend/process evidence, VRAM bytes and `gpuFraction` when accelerated |
+| Outcome | `result.ready=true`, plus all `warnings` and `blockers`; plan and final console logs |
+
+Partner hardware remains pending for NVIDIA x64, AMD x64, Intel x64 GPU/NPU,
+and Qualcomm/Adreno ARM64. Native Windows ARM64 ROCm/XPU and native Windows AMD
+Triton remain explicitly upstream-unavailable.
+
+
+
+## 🎨 Command Palette extension (coming soon)
+
+A [PowerToys Command Palette](https://learn.microsoft.com/windows/powertoys/command-palette/overview) extension lives under [`src/future/cmdpal/`](./src/future/cmdpal/). It reads the same flow list as the rest of the repo and launches DSC-backed or PowerShell-native flows from one list.
+
+See [`src/future/cmdpal/README.md`](./src/future/cmdpal/README.md) for build and install instructions.
+
## 🩺 Troubleshooting
diff --git a/src/Workloads/_common/ai-catalog.psd1 b/src/Workloads/_common/ai-catalog.psd1
new file mode 100644
index 00000000..5d996600
--- /dev/null
+++ b/src/Workloads/_common/ai-catalog.psd1
@@ -0,0 +1,709 @@
+@{
+ SchemaVersion = 1
+ Components = @{
+ CudaX64 = @{
+ Component = 'NVIDIA CUDA Toolkit'
+ Vendor = 'NVIDIA'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'winget'
+ PackageId = 'Nvidia.CUDA'
+ VersionPolicy = 'latest applicable stable package'
+ Integrity = 'WinGet manifest SHA-256 and installer signature'
+ CachePath = 'WinGet managed'
+ InstallPath = '%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v*'
+ NormalChannelLimitation = 'None'
+ ExpectedStableSource = 'Nvidia.CUDA'
+ MigrationTrigger = 'WinGet reports a newer applicable stable package'
+ CleanupUpgrade = 'WinGet upgrade; vendor uninstaller for removal'
+ }
+ CudaArm64 = @{
+ Component = 'NVIDIA CUDA Toolkit'
+ Vendor = 'NVIDIA'
+ Architectures = @('Arm64')
+ Maturity = 'qualified-interim-developer-preview'
+ SourceType = 'direct'
+ Version = '13.4.0'
+ Artifact = 'cuda_13.4.0_windows_arm64.exe'
+ Uri = 'https://packages.nvidia.com/prerelease/cuda/13.4.0/local_installers/cuda_13.4.0_windows_arm64.exe'
+ Sha256 = 'a1f68c81160b16d519c4087788b9c07de41306c3f1b872471ceee0996621374d'
+ VersionPolicy = 'exact N1X-qualified interim release'
+ Integrity = 'Pinned SHA-256 plus valid NVIDIA Corporation Authenticode signature'
+ CachePath = '%ProgramData%\WindowsDeveloperConfig\cache\nvidia-cuda\13.4.0'
+ InstallPath = '%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v13.4'
+ NormalChannelLimitation = 'Nvidia.CUDA WinGet has no ARM64 payload; stable 13.4.1 direct installer is discovered but not yet N1X workload-qualified'
+ ExpectedStableSource = 'NVIDIA stable CUDA direct download, then Nvidia.CUDA ARM64 if published'
+ MigrationTrigger = 'CUDA 13.4.1 passes N1X nvcc compile/kernel and PyTorch Triton JIT acceptance'
+ CleanupUpgrade = 'Qualify newer version side-by-side, then use NVIDIA uninstaller for the old interim release'
+ PromotionCandidate = @{
+ Version = '13.4.1'
+ Maturity = 'official-stable-direct-candidate'
+ Artifact = 'cuda_13.4.1_windows_arm64.exe'
+ Uri = 'https://developer.download.nvidia.com/compute/cuda/13.4.1/local_installers/cuda_13.4.1_windows_arm64.exe'
+ Sha256 = '39af79e5e136c4e0de03bba816bda60fd7b70aad033e37ecaacf9f2e2c982442'
+ Size = 3711598920
+ Authenticity = 'Valid NVIDIA Corporation Authenticode signature verified'
+ TrackingStatus = 'awaiting N1X kernel and Triton qualification'
+ }
+ }
+ FoundryLocal = @{
+ Component = 'Foundry Local'
+ Vendor = 'Microsoft'
+ Architectures = @('X64', 'Arm64')
+ Maturity = 'qualified-preview'
+ SourceType = 'winget'
+ PackageId = 'Microsoft.FoundryLocal'
+ VersionPolicy = 'latest applicable qualified preview package'
+ Integrity = 'WinGet manifest SHA-256 and MSIX signature'
+ CachePath = 'Foundry cache reported by foundry cache location'
+ InstallPath = 'Per-user MSIX'
+ NormalChannelLimitation = 'WinGet remains 0.10.3 preview; official v2.0.1 is a candidate with a new SDK/API and is not yet workload-qualified'
+ ExpectedStableSource = 'Official Foundry Local v2 release or a current stable Microsoft.FoundryLocal package'
+ MigrationTrigger = 'v2.0.1 passes x64/ARM64 installation, provider registration, cached rerun, and real model inference'
+ CleanupUpgrade = 'Preserve model cache while replacing the qualified runtime'
+ PromotionCandidate = @{
+ Version = '2.0.1'
+ Maturity = 'official-non-prerelease-candidate; Python package metadata remains alpha'
+ Repository = 'microsoft/Foundry-Local'
+ PythonRequirement = 'foundry-local-sdk==2.0.1'
+ X64Asset = 'foundry-local-win-x64.zip'
+ X64Sha256 = '0551db07d5cba6a523e4c1832f0d38e023301ab67b946378239f8cee156ba5a4'
+ Arm64Asset = 'foundry-local-win-arm64.zip'
+ Arm64Sha256 = '2fa8510281cfaa554e21ffae8de41366a08051bce92fd592b919bc4413b57b09'
+ TrackingStatus = 'awaiting v2 CLI/SDK migration and x64/ARM64 real inference qualification'
+ }
+ }
+ NvidiaPyTorchArm64 = @{
+ Component = 'PyTorch CUDA for Windows ARM64'
+ Vendor = 'NVIDIA/PyTorch'
+ Architectures = @('Arm64')
+ Maturity = 'qualified-interim-nightly'
+ SourceType = 'direct-python-wheel'
+ Version = '2.15.0.dev20260904+cu134'
+ Uri = 'https://pypi.nvidia.com/nvtorch_oot_nightly/torch/torch-2.15.0.dev20260904%2Bcu134-cp313-cp313-win_arm64.whl'
+ Sha256 = 'af0872854d183cb6894dbd5b1e5e9291875ce139d138b5fc0b501498828265d3'
+ VersionPolicy = 'exact N1X-qualified interim nightly'
+ Integrity = 'Pinned SHA-256; dependencies resolve from the configured primary Python index'
+ CachePath = '%LOCALAPPDATA%\DevConfig\pytorch\wheel-cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ NormalChannelLimitation = 'Stable NVIDIA nvtorch_oot tuple is published but has not yet passed the N1X tensor/Triton qualification'
+ ExpectedStableSource = 'NVIDIA stable nvtorch_oot index'
+ MigrationTrigger = 'Stable 2.14.0 cu134 trio passes imports, N1X CUDA tensor, and Triton vector-add'
+ CleanupUpgrade = 'Replace contained venv; retain only qualified wheel cache entries'
+ NativeToolkitRequired = $false
+ NativeToolkitRelationship = 'The wheel carries the CUDA runtime. The standalone cuda flow is for native CUDA development; this setup acquires compiler/toolkit components only for supported Triton JIT.'
+ PromotionCandidate = @{
+ Maturity = 'official-out-of-tree-stable-candidate'
+ IndexUrl = 'https://pypi.nvidia.com/nvtorch_oot/'
+ Torch = 'torch==2.14.0+cu134'
+ TorchSha256 = '4f781babc0e0e0722cc48d0b15107a28e6003fc2b6544f1578b6eb6f5177dcb5'
+ Torchvision = 'torchvision==0.29.0+cu134'
+ TorchvisionSha256 = 'e935037b6a97c32642d47f73da8cf62acf6453bfe15825314f774f62ec395d26'
+ Torchaudio = 'torchaudio==2.11.0+cu134'
+ TorchaudioSha256 = 'e4f18fa7359528416964d525ba620a0ca95ad231d6ab573b26c8b09c6ea8bf6b'
+ TrackingStatus = 'awaiting N1X trio import, tensor, and Triton qualification'
+ }
+ }
+ PyTorchCpu = @{
+ Component = 'PyTorch CPU'
+ Vendor = 'PyTorch'
+ Architectures = @('X64', 'Arm64')
+ Maturity = 'stable'
+ SourceType = 'pytorch-index'
+ IndexUrl = 'https://download.pytorch.org/whl/cpu'
+ Version = '2.14.0'
+ VersionPolicy = 'exact stable backend-qualified wheel'
+ Integrity = 'Official PyTorch package index hashes and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ NormalChannelLimitation = 'None'
+ ExpectedStableSource = 'https://download.pytorch.org/whl/cpu'
+ MigrationTrigger = 'New stable tuple passes CPU tensor acceptance'
+ CleanupUpgrade = 'Replace contained venv'
+ NativeToolkitRequired = $false
+ NativeToolkitRelationship = 'No vendor toolkit is required.'
+ }
+ PyTorchCudaX64 = @{
+ Component = 'PyTorch CUDA'
+ Vendor = 'NVIDIA/PyTorch'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'pytorch-index'
+ IndexUrl = 'https://download.pytorch.org/whl/cu126 or cu130'
+ Version = '2.14.0'
+ VersionPolicy = 'exact stable wheel selected by GPU capability and driver branch'
+ Integrity = 'Official PyTorch package index hashes and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ NormalChannelLimitation = 'None'
+ ExpectedStableSource = 'Official PyTorch CUDA index'
+ MigrationTrigger = 'New stable runtime tuple passes CUDA tensor and Triton acceptance'
+ CleanupUpgrade = 'Replace contained venv'
+ NativeToolkitRequired = $false
+ NativeToolkitRelationship = 'The wheel carries the CUDA runtime. The standalone cuda flow is for nvcc/native development; this setup acquires CUDA/MSVC only when Triton JIT requires toolchain components.'
+ }
+ PyTorchRocm = @{
+ Component = 'PyTorch ROCm'
+ Vendor = 'AMD/PyTorch'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'amd-python-index'
+ IndexUrl = 'https://stable.repo.amd.com/rocm/whl-next/'
+ Version = '2.13.0+rocm10.0.0'
+ VersionPolicy = 'exact production tuple and exact supported gfx target'
+ Integrity = 'Official AMD HTTPS feed allowlist and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ NormalChannelLimitation = 'Default PyPI does not publish the AMD ROCm Windows build'
+ ExpectedStableSource = 'AMD stable ROCm package feed'
+ MigrationTrigger = 'New production tuple lists the GPU and tensor acceptance passes'
+ CleanupUpgrade = 'Replace contained venv'
+ NativeToolkitRequired = $false
+ NativeToolkitRelationship = 'The AMD device-specific PyTorch tuple carries its ROCm runtime dependencies. The standalone rocm flow is not a prerequisite; it is for hipcc/native HIP kernel development.'
+ }
+ PyTorchXpu = @{
+ Component = 'PyTorch XPU'
+ Vendor = 'Intel/PyTorch'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'pytorch-index'
+ IndexUrl = 'https://download.pytorch.org/whl/xpu'
+ Version = '2.14.0+xpu'
+ VersionPolicy = 'exact stable XPU tuple'
+ Integrity = 'Official PyTorch package index hashes and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ NormalChannelLimitation = 'Default PyPI does not publish the Intel XPU build'
+ ExpectedStableSource = 'https://download.pytorch.org/whl/xpu'
+ MigrationTrigger = 'New stable tuple passes XPU tensor and torch.compile acceptance'
+ CleanupUpgrade = 'Replace contained venv'
+ NativeToolkitRequired = $false
+ NativeToolkitRelationship = 'The official XPU wheel tuple carries the PyTorch runtime and does not install full oneAPI. The standalone intel-ai SYCL/Full profiles install oneAPI only for native SYCL development.'
+ }
+ TritonWindows = @{
+ Component = 'Triton Windows'
+ Vendor = 'Triton project'
+ Architectures = @('X64', 'Arm64')
+ Maturity = 'community'
+ SourceType = 'pypi'
+ Package = 'triton-windows==3.8.0.post28'
+ VersionPolicy = 'exact qualified build matched to PyTorch'
+ Integrity = 'Python package index TLS and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = 'PyTorch contained venv'
+ NormalChannelLimitation = 'Upstream Triton does not publish a general stable Windows package'
+ ExpectedStableSource = 'Official PyTorch/Triton Windows package feed (unconfirmed)'
+ MigrationTrigger = 'Official Windows package is published and vector-add acceptance passes'
+ CleanupUpgrade = 'Replace contained venv when PyTorch/Triton tuple changes'
+ }
+ TritonXpu = @{
+ Component = 'Triton XPU'
+ Vendor = 'Intel/PyTorch'
+ Architectures = @('X64')
+ Maturity = 'stable-integrated'
+ SourceType = 'pytorch-index'
+ Package = 'triton-xpu==3.8.0'
+ VersionPolicy = 'exact PyTorch XPU-compatible tuple'
+ Integrity = 'Official PyTorch XPU index hash and wheel RECORD'
+ CachePath = 'Python package cache'
+ InstallPath = 'PyTorch contained venv'
+ NormalChannelLimitation = 'Standalone Intel Triton still documents Linux; Windows support is through PyTorch torch.compile'
+ ExpectedStableSource = 'Official PyTorch XPU index'
+ MigrationTrigger = 'New PyTorch XPU tuple passes cold torch.compile acceptance'
+ CleanupUpgrade = 'Replace contained venv'
+ }
+ LlamaCppRolling = @{
+ Component = 'llama.cpp Windows binaries'
+ Vendor = 'ggml-org'
+ Architectures = @('X64', 'Arm64')
+ Maturity = 'rolling'
+ SourceType = 'github-release'
+ Repository = 'ggml-org/llama.cpp'
+ BackendAssets = @{
+ CpuX64 = @{
+ Backend = 'CPU'
+ Vendor = 'CPU'
+ Architecture = 'X64'
+ Runtime = 'CPU x64'
+ Patterns = @('^llama-b[0-9]+-bin-win-cpu-x64\.zip$')
+ }
+ CpuArm64 = @{
+ Backend = 'CPU'
+ Vendor = 'CPU'
+ Architecture = 'Arm64'
+ Runtime = 'CPU ARM64'
+ Patterns = @('^llama-b[0-9]+-bin-win-cpu-arm64\.zip$')
+ }
+ Cuda124X64 = @{
+ Backend = 'CUDA'
+ Vendor = 'NVIDIA'
+ Architecture = 'X64'
+ Runtime = 'CUDA 12.4'
+ Patterns = @(
+ '^llama-b[0-9]+-bin-win-cuda-12\.4-x64\.zip$',
+ '^cudart-llama-bin-win-cuda-12\.4-x64\.zip$'
+ )
+ }
+ Cuda133X64 = @{
+ Backend = 'CUDA'
+ Vendor = 'NVIDIA'
+ Architecture = 'X64'
+ Runtime = 'CUDA 13.3'
+ Patterns = @(
+ '^llama-b[0-9]+-bin-win-cuda-13\.3-x64\.zip$',
+ '^cudart-llama-bin-win-cuda-13\.3-x64\.zip$'
+ )
+ }
+ Cuda134Arm64 = @{
+ Backend = 'CUDA'
+ Vendor = 'NVIDIA'
+ Architecture = 'Arm64'
+ Runtime = 'CUDA 13.4 Developer Preview'
+ Maturity = 'rolling-developer-preview'
+ Patterns = @(
+ '^llama-b[0-9]+-bin-win-cuda-13\.4-arm64\.zip$',
+ '^cudart-llama-bin-win-cuda-13\.4-arm64\.zip$'
+ )
+ }
+ Rocm10X64 = @{
+ Backend = 'ROCm'
+ Vendor = 'AMD'
+ Architecture = 'X64'
+ Runtime = 'ROCm 10.0'
+ Patterns = @('^llama-b[0-9]+-bin-win-rocm-10\.0-x64\.zip$')
+ }
+ SyclX64 = @{
+ Backend = 'SYCL'
+ Vendor = 'Intel'
+ Architecture = 'X64'
+ Runtime = 'SYCL'
+ Patterns = @('^llama-b[0-9]+-bin-win-sycl-x64\.zip$')
+ }
+ OpenVinoX64 = @{
+ Backend = 'OpenVINO'
+ Vendor = 'Intel/general'
+ Architecture = 'X64'
+ Runtime = 'OpenVINO 2026.3.1'
+ Patterns = @('^llama-b[0-9]+-bin-win-openvino-2026\.3\.1-x64\.zip$')
+ }
+ VulkanX64 = @{
+ Backend = 'Vulkan'
+ Vendor = 'Cross-vendor'
+ Architecture = 'X64'
+ Runtime = 'Vulkan'
+ Patterns = @('^llama-b[0-9]+-bin-win-vulkan-x64\.zip$')
+ }
+ OpenClAdrenoArm64 = @{
+ Backend = 'OpenCL'
+ Vendor = 'Qualcomm'
+ Architecture = 'Arm64'
+ Runtime = 'OpenCL Adreno'
+ Patterns = @('^llama-b10917-bin-win-opencl-adreno-arm64\.zip$')
+ VersionPolicy = 'pinned b10917 qualified on Qualcomm ARM64; b10919 is blocked by managed Defender ransomware protection'
+ }
+ }
+ VersionPolicy = 'newest bNNNNN release containing a complete backend asset set'
+ Integrity = 'GitHub release asset SHA-256 digest'
+ CachePath = '%LOCALAPPDATA%\DevConfig\llama.cpp\asset-cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\llama.cpp\runtime'
+ NormalChannelLimitation = 'WinGet ggml.llamacpp currently maps only to the x64 Vulkan variant and does not expose backend-specific package choices'
+ ExpectedStableSource = 'ggml.llamacpp backend-specific package variants when published; otherwise unconfirmed'
+ MigrationTrigger = 'WinGet publishes the required backend for the host and inference/benchmark acceptance passes'
+ CleanupUpgrade = 'Reuse digest-verified asset cache and atomically replace resolver-owned runtime directory'
+ }
+ OllamaX64 = @{
+ Component = 'Ollama'
+ Vendor = 'Ollama'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'winget'
+ PackageId = 'Ollama.Ollama'
+ VersionPolicy = 'latest applicable stable package'
+ Integrity = 'WinGet manifest SHA-256 and installer signature'
+ CachePath = '%USERPROFILE%\.ollama\models or OLLAMA_MODELS'
+ InstallPath = 'Per-user application'
+ NormalChannelLimitation = 'None'
+ ExpectedStableSource = 'Ollama.Ollama'
+ MigrationTrigger = 'WinGet reports a newer applicable stable package'
+ CleanupUpgrade = 'WinGet upgrade; ollama rm for models'
+ }
+ OllamaArm64 = @{
+ Component = 'Ollama managed native ARM64 application'
+ Vendor = 'Ollama'
+ Architectures = @('Arm64')
+ Maturity = 'stable-direct'
+ SourceType = 'native-arm64-managed-archive'
+ Repository = 'ollama/ollama'
+ AssetPattern = '^ollama-windows-arm64\.zip$'
+ VersionPolicy = 'latest non-prerelease release'
+ Integrity = 'GitHub release asset SHA-256 digest'
+ CachePath = '%LOCALAPPDATA%\DevConfig\ollama\asset-cache'
+ InstallPath = '%LOCALAPPDATA%\Programs\Ollama'
+ StartupRegistration = 'HKCU Run: WindowsDeveloperConfig.Ollama'
+ InstallManifest = '%LOCALAPPDATA%\Programs\Ollama\.devconfig-install.json'
+ NormalChannelLimitation = 'Ollama.Ollama uses the x64 setup EXE; upstream publishes native ARM64 only as an archive'
+ ExpectedStableSource = 'Official ARM64 setup EXE or architecture-correct non-portable WinGet payload'
+ MigrationTrigger = 'Official managed ARM64 installer/package appears and passes native process, API, model, and backend-allocation acceptance'
+ CleanupUpgrade = 'Stop only Dev Config-managed processes; reuse verified cache; atomic runtime replacement; preserve models by default on uninstall'
+ }
+ AmdRocm = @{
+ Component = 'AMD ROCm Core SDK'
+ Vendor = 'AMD'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'amd-python-index'
+ IndexUrl = 'https://stable.repo.amd.com/rocm/whl-next/'
+ Version = '10.0.0'
+ PackageTemplate = 'rocm[libraries,devel,device-{0}]==10.0.0'
+ VersionPolicy = 'exact production tuple and exact supported gfx target'
+ Integrity = 'Exact AMD HTTPS feed allowlist and wheel RECORD; AMD feed does not publish SHA-256 fragments'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\rocm\.venv'
+ NormalChannelLimitation = 'No WinGet package and not published on the default PyPI channel'
+ ExpectedStableSource = 'AMD stable ROCm package feed; WinGet package unconfirmed'
+ MigrationTrigger = 'New AMD production tuple lists the exact GPU in Windows compatibility data and HIP kernel acceptance passes'
+ CleanupUpgrade = 'Replace versioned contained environment after HIP kernel validation'
+ }
+ IntelOpenVino = @{
+ Component = 'Intel OpenVINO Runtime/GenAI'
+ Vendor = 'Intel'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'pypi'
+ Packages = @('openvino==2026.3.1', 'openvino-tokenizers==2026.3.1.0', 'openvino-genai==2026.3.1.0')
+ VersionPolicy = 'exact matched regular release tuple'
+ Integrity = 'Official PyPI wheel hashes/RECORD; contained environment'
+ CachePath = 'Python package cache'
+ InstallPath = '%LOCALAPPDATA%\DevConfig\intel-ai\openvino\.venv'
+ NormalChannelLimitation = 'WinGet C++ package is community-maintained and can lag the Python runtime'
+ ExpectedStableSource = 'Official PyPI OpenVINO packages'
+ MigrationTrigger = 'Matched newer regular/LTS tuple passes selected-device inference'
+ CleanupUpgrade = 'Replace contained environment'
+ }
+ IntelOneApi = @{
+ Component = 'Intel oneAPI Toolkit'
+ Vendor = 'Intel'
+ Architectures = @('X64')
+ Maturity = 'stable'
+ SourceType = 'winget'
+ PackageId = 'Intel.OneAPI.Toolkit'
+ Version = '2026.0.0.193'
+ VersionPolicy = 'latest qualified stable WinGet package'
+ Integrity = 'WinGet manifest SHA-256 and Intel installer signature'
+ CachePath = 'WinGet managed'
+ InstallPath = '%ProgramFiles(x86)%\Intel\oneAPI'
+ NormalChannelLimitation = 'None'
+ ExpectedStableSource = 'Intel.OneAPI.Toolkit'
+ MigrationTrigger = 'New stable WinGet version passes SYCL kernel acceptance'
+ CleanupUpgrade = 'WinGet upgrade; Intel installer for removal'
+ }
+ }
+ CapabilityMatrix = @(
+ @{
+ Id = 'cuda-nvidia-x64'
+ Workload = 'cuda'; Architecture = 'X64'; Vendor = 'NVIDIA'; DeviceFamily = 'CUDA 13-supported CC7.5+ GPU'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:CudaX64', 'winget:Microsoft.VisualStudio.2022.BuildTools')
+ Prerequisites = 'NVIDIA driver 580+ and compute capability 7.5+ for current stable GPU readiness; toolkit-only mode is explicit'
+ Resolver = 'Resolve-CudaInstallPlan'; ResolverArguments = @{ Architecture = 'X64' }; Expected = @{ Method = 'WinGet' }
+ ProbePath = 'src/Workloads/cuda/smoke.cu'; ReportEvidence = 'nvcc, compiler path, driver/device/compute capability, compiled and executed kernel'
+ PartnerCommand = '.\src\Workloads\cuda\install.ps1 -ReportPath "$env:TEMP\cuda-x64-report.json"'
+ }
+ @{
+ Id = 'cuda-nvidia-arm64'
+ Workload = 'cuda'; Architecture = 'Arm64'; Vendor = 'NVIDIA'; DeviceFamily = 'RTX Spark-class CC12.x'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'qualified-interim-developer-preview'; Acquisition = @('component:CudaArm64', 'winget:Microsoft.VisualStudio.2022.BuildTools')
+ Prerequisites = 'Windows 11, driver 616+, RTX Spark-class NVIDIA GPU'
+ Resolver = 'Resolve-CudaInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64'; WindowsBuild = 28120 }; Expected = @{ Method = 'NvidiaInstaller'; ToolkitVersion = '13.4' }
+ ProbePath = 'src/Workloads/cuda/smoke.cu'; ReportEvidence = 'pinned installer hash/signature, ARM64 compiler, nvcc, N1X driver/device, executed kernel'
+ PartnerCommand = '.\src\Workloads\cuda\install.ps1 -ReportPath "$env:TEMP\cuda-arm64-report.json"'
+ }
+ @{
+ Id = 'rocm-amd-x64'
+ Workload = 'rocm'; Architecture = 'X64'; Vendor = 'AMD'; DeviceFamily = 'ROCm 10 Windows gfx matrix'; Backend = 'HIP'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:AmdRocm')
+ Prerequisites = 'AMD GPU marketing name must map to a published gfx target'
+ Resolver = 'Resolve-RocmInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; GpuName = 'AMD Radeon RX 9070 XT' }; Expected = @{ GfxTarget = 'gfx1201' }
+ ProbePath = 'src/Workloads/rocm/hip-smoke.cpp'; ReportEvidence = 'AMD device, gfx target, hipcc/runtime tuple, compiled and executed HIP kernel'
+ PartnerCommand = '.\src\Workloads\rocm\install.ps1 -ReportPath "$env:TEMP\rocm-hip-report.json"'
+ }
+ @{
+ Id = 'intel-openvino-cpu-x64'
+ Workload = 'intel-ai'; Architecture = 'X64'; Vendor = 'Intel/general'; DeviceFamily = 'CPU'; Backend = 'OpenVINO'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:IntelOpenVino')
+ Prerequisites = 'Windows x64 and CPython 3.13'
+ Resolver = 'Resolve-IntelAiPlan'; ResolverArguments = @{ Architecture = 'X64'; Device = 'CPU'; Profile = 'OpenVINO' }; Expected = @{ Device = 'CPU'; InstallOpenVino = $true }
+ ProbePath = 'src/Workloads/intel-ai/openvino-smoke.py'; ReportEvidence = 'requested and actual OpenVINO device, full device name, generated-model inference'
+ PartnerCommand = '.\src\Workloads\intel-ai\install.ps1 -Device CPU -Profile OpenVINO -ReportPath "$env:TEMP\intel-openvino-cpu-report.json"'
+ }
+ @{
+ Id = 'intel-openvino-gpu-x64'
+ Workload = 'intel-ai'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'OpenVINO-supported Intel GPU'; Backend = 'OpenVINO'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:IntelOpenVino')
+ Prerequisites = 'Detected Intel display adapter and installed compatible driver'
+ Resolver = 'Resolve-IntelAiPlan'; ResolverArguments = @{ Architecture = 'X64'; Device = 'GPU'; Profile = 'OpenVINO'; IntelGpuPresent = $true }; Expected = @{ Device = 'GPU'; InstallOpenVino = $true }
+ ProbePath = 'src/Workloads/intel-ai/openvino-smoke.py'; ReportEvidence = 'actual OpenVINO GPU and full device name, generated-model inference'
+ PartnerCommand = '.\src\Workloads\intel-ai\install.ps1 -Device GPU -Profile OpenVINO -ReportPath "$env:TEMP\intel-openvino-gpu-report.json"'
+ }
+ @{
+ Id = 'intel-openvino-npu-x64'
+ Workload = 'intel-ai'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'Intel AI Boost/OpenVINO NPU'; Backend = 'OpenVINO'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:IntelOpenVino')
+ Prerequisites = 'Detected Intel NPU and compatible installed driver'
+ Resolver = 'Resolve-IntelAiPlan'; ResolverArguments = @{ Architecture = 'X64'; Device = 'NPU'; Profile = 'OpenVINO'; IntelNpuPresent = $true }; Expected = @{ Device = 'NPU'; InstallOpenVino = $true }
+ ProbePath = 'src/Workloads/intel-ai/openvino-smoke.py'; ReportEvidence = 'actual OpenVINO NPU and full device name, generated-model inference'
+ PartnerCommand = '.\src\Workloads\intel-ai\install.ps1 -Device NPU -Profile OpenVINO -ReportPath "$env:TEMP\intel-openvino-npu-report.json"'
+ }
+ @{
+ Id = 'intel-sycl-gpu-x64'
+ Workload = 'intel-ai'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'oneAPI-supported Intel GPU'; Backend = 'SYCL'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:IntelOneApi')
+ Prerequisites = 'Detected Intel GPU and compatible installed driver'
+ Resolver = 'Resolve-IntelAiPlan'; ResolverArguments = @{ Architecture = 'X64'; Device = 'GPU'; Profile = 'SYCL'; IntelGpuPresent = $true }; Expected = @{ Device = 'GPU'; InstallOneApi = $true }
+ ProbePath = 'src/Workloads/intel-ai/sycl-smoke.cpp'; ReportEvidence = 'oneAPI compiler/runtime, selected Intel GPU, compiled and executed SYCL kernel'
+ PartnerCommand = '.\src\Workloads\intel-ai\install.ps1 -Device GPU -Profile SYCL -ReportPath "$env:TEMP\intel-sycl-report.json"'
+ }
+ @{
+ Id = 'intel-full-gpu-x64'
+ Workload = 'intel-ai'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'OpenVINO/oneAPI-supported Intel GPU'; Backend = 'OpenVINO+SYCL'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:IntelOpenVino', 'component:IntelOneApi')
+ Prerequisites = 'Detected Intel GPU and compatible installed driver'
+ Resolver = 'Resolve-IntelAiPlan'; ResolverArguments = @{ Architecture = 'X64'; Device = 'GPU'; Profile = 'Full'; IntelGpuPresent = $true }; Expected = @{ Device = 'GPU'; InstallOpenVino = $true; InstallOneApi = $true }
+ ProbePath = 'src/Workloads/intel-ai/install.ps1'; ReportEvidence = 'actual OpenVINO GPU inference plus compiled and executed oneAPI SYCL kernel'
+ PartnerCommand = '.\src\Workloads\intel-ai\install.ps1 -Device GPU -Profile Full -ReportPath "$env:TEMP\intel-full-report.json"'
+ }
+ @{
+ Id = 'pytorch-cpu-x64'
+ Workload = 'pytorch'; Architecture = 'X64'; Vendor = 'CPU'; DeviceFamily = 'x64 CPU'; Backend = 'CPU'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:PyTorchCpu', 'winget:Python.Python.3.13')
+ Prerequisites = 'Windows x64'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'CPU'; PythonVersion = '3.13' }; Expected = @{ Backend = 'CPU'; Runtime = 'cpu' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'exact wheel tuple, CPU device, tensor and NumPy bridge'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CPU -ReportPath "$env:TEMP\pytorch-cpu-x64-report.json"'
+ }
+ @{
+ Id = 'pytorch-cpu-arm64'
+ Workload = 'pytorch'; Architecture = 'Arm64'; Vendor = 'CPU'; DeviceFamily = 'ARM64 CPU'; Backend = 'CPU'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:PyTorchCpu', 'winget:Python.Python.3.13')
+ Prerequisites = 'Windows ARM64'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'CPU'; PythonVersion = '3.13' }; Expected = @{ Backend = 'CPU'; Runtime = 'cpu' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'native ARM64 CPU wheel, CPU tensor and NumPy bridge'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CPU -ReportPath "$env:TEMP\pytorch-cpu-arm64-report.json"'
+ }
+ @{
+ Id = 'pytorch-cuda-x64'
+ Workload = 'pytorch'; Architecture = 'X64'; Vendor = 'NVIDIA'; DeviceFamily = 'CUDA-capable GPU'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:PyTorchCudaX64', 'winget:Python.Python.3.13')
+ Prerequisites = 'Compute capability 5.0+, driver 525+; CUDA 13-class GPUs require driver 580+'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'CUDA'; PythonVersion = '3.13'; HasNvidia = $true; DriverMajor = 580; ComputeCapability = '8.9'; GpuName = 'NVIDIA GeForce RTX 4090' }; Expected = @{ Backend = 'CUDA'; Runtime = 'cu130' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'NVIDIA device, torch CUDA runtime, exact wheel tuple, executed tensor'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CUDA -ReportPath "$env:TEMP\pytorch-cuda-x64-report.json"'
+ }
+ @{
+ Id = 'pytorch-cuda-arm64'
+ Workload = 'pytorch'; Architecture = 'Arm64'; Vendor = 'NVIDIA'; DeviceFamily = 'RTX Spark-class CC12.x'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'qualified-interim-nightly'; Acquisition = @('component:NvidiaPyTorchArm64', 'winget:Python.Python.3.13')
+ Prerequisites = 'CPython 3.13, driver 616+, compute capability 12.x'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'CUDA'; PythonVersion = '3.13'; HasNvidia = $true; DriverMajor = 616; ComputeCapability = '12.1'; GpuName = 'NVIDIA RTX Spark N1X' }; Expected = @{ Backend = 'CUDA'; Runtime = 'cu134' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'pinned wheel hash, N1X device, torch CUDA 13.4 tensor'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CUDA -ReportPath "$env:TEMP\pytorch-cuda-arm64-report.json"'
+ }
+ @{
+ Id = 'pytorch-rocm-x64'
+ Workload = 'pytorch'; Architecture = 'X64'; Vendor = 'AMD'; DeviceFamily = 'ROCm 10 Windows gfx matrix'; Backend = 'ROCm'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:PyTorchRocm', 'winget:Python.Python.3.13')
+ Prerequisites = 'Exact supported AMD GPU/gfx target'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'ROCm'; PythonVersion = '3.13'; HasAmd = $true; AmdGpuName = 'AMD Radeon RX 9070 XT'; AmdGfxTarget = 'gfx1201' }; Expected = @{ Backend = 'ROCm'; AmdGfxTarget = 'gfx1201' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'exact AMD package tuple, non-null torch.version.hip, AMD device/gfx, tensor'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend ROCm -ReportPath "$env:TEMP\pytorch-rocm-report.json"'
+ }
+ @{
+ Id = 'pytorch-xpu-x64'
+ Workload = 'pytorch'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'validated Intel XPU GPU families'; Backend = 'XPU'
+ Status = 'implemented-supported'; Maturity = 'stable'; Acquisition = @('component:PyTorchXpu', 'winget:Python.Python.3.13')
+ Prerequisites = 'Supported Intel GPU and compatible driver'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'XPU'; PythonVersion = '3.13'; HasIntel = $true; IntelGpuName = 'Intel Arc B580 Graphics' }; Expected = @{ Backend = 'XPU'; Runtime = 'xpu' }
+ ProbePath = 'src/Workloads/pytorch/smoke.py'; ReportEvidence = 'Intel XPU device, exact wheel tuple, executed tensor'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend XPU -ReportPath "$env:TEMP\pytorch-xpu-report.json"'
+ }
+ @{
+ Id = 'triton-cuda-x64'
+ Workload = 'pytorch-triton'; Architecture = 'X64'; Vendor = 'NVIDIA'; DeviceFamily = 'CUDA CC8.0+'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'community'; Acquisition = @('component:PyTorchCudaX64', 'component:TritonWindows')
+ Prerequisites = 'Compatible PyTorch CUDA tuple, CC8.0+, MSVC/CUDA JIT toolchain'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'CUDA'; PythonVersion = '3.13'; HasNvidia = $true; DriverMajor = 580; ComputeCapability = '8.9'; GpuName = 'NVIDIA GeForce RTX 4090' }; Expected = @{ InstallTriton = $true }
+ ProbePath = 'src/Workloads/pytorch/triton-smoke.py'; ReportEvidence = 'triton-windows version and executed vector-add kernel'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CUDA -RequireTriton -ReportPath "$env:TEMP\triton-cuda-x64-report.json"'
+ }
+ @{
+ Id = 'triton-cuda-arm64'
+ Workload = 'pytorch-triton'; Architecture = 'Arm64'; Vendor = 'NVIDIA'; DeviceFamily = 'RTX Spark CC12.x'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'community-stable-on-qualified-interim'; Acquisition = @('component:NvidiaPyTorchArm64', 'component:TritonWindows')
+ Prerequisites = 'Qualified ARM64 PyTorch CUDA preview, MSVC ARM64, CUDA 13.4'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'CUDA'; PythonVersion = '3.13'; HasNvidia = $true; DriverMajor = 616; ComputeCapability = '12.1'; GpuName = 'NVIDIA RTX Spark N1X' }; Expected = @{ InstallTriton = $true }
+ ProbePath = 'src/Workloads/pytorch/triton-smoke.py'; ReportEvidence = 'triton-windows version and N1X vector-add JIT kernel'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend CUDA -RequireTriton -ReportPath "$env:TEMP\triton-cuda-arm64-report.json"'
+ }
+ @{
+ Id = 'triton-xpu-x64'
+ Workload = 'pytorch-triton'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'validated Intel XPU GPU families'; Backend = 'XPU'
+ Status = 'implemented-supported'; Maturity = 'stable-integrated'; Acquisition = @('component:PyTorchXpu', 'component:TritonXpu')
+ Prerequisites = 'Supported Intel XPU GPU and official XPU tuple'
+ Resolver = 'Resolve-PyTorchPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'XPU'; PythonVersion = '3.13'; HasIntel = $true; IntelGpuName = 'Intel Arc B580 Graphics' }; Expected = @{ InstallTriton = $true }
+ ProbePath = 'src/Workloads/pytorch/xpu-smoke.py'; ReportEvidence = 'triton-xpu version, Intel device and successful cold torch.compile'
+ PartnerCommand = '.\src\Workloads\pytorch\install.ps1 -Backend XPU -RequireTriton -ReportPath "$env:TEMP\triton-xpu-report.json"'
+ }
+ @{
+ Id = 'llama-cpu-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'CPU'; DeviceFamily = 'x64 CPU'; Backend = 'CPU'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Windows x64'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'CPU' }; Expected = @{ Backend = 'CPU'; Runtime = 'CPU x64' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, zero GPU layers, pinned model inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend CPU -ReportPath "$env:TEMP\llama-cpu-x64-report.json"'
+ }
+ @{
+ Id = 'llama-cpu-arm64'
+ Workload = 'llama.cpp'; Architecture = 'Arm64'; Vendor = 'CPU'; DeviceFamily = 'ARM64 CPU'; Backend = 'CPU'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Windows ARM64'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'CPU' }; Expected = @{ Backend = 'CPU'; Runtime = 'CPU ARM64' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, zero GPU layers, pinned model inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend CPU -ReportPath "$env:TEMP\llama-cpu-arm64-report.json"'
+ }
+ @{
+ Id = 'llama-cuda-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'NVIDIA'; DeviceFamily = 'CUDA-capable GPU'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'CC5.x-9.x with driver 551.61+ or CC7.5+ with driver 580+ for CUDA 13.3'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'CUDA'; HasNvidia = $true; DriverVersion = '581.10'; ComputeCapability = '8.9'; NvidiaGpuName = 'NVIDIA GeForce RTX 4090' }; Expected = @{ Backend = 'CUDA'; Runtime = 'CUDA 13.3' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'paired app/cudart tag/digests, NVIDIA device, CUDA backend, GPU layers, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend CUDA -ReportPath "$env:TEMP\llama-cuda-x64-report.json"'
+ }
+ @{
+ Id = 'llama-cuda-arm64'
+ Workload = 'llama.cpp'; Architecture = 'Arm64'; Vendor = 'NVIDIA'; DeviceFamily = 'RTX Spark CC12.x'; Backend = 'CUDA'
+ Status = 'implemented-supported'; Maturity = 'rolling-developer-preview'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Driver 616+, compute capability 12.x'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'CUDA'; HasNvidia = $true; DriverVersion = '616.62'; ComputeCapability = '12.1'; NvidiaGpuName = 'NVIDIA RTX Spark N1X' }; Expected = @{ Backend = 'CUDA'; Runtime = 'CUDA 13.4 Developer Preview' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'paired app/cudart tag/digests, N1X device, CUDA backend, GPU layers, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend CUDA -ReportPath "$env:TEMP\llama-cuda-arm64-report.json"'
+ }
+ @{
+ Id = 'llama-rocm-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'AMD'; DeviceFamily = 'ROCm 10 Windows gfx matrix'; Backend = 'ROCm'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Supported AMD GPU/gfx target'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'ROCm'; AmdGpuName = 'AMD Radeon RX 9070 XT'; AmdGfxTarget = 'gfx1201' }; Expected = @{ Backend = 'ROCm'; Runtime = 'ROCm 10.0' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, AMD device/gfx, ROCm backend, GPU layers, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend ROCm -ReportPath "$env:TEMP\llama-rocm-report.json"'
+ }
+ @{
+ Id = 'llama-sycl-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'Intel'; DeviceFamily = 'validated Intel GPU families'; Backend = 'SYCL'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Supported Intel GPU and installed compatible driver'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'SYCL'; IntelGpuName = 'Intel Arc B580 Graphics' }; Expected = @{ Backend = 'SYCL'; Runtime = 'SYCL' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, Intel device, SYCL backend, GPU layers, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend SYCL -ReportPath "$env:TEMP\llama-sycl-report.json"'
+ }
+ @{
+ Id = 'llama-openvino-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'Intel/general'; DeviceFamily = 'OpenVINO backend device'; Backend = 'OpenVINO'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Windows x64 and backend-visible device'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'OpenVINO'; IntelGpuName = 'Intel Arc B580 Graphics' }; Expected = @{ Backend = 'OpenVINO'; Runtime = 'OpenVINO 2026.3.1' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, actual OpenVINO device/backend, GPU layers, inference; no NPU claim'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend OpenVINO -ReportPath "$env:TEMP\llama-openvino-report.json"'
+ }
+ @{
+ Id = 'llama-vulkan-x64'
+ Workload = 'llama.cpp'; Architecture = 'X64'; Vendor = 'Cross-vendor'; DeviceFamily = 'Vulkan-capable GPU'; Backend = 'Vulkan'
+ Status = 'implemented-supported'; Maturity = 'rolling-fallback'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Vulkan loader and usable display adapter'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; Backend = 'Vulkan'; HasVulkan = $true; VulkanGpuName = 'Generic Vulkan GPU' }; Expected = @{ Backend = 'Vulkan'; Runtime = 'Vulkan' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, Vulkan backend/device, GPU layers, fallback status, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend Vulkan -ReportPath "$env:TEMP\llama-vulkan-report.json"'
+ }
+ @{
+ Id = 'llama-opencl-adreno-arm64'
+ Workload = 'llama.cpp'; Architecture = 'Arm64'; Vendor = 'Qualcomm'; DeviceFamily = 'Adreno'; Backend = 'OpenCL'
+ Status = 'implemented-supported'; Maturity = 'rolling'; Acquisition = @('component:LlamaCppRolling')
+ Prerequisites = 'Qualcomm/Adreno adapter and Windows OpenCL loader'
+ Resolver = 'Resolve-LlamaCppInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64'; Backend = 'OpenCL'; QualcommGpuName = 'Qualcomm Adreno X1-85 GPU'; HasOpenCl = $true }; Expected = @{ Backend = 'OpenCL'; Runtime = 'OpenCL Adreno' }
+ ProbePath = 'src/tests/llama.cpp/probe.ps1'; ReportEvidence = 'official asset tag/digest, Adreno device, OpenCL backend, GPU layers, inference'
+ PartnerCommand = '.\src\Workloads\llama.cpp\install.ps1 -Backend OpenCL -ReportPath "$env:TEMP\llama-adreno-report.json"'
+ }
+ @{
+ Id = 'foundry-source-managed-x64'
+ Workload = 'foundry'; Architecture = 'X64'; Vendor = 'Source-managed'; DeviceFamily = 'WinML provider selected by Foundry'; Backend = 'WinML'
+ Status = 'source-managed'; Maturity = 'qualified-preview'; Acquisition = @('component:FoundryLocal')
+ Prerequisites = 'Windows 11 build 26100+'
+ Resolver = 'Resolve-FoundryInstallPlan'; ResolverArguments = @{ Architecture = 'X64'; WindowsBuild = 26100 }; Expected = @{ PackageId = 'Microsoft.FoundryLocal'; RequiresCuda = $false }
+ ProbePath = 'src/Workloads/foundry/install.ps1'; ReportEvidence = 'resolved package variant, model inference, actual execution provider/device, truthful CPU fallback'
+ PartnerCommand = '.\src\Workloads\foundry\install.ps1 -ReportPath "$env:TEMP\foundry-x64-report.json"'
+ }
+ @{
+ Id = 'foundry-source-managed-arm64'
+ Workload = 'foundry'; Architecture = 'Arm64'; Vendor = 'Source-managed'; DeviceFamily = 'WinML provider selected by Foundry'; Backend = 'WinML'
+ Status = 'source-managed'; Maturity = 'qualified-preview'; Acquisition = @('component:FoundryLocal')
+ Prerequisites = 'Windows 11 build 26100+'
+ Resolver = 'Resolve-FoundryInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64'; WindowsBuild = 26100 }; Expected = @{ PackageId = 'Microsoft.FoundryLocal'; RequiresCuda = $false }
+ ProbePath = 'src/Workloads/foundry/install.ps1'; ReportEvidence = 'resolved package variant, model inference, actual execution provider/device, truthful CPU fallback'
+ PartnerCommand = '.\src\Workloads\foundry\install.ps1 -ReportPath "$env:TEMP\foundry-arm64-report.json"'
+ }
+ @{
+ Id = 'ollama-source-managed-x64'
+ Workload = 'ollama'; Architecture = 'X64'; Vendor = 'Source-managed'; DeviceFamily = 'Ollama-selected CPU/GPU'; Backend = 'Ollama'
+ Status = 'source-managed'; Maturity = 'stable'; Acquisition = @('component:OllamaX64')
+ Prerequisites = 'Windows x64'
+ Resolver = 'Resolve-OllamaInstallPlan'; ResolverArguments = @{ Architecture = 'X64' }; Expected = @{ Method = 'WinGet'; PackageId = 'Ollama.Ollama' }
+ ProbePath = 'src/Workloads/ollama/install.ps1'; ReportEvidence = 'model digest/inference, actual process backend and CPU/GPU VRAM allocation; no forced vendor selector'
+ PartnerCommand = '.\src\Workloads\ollama\install.ps1 -ReportPath "$env:TEMP\ollama-x64-report.json"'
+ }
+ @{
+ Id = 'ollama-source-managed-arm64'
+ Workload = 'ollama'; Architecture = 'Arm64'; Vendor = 'Source-managed'; DeviceFamily = 'Ollama-selected CPU/NVIDIA'; Backend = 'Ollama'
+ Status = 'source-managed'; Maturity = 'stable-direct'; Acquisition = @('component:OllamaArm64')
+ Prerequisites = 'Windows ARM64'
+ Resolver = 'Resolve-OllamaInstallPlan'; ResolverArguments = @{ Architecture = 'Arm64' }; Expected = @{ Method = 'GitHubRelease'; LaunchMode = 'ManagedStartup'; InstallType = 'native-arm64-managed-archive' }
+ ProbePath = 'src/Workloads/ollama/install.ps1'; ReportEvidence = 'model digest/inference, actual process backend and CPU/GPU VRAM allocation; no Adreno claim'
+ PartnerCommand = '.\src\Workloads\ollama\install.ps1 -ReportPath "$env:TEMP\ollama-arm64-report.json"'
+ }
+ @{
+ Id = 'rocm-arm64-unavailable'; Workload = 'rocm'; Architecture = 'Arm64'; Vendor = 'AMD'; DeviceFamily = 'GPU'; Backend = 'HIP'
+ Status = 'upstream-unavailable'; Blocker = 'AMD does not publish the ROCm Core SDK or PyTorch ROCm runtime for native Windows ARM64.'
+ }
+ @{
+ Id = 'pytorch-rocm-arm64-unavailable'; Workload = 'pytorch'; Architecture = 'Arm64'; Vendor = 'AMD'; DeviceFamily = 'GPU'; Backend = 'ROCm'
+ Status = 'upstream-unavailable'; Blocker = 'AMD does not publish the PyTorch ROCm runtime tuple for native Windows ARM64.'
+ }
+ @{
+ Id = 'intel-ai-arm64-unavailable'; Workload = 'intel-ai'; Architecture = 'Arm64'; Vendor = 'Intel'; DeviceFamily = 'GPU/NPU'; Backend = 'OpenVINO/SYCL'
+ Status = 'upstream-unavailable'; Blocker = 'Intel OpenVINO/oneAPI Windows artifacts used by this flow are native x64; no equivalent native Windows ARM64 tuple is published.'
+ }
+ @{
+ Id = 'pytorch-xpu-arm64-unavailable'; Workload = 'pytorch'; Architecture = 'Arm64'; Vendor = 'Intel'; DeviceFamily = 'GPU'; Backend = 'XPU'
+ Status = 'upstream-unavailable'; Blocker = 'PyTorch does not publish native Windows ARM64 XPU wheels.'
+ }
+ @{
+ Id = 'pytorch-qualcomm-arm64-unavailable'; Workload = 'pytorch'; Architecture = 'Arm64'; Vendor = 'Qualcomm'; DeviceFamily = 'Adreno'; Backend = 'Qualcomm accelerator'
+ Status = 'upstream-unavailable'; Blocker = 'PyTorch does not publish a native Windows Qualcomm/Adreno accelerator backend.'
+ }
+ @{
+ Id = 'triton-amd-windows-unavailable'; Workload = 'pytorch-triton'; Architecture = 'X64'; Vendor = 'AMD'; DeviceFamily = 'ROCm GPU'; Backend = 'ROCm'
+ Status = 'upstream-unavailable'; Blocker = 'No supported native Windows AMD Triton package is published.'
+ }
+ @{
+ Id = 'generic-arm-gpu-toolkit-unavailable'; Workload = 'vendor-toolkit'; Architecture = 'Arm64'; Vendor = 'Generic'; DeviceFamily = 'ARM GPU'; Backend = 'Generic'
+ Status = 'upstream-unavailable'; Blocker = 'There is no standalone generic ARM GPU toolkit with authoritative Windows artifacts; use a published vendor backend or source-managed WinML provider.'
+ }
+ @{
+ Id = 'amd-ryzen-ai-npu-unavailable'; Workload = 'vendor-toolkit'; Architecture = 'X64'; Vendor = 'AMD'; DeviceFamily = 'Ryzen AI NPU'; Backend = 'NPU'
+ Status = 'upstream-unavailable'; Blocker = 'ROCm is the AMD GPU/HIP stack. This repository does not implement or claim an authoritative AMD Ryzen AI NPU runtime.'
+ }
+ @{
+ Id = 'other-windows-gpu-unavailable'; Workload = 'vendor-toolkit'; Architecture = 'X64/Arm64'; Vendor = 'Other'; DeviceFamily = 'Mali or unlisted GPU'; Backend = 'Unpublished'
+ Status = 'upstream-unavailable'; Blocker = 'No authoritative supported Windows artifact is implemented for this vendor/backend combination.'
+ }
+ )
+}
diff --git a/src/Workloads/_common/ai-report.ps1 b/src/Workloads/_common/ai-report.ps1
new file mode 100644
index 00000000..6d096b01
--- /dev/null
+++ b/src/Workloads/_common/ai-report.ps1
@@ -0,0 +1,141 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+function Get-AiGpuInventory {
+ $controllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.PNPDeviceID -match '^PCI\\' -and $_.Name -notmatch '(?i)\bNPU\b' })
+ return @($controllers | ForEach-Object {
+ $vendor = if ($_.PNPDeviceID -match 'VEN_10DE' -or $_.Name -match 'NVIDIA') {
+ 'NVIDIA'
+ } elseif ($_.PNPDeviceID -match 'VEN_1002' -or $_.Name -match 'AMD|Radeon') {
+ 'AMD'
+ } elseif ($_.PNPDeviceID -match 'VEN_8086' -or $_.Name -match 'Intel') {
+ 'Intel'
+ } elseif ($_.PNPDeviceID -match 'VEN_17CB' -or $_.Name -match 'Qualcomm|Adreno') {
+ 'Qualcomm'
+ } else {
+ 'Unknown'
+ }
+ [ordered]@{
+ vendor = $vendor
+ name = $_.Name
+ pnpDeviceId = $_.PNPDeviceID
+ driverVersion = $_.DriverVersion
+ }
+ })
+}
+
+function Get-AiNpuInventory {
+ return @(Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
+ Where-Object { $_.FriendlyName -match '(?i)(\bNPU\b|AI Boost|Neural Processing)' } |
+ ForEach-Object {
+ [ordered]@{
+ name = $_.FriendlyName
+ instanceId = $_.InstanceId
+ status = $_.Status
+ }
+ })
+}
+
+function New-AiWorkloadReport {
+ param(
+ [Parameter(Mandatory)] [string] $Id,
+ [hashtable] $Request = @{}
+ )
+ $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
+ return [ordered]@{
+ schemaVersion = 1
+ workload = $Id
+ startedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
+ completedAtUtc = $null
+ host = [ordered]@{
+ os = $os.Caption
+ osVersion = $os.Version
+ osBuild = $os.BuildNumber
+ architecture = (Get-DevConfigArchitecture)
+ powershell = $PSVersionTable.PSVersion.ToString()
+ gpus = @(Get-AiGpuInventory)
+ npus = @(Get-AiNpuInventory)
+ }
+ request = $Request
+ acquisitions = [System.Collections.ArrayList]::new()
+ phases = [System.Collections.ArrayList]::new()
+ acceptance = [ordered]@{}
+ result = [ordered]@{
+ ready = $false
+ planOnly = [bool]$(if ($Request.ContainsKey('PlanOnly')) { $Request.PlanOnly } else { $false })
+ fallbackUsed = $false
+ warnings = [System.Collections.ArrayList]::new()
+ blockers = [System.Collections.ArrayList]::new()
+ }
+ }
+}
+
+function Add-AiReportAcquisition {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Report,
+ [Parameter(Mandatory)] $Entry
+ )
+ [void]$Report.acquisitions.Add($Entry)
+}
+
+function Add-AiReportPhase {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Report,
+ [Parameter(Mandatory)] [string] $Name,
+ [Parameter(Mandatory)] [string] $Status,
+ $Evidence = $null
+ )
+ [void]$Report.phases.Add([ordered]@{ name = $Name; status = $Status; evidence = $Evidence })
+}
+
+function Complete-AiWorkloadReport {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Report,
+ [Parameter(Mandatory)] [bool] $Ready,
+ [Parameter(Mandatory)] [string] $Path
+ )
+ $Report.completedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
+ $Report.result.ready = $Ready
+ Write-DevConfigTextFile -Path $Path -Content ($Report | ConvertTo-Json -Depth 20)
+ Write-Host "AI_REPORT: $Path"
+}
+
+function Write-AiFailureReport {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Report,
+ [Parameter(Mandatory)] [string] $Path,
+ [Parameter(Mandatory)] $ErrorRecord
+ )
+ [void]$Report.result.blockers.Add($ErrorRecord.Exception.Message)
+ Complete-AiWorkloadReport -Report $Report -Ready $false -Path $Path
+}
+
+function Get-AiDefaultReportPath {
+ param([Parameter(Mandatory)] [string] $Id)
+ return Join-Path $env:LOCALAPPDATA "DevConfig\reports\$Id-latest.json"
+}
+
+function Get-AiCatalog {
+ return Get-AiCatalogData
+}
+
+function Get-AiCatalogValue {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Entry,
+ [Parameter(Mandatory)] [string] $Name
+ )
+ if ($Entry.ContainsKey($Name)) {
+ return $Entry[$Name]
+ }
+ return $null
+}
+
+function Set-AiAcquisitionAction {
+ param(
+ [Parameter(Mandatory)] [hashtable] $Report,
+ [Parameter(Mandatory)] [int] $Index,
+ [Parameter(Mandatory)] [string] $Action
+ )
+ $Report.acquisitions[$Index].action = $Action
+}
diff --git a/src/Workloads/_common/ai-support.ps1 b/src/Workloads/_common/ai-support.ps1
new file mode 100644
index 00000000..f198eade
--- /dev/null
+++ b/src/Workloads/_common/ai-support.ps1
@@ -0,0 +1,2466 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+function Get-AiCatalogData {
+ Import-Module Microsoft.PowerShell.Utility -ErrorAction Stop
+ return Microsoft.PowerShell.Utility\Import-PowerShellDataFile `
+ -LiteralPath (Join-Path $PSScriptRoot 'ai-catalog.psd1')
+}
+
+function Get-AiCapabilityMatrix {
+ return @((Get-AiCatalogData).CapabilityMatrix)
+}
+
+function Resolve-AiCapabilityCell {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Id)
+
+ $cell = Get-AiCapabilityMatrix | Where-Object { $_.Id -eq $Id } | Select-Object -First 1
+ if (-not $cell) {
+ throw "Unknown AI capability cell '$Id'."
+ }
+ if ($cell.Status -eq 'upstream-unavailable') {
+ throw [string]$cell.Blocker
+ }
+ $resolver = Get-Command -Name $cell.Resolver -CommandType Function -ErrorAction SilentlyContinue
+ if (-not $resolver) {
+ throw "Capability '$Id' references missing resolver '$($cell.Resolver)'."
+ }
+ $arguments = @{}
+ foreach ($entry in $cell.ResolverArguments.GetEnumerator()) {
+ $arguments[$entry.Key] = $entry.Value
+ }
+ $plan = & $resolver.Name @arguments
+ foreach ($entry in $cell.Expected.GetEnumerator()) {
+ $property = $plan.PSObject.Properties[$entry.Key]
+ if (-not $property) {
+ throw "Capability '$Id' resolver result did not contain expected field '$($entry.Key)'."
+ }
+ if ($property.Value -ne $entry.Value) {
+ throw "Capability '$Id' expected $($entry.Key)='$($entry.Value)' but resolved '$($property.Value)'."
+ }
+ }
+ return $plan
+}
+
+function Enable-AiUtf8Console {
+ try {
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [Console]::InputEncoding = $utf8NoBom
+ [Console]::OutputEncoding = $utf8NoBom
+ $global:OutputEncoding = $utf8NoBom
+ } catch {
+ Write-Verbose "Could not force UTF-8 console encoding: $($_.Exception.Message)"
+ }
+ try {
+ $null = & $env:ComSpec /d /c 'chcp 65001 >nul 2>&1'
+ } catch {
+ Write-Verbose "Could not set the console code page to UTF-8: $($_.Exception.Message)"
+ }
+}
+
+function ConvertFrom-AiPrefixedJsonArray {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Text)
+
+ $normalized = $Text -replace "`r`n", "`n" -replace "`r", "`n"
+ $lines = @($normalized -split "`n")
+ for ($index = 0; $index -lt $lines.Count; $index++) {
+ if (-not $lines[$index].TrimStart().StartsWith('[')) {
+ continue
+ }
+ $jsonText = ($lines[$index..($lines.Count - 1)] -join "`n").Trim()
+ $diagnostics = if ($index -gt 0) {
+ ($lines[0..($index - 1)] -join "`n").Trim()
+ } else {
+ ''
+ }
+ try {
+ return ConvertFrom-AiJsonArrayWithDiagnostics -Json $jsonText -Diagnostics $diagnostics
+ } catch {
+ continue
+ }
+ }
+ throw 'No valid JSON array was found after the diagnostic output.'
+}
+
+function Get-AiWindowsPathFromOutput {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Text)
+
+ $withoutAnsi = [regex]::Replace($Text, "$([char]27)\[[0-?]*[ -/]*[@-~]", '')
+ $match = [regex]::Match($withoutAnsi, '(?im)([A-Za-z]:\\[^\r\n]+)')
+ if (-not $match.Success) {
+ throw "No absolute Windows path was found in output: $Text"
+ }
+ $path = $match.Groups[1].Value.Trim().Trim('"', "'", ' ')
+ if (-not [System.IO.Path]::IsPathRooted($path)) {
+ throw "Output did not contain a rooted Windows path: $Text"
+ }
+ return $path
+}
+
+function ConvertTo-AiNativeCommandLineArgument {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [AllowEmptyString()] [string] $Argument
+ )
+
+ if ($Argument.Length -gt 0 -and $Argument -notmatch '[\s"]') {
+ return $Argument
+ }
+
+ $quoted = [System.Text.StringBuilder]::new()
+ [void]$quoted.Append('"')
+ $backslashes = 0
+ foreach ($character in $Argument.ToCharArray()) {
+ if ($character -eq [char]0x5c) {
+ $backslashes++
+ continue
+ }
+ if ($character -eq '"') {
+ [void]$quoted.Append([char]0x5c, ($backslashes * 2) + 1)
+ [void]$quoted.Append('"')
+ $backslashes = 0
+ continue
+ }
+ if ($backslashes -gt 0) {
+ [void]$quoted.Append([char]0x5c, $backslashes)
+ $backslashes = 0
+ }
+ [void]$quoted.Append($character)
+ }
+ if ($backslashes -gt 0) {
+ [void]$quoted.Append([char]0x5c, $backslashes * 2)
+ }
+ [void]$quoted.Append('"')
+ return $quoted.ToString()
+}
+
+function Invoke-AiNativeCommandSeparated {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $FilePath,
+ [string[]] $Arguments = @(),
+ [ValidateRange(1, 2147483)] [int] $TimeoutSeconds = 600
+ )
+ $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
+ $startInfo.FileName = $FilePath
+ $startInfo.UseShellExecute = $false
+ $startInfo.CreateNoWindow = $true
+ $startInfo.RedirectStandardOutput = $true
+ $startInfo.RedirectStandardError = $true
+ $startInfo.Arguments = @($Arguments | ForEach-Object {
+ ConvertTo-AiNativeCommandLineArgument -Argument $_
+ }) -join ' '
+ $process = [System.Diagnostics.Process]::new()
+ $process.StartInfo = $startInfo
+ $started = $false
+ try {
+ if (-not $process.Start()) {
+ throw "Native command '$FilePath' did not start."
+ }
+ $started = $true
+ $stdoutTask = $process.StandardOutput.ReadToEndAsync()
+ $stderrTask = $process.StandardError.ReadToEndAsync()
+ if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
+ try { $process.Kill() } catch {
+ Write-Verbose "Could not stop timed-out native command '$FilePath': $($_.Exception.Message)"
+ }
+ throw [System.TimeoutException]::new(
+ "Native command '$FilePath' did not finish within $TimeoutSeconds seconds, so it was stopped."
+ )
+ }
+ $process.WaitForExit()
+ return [pscustomobject]@{
+ ExitCode = $process.ExitCode
+ StandardOutput = $stdoutTask.GetAwaiter().GetResult()
+ StandardError = $stderrTask.GetAwaiter().GetResult()
+ }
+ } finally {
+ if ($started -and -not $process.HasExited) {
+ try { $process.Kill() } catch {
+ Write-Verbose "Could not stop native command '$FilePath': $($_.Exception.Message)"
+ }
+ }
+ $process.Dispose()
+ }
+}
+
+function ConvertFrom-AiJsonArrayWithDiagnostics {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Json,
+ [AllowEmptyString()] [string] $Diagnostics = ''
+ )
+ $jsonText = $Json.Trim()
+ $jsonRepaired = $jsonText.StartsWith('[') -and $jsonText.EndsWith('}')
+ if ($jsonRepaired) {
+ $jsonText = "$jsonText`n]"
+ }
+ try {
+ $parsedData = $jsonText | ConvertFrom-Json -ErrorAction Stop
+ $data = @($parsedData)
+ } catch {
+ throw "llama-bench stdout was not a valid JSON array: $($_.Exception.Message)"
+ }
+ if ($data.Count -eq 0) {
+ throw 'llama-bench returned an empty JSON array.'
+ }
+ $diagnosticText = $Diagnostics.Trim()
+ if ($jsonRepaired) {
+ $repairDiagnostic = 'LLAMA_BENCH_JSON_REPAIRED: appended the missing closing array bracket.'
+ $diagnosticText = @($repairDiagnostic, $diagnosticText | Where-Object { $_ }) -join "`n"
+ }
+ return [pscustomobject]@{
+ Data = $data
+ Json = $jsonText
+ Diagnostics = $diagnosticText
+ JsonRepaired = $jsonRepaired
+ }
+}
+
+function ConvertFrom-AiKeyedJsonLine {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Text,
+ [Parameter(Mandatory)] [string] $Prefix
+ )
+
+ $line = @($Text -split '\r?\n' | Where-Object { $_.StartsWith($Prefix) }) | Select-Object -Last 1
+ if (-not $line) {
+ throw "Output did not contain a '$Prefix' JSON record."
+ }
+ $json = $line.Substring($Prefix.Length)
+ try {
+ return $json | ConvertFrom-Json -ErrorAction Stop
+ } catch {
+ throw "The '$Prefix' record was not valid JSON: $json"
+ }
+}
+
+function Test-AiDeviceNameMatch {
+ [CmdletBinding()]
+ param(
+ [AllowNull()] [string] $Expected,
+ [AllowNull()] [string] $Actual
+ )
+ if (-not $Expected -or -not $Actual) { return $false }
+ $normalizedExpected = ($Expected -replace '\((TM|R)\)', '' -replace '[^A-Za-z0-9]+', ' ').Trim()
+ $normalizedActual = ($Actual -replace '\((TM|R)\)', '' -replace '[^A-Za-z0-9]+', ' ').Trim()
+ return $normalizedActual -eq $normalizedExpected -or
+ $normalizedActual.Contains($normalizedExpected) -or
+ $normalizedExpected.Contains($normalizedActual)
+}
+
+function Get-FoundryModelVariantEvidence {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [AllowEmptyString()] [string] $ModelInfo,
+ [AllowEmptyString()] [string] $ServerLogs = ''
+ )
+
+ if (-not $ModelInfo) { return $null }
+ $variants = [System.Collections.Generic.List[object]]::new()
+ $current = $null
+ $inVariantTable = $false
+ foreach ($line in @($ModelInfo -split '\r?\n')) {
+ if ($line -match '^\|\s*Variant\s*\|') {
+ $inVariantTable = $true
+ continue
+ }
+ if (-not $inVariantTable) { continue }
+ if ($line -match '^\+') {
+ if ($null -ne $current) {
+ [void]$variants.Add([pscustomobject]$current)
+ $current = $null
+ }
+ if ($variants.Count -gt 0) { break }
+ continue
+ }
+ if ($line -notmatch '^\|') { continue }
+ $columns = @($line -split '\|')
+ if ($columns.Count -lt 8) { continue }
+ $devicePart = $columns[3].Trim()
+ $providerPart = $columns[4].Trim()
+ $cachedPart = $columns[6].Trim()
+ if ($devicePart -and $devicePart -ne 'Device' -and $devicePart -notmatch '^-+$') {
+ if ($null -ne $current) {
+ [void]$variants.Add([pscustomobject]$current)
+ }
+ $current = [ordered]@{
+ Device = $devicePart
+ Provider = $providerPart
+ Cached = $cachedPart
+ }
+ } elseif ($null -ne $current -and $providerPart -and $providerPart -ne 'Provider') {
+ $current.Provider += $providerPart
+ }
+ }
+ if ($null -ne $current) {
+ [void]$variants.Add([pscustomobject]$current)
+ }
+ if ($variants.Count -eq 0) { return $null }
+
+ $loadedMatches = @([regex]::Matches(
+ $ServerLogs,
+ "(?im)Model\s+'?[^'\r\n]*-(gpu|cpu|npu):\d+'?\s*(?:\r?\n)?loaded successfully"
+ ))
+ $selected = $null
+ if ($loadedMatches.Count -gt 0) {
+ $loadedDevice = $loadedMatches[$loadedMatches.Count - 1].Groups[1].Value.ToUpperInvariant()
+ $selected = @($variants | Where-Object { $_.Device -ieq $loadedDevice }) | Select-Object -First 1
+ }
+ if ($null -eq $selected) {
+ $cachedMarker = [string][char]0x25CF
+ $selected = @($variants | Where-Object {
+ $_.Cached -eq $cachedMarker -or $_.Cached -match '(?i)^(yes|true)$'
+ }) | Select-Object -First 1
+ }
+ if ($null -eq $selected -and $variants.Count -eq 1) {
+ $selected = $variants[0]
+ }
+ return $selected
+}
+
+function Get-FoundryExecutionProviderEvidence {
+ [CmdletBinding()]
+ param(
+ [AllowEmptyString()] [string] $ModelInfo = '',
+ [AllowEmptyString()] [string] $ServerLogs = ''
+ )
+ $providerNames = @(
+ 'CUDAExecutionProvider',
+ 'NvTensorRTRTXExecutionProvider',
+ 'QNNExecutionProvider',
+ 'OpenVINOExecutionProvider',
+ 'VitisAIExecutionProvider',
+ 'MIGraphXExecutionProvider',
+ 'WebGPUExecutionProvider',
+ 'DmlExecutionProvider',
+ 'CPUExecutionProvider'
+ )
+ $providerPattern = '(?i)(' + ($providerNames -join '|') + ')'
+ $selectionMatches = @([regex]::Matches($ServerLogs, '(?im)Device:\s*([^,\r\n]+),\s*EPs:\s*([^\r\n]+)'))
+ if ($selectionMatches.Count -gt 0) {
+ $selection = $selectionMatches[$selectionMatches.Count - 1]
+ $selectedDevice = $selection.Groups[1].Value.Trim()
+ $providerText = $selection.Groups[2].Value
+ } elseif ($ServerLogs -match '(?im)Using\s+WebGPU\s+EP\s+for\s+model:') {
+ $selectedDevice = 'GPU'
+ $providerText = 'WebGPUExecutionProvider'
+ } else {
+ $variant = Get-FoundryModelVariantEvidence -ModelInfo $ModelInfo -ServerLogs $ServerLogs
+ if ($null -eq $variant -or -not $variant.Device -or -not $variant.Provider) {
+ throw 'Foundry inference succeeded, but neither the current inference logs nor the selected model variant identified its device and execution provider.'
+ }
+ $selectedDevice = $variant.Device
+ $providerText = $variant.Provider
+ }
+ $providers = @([regex]::Matches($providerText, $providerPattern) |
+ ForEach-Object {
+ $matchedProvider = $_.Groups[1].Value
+ @($providerNames | Where-Object { $_ -ieq $matchedProvider })[0]
+ } |
+ Select-Object -Unique)
+ if ($providers.Count -eq 0) {
+ throw "Foundry selection event for device '$selectedDevice' did not identify a supported execution provider."
+ }
+ return [pscustomobject]@{
+ SelectedDevice = $selectedDevice
+ SelectedProvider = $providers -join ','
+ ObservedProviders = $providers
+ CpuFallback = $providers.Count -eq 1 -and $providers[0] -ieq 'CPUExecutionProvider'
+ }
+}
+
+function Get-AiAppendedLogText {
+ [CmdletBinding()]
+ param(
+ [AllowEmptyString()] [string] $Before = '',
+ [AllowEmptyString()] [string] $After = ''
+ )
+ $beforeCounts = @{}
+ foreach ($line in @($Before -split '\r?\n' | Where-Object { $_ })) {
+ $beforeCounts[$line] = 1 + $(if ($beforeCounts.ContainsKey($line)) { $beforeCounts[$line] } else { 0 })
+ }
+ $appended = [System.Collections.Generic.List[string]]::new()
+ foreach ($line in @($After -split '\r?\n' | Where-Object { $_ })) {
+ if ($beforeCounts.ContainsKey($line) -and $beforeCounts[$line] -gt 0) {
+ $beforeCounts[$line]--
+ } else {
+ [void]$appended.Add($line)
+ }
+ }
+ return $appended -join "`n"
+}
+
+function Get-LlamaBenchmarkBackendEvidence {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [object[]] $Data,
+ [AllowEmptyString()] [string] $Diagnostics = '',
+ [Parameter(Mandatory)] [ValidateSet('CUDA', 'ROCm', 'SYCL', 'OpenVINO', 'Vulkan', 'OpenCL', 'CPU')] [string] $Backend,
+ [AllowNull()] [string] $ExpectedDeviceName,
+ [AllowNull()] [string] $RequestedDevice
+ )
+
+ $actualBackends = @($Data | ForEach-Object {
+ $property = $_.PSObject.Properties['backends']
+ if ($property) { @($property.Value) | ForEach-Object { [string]$_ } }
+ } | Where-Object { $_ } | Select-Object -Unique)
+ $requestedDevices = @($Data | ForEach-Object {
+ $property = $_.PSObject.Properties['devices']
+ if ($property) { [string]$property.Value }
+ } | Where-Object { $_ } | Select-Object -Unique)
+ $gpuInfo = @($Data | ForEach-Object {
+ $property = $_.PSObject.Properties['gpu_info']
+ if ($property) { [string]$property.Value }
+ } | Where-Object { $_ } | Select-Object -Unique)
+ $requestedGpuMeasurements = @($Data | Where-Object {
+ $property = $_.PSObject.Properties['n_gpu_layers']
+ $property -and [int]$property.Value -gt 0
+ })
+ $offloadMatches = @([regex]::Matches($Diagnostics, '(?im)offloaded\s+([0-9]+)\s*/\s*([0-9]+)\s+layers(?:\s+to\s+GPU)?'))
+ $actualOffloadedLayers = 0
+ $totalModelLayers = 0
+ foreach ($match in $offloadMatches) {
+ $actualOffloadedLayers = [math]::Max($actualOffloadedLayers, [int]$match.Groups[1].Value)
+ $totalModelLayers = [math]::Max($totalModelLayers, [int]$match.Groups[2].Value)
+ }
+ $evidenceText = (@($actualBackends) + @($gpuInfo) + @($Diagnostics)) -join "`n"
+ $backendPattern = switch ($Backend) {
+ 'CUDA' { 'CUDA' }
+ 'ROCm' { 'ROCm|HIP' }
+ 'SYCL' { 'SYCL' }
+ 'OpenVINO' { 'OpenVINO' }
+ 'Vulkan' { 'Vulkan' }
+ 'OpenCL' { 'OpenCL' }
+ 'CPU' { 'CPU' }
+ }
+ if (($actualBackends -join "`n") -notmatch $backendPattern) {
+ throw "llama-bench did not identify the selected $Backend backend. Actual backends: $($actualBackends -join ', ')."
+ }
+ if ($Backend -ne 'CPU' -and $gpuInfo.Count -eq 0) {
+ throw "llama-bench identified $Backend but did not provide physical device evidence in gpu_info."
+ }
+ if ($Backend -eq 'CPU') {
+ if ($actualOffloadedLayers -gt 0) {
+ throw "llama-bench offloaded $actualOffloadedLayers layers while the CPU backend was selected."
+ }
+ } elseif ($actualOffloadedLayers -le 0) {
+ throw "llama-bench identified $Backend but diagnostics did not prove any layers were actually offloaded."
+ }
+ $vendorPattern = switch ($Backend) {
+ 'CUDA' { 'NVIDIA|CUDA' }
+ 'ROCm' { 'AMD|Radeon|ROCm|HIP' }
+ 'SYCL' { 'Intel|SYCL' }
+ 'OpenVINO' { 'OpenVINO' }
+ 'Vulkan' { 'Vulkan' }
+ 'OpenCL' { 'Qualcomm|Adreno|OpenCL' }
+ 'CPU' { 'CPU' }
+ }
+ if ($evidenceText -notmatch $vendorPattern) {
+ throw "llama-bench did not report device evidence for the selected $Backend backend."
+ }
+ if ($ExpectedDeviceName -and $Backend -ne 'CPU' -and $ExpectedDeviceName -ne 'OpenVINO-selected device') {
+ if (-not (Test-AiDeviceNameMatch -Expected $ExpectedDeviceName -Actual $evidenceText)) {
+ throw "llama-bench selected $Backend but did not identify the expected device '$ExpectedDeviceName'."
+ }
+ }
+ if ($RequestedDevice -and $Backend -ne 'CPU') {
+ $matchingRequestedDevices = @($requestedDevices | Where-Object { $_ -ieq $RequestedDevice })
+ if ($matchingRequestedDevices.Count -eq 0) {
+ throw "llama-bench structured devices '$($requestedDevices -join ',')' did not match requested selector '$RequestedDevice'."
+ }
+ $selectorPattern = "(?im)using device\s+$([regex]::Escape($RequestedDevice))\b|dev\s*=\s*$([regex]::Escape($RequestedDevice))\b"
+ if ($Diagnostics -notmatch $selectorPattern) {
+ throw "llama-bench did not prove that requested device selector '$RequestedDevice' was used."
+ }
+ }
+
+ return [pscustomobject]@{
+ Backend = $Backend
+ ActualBackends = $actualBackends
+ RequestedDevices = $requestedDevices
+ GpuInfo = $gpuInfo
+ ExpectedDevice = $ExpectedDeviceName
+ RequestedDevice = $RequestedDevice
+ RequestedGpuLayerMeasurements = $requestedGpuMeasurements.Count
+ ActualOffloadedLayers = $actualOffloadedLayers
+ TotalModelLayers = $totalModelLayers
+ HardwareAccelerated = $Backend -ne 'CPU' -and $actualOffloadedLayers -gt 0
+ }
+}
+
+function Get-DevConfigArchitecture {
+ [CmdletBinding()]
+ param([ValidateSet('', 'X64', 'Arm64')] [string] $Override = '')
+
+ if ($Override) {
+ return $Override
+ }
+
+ $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
+ switch ($architecture) {
+ 'X64' { return 'X64' }
+ 'Arm64' { return 'Arm64' }
+ default { throw "Unsupported Windows architecture '$architecture'. Supported architectures: X64, Arm64." }
+ }
+}
+
+function Assert-DevConfigArchitecture {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Architecture,
+ [Parameter(Mandatory)] [string[]] $Supported,
+ [Parameter(Mandatory)] [string] $Component
+ )
+
+ if ($Architecture -notin $Supported) {
+ throw "$Component does not publish a compatible Windows artifact for $Architecture. Supported architectures: $($Supported -join ', ')."
+ }
+}
+
+function Get-WindowsBuildNumber {
+ [CmdletBinding()]
+ param()
+
+ return [Environment]::OSVersion.Version.Build
+}
+
+function Resolve-CudaInstallPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [int] $WindowsBuild = 26100
+ )
+
+ if ($Architecture -eq 'X64') {
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ Method = 'WinGet'
+ PackageId = 'Nvidia.CUDA'
+ ToolkitVersion = $null
+ Preview = $false
+ InstallerUrl = $null
+ InstallerSha256 = $null
+ }
+ }
+
+ if ($WindowsBuild -lt 22000) {
+ throw "CUDA 13.4 Developer Preview for Windows ARM64 requires Windows 11; detected build $WindowsBuild."
+ }
+
+ $catalog = (Get-AiCatalogData).Components.CudaArm64
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ Method = 'NvidiaInstaller'
+ InstallerIdentity = $catalog.Artifact
+ ToolkitVersion = $catalog.Version.Substring(0, 4)
+ Preview = $true
+ InstallerUrl = $catalog.Uri
+ InstallerSha256 = $catalog.Sha256
+ }
+}
+
+function Resolve-FoundryInstallPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [Parameter(Mandatory)] [int] $WindowsBuild
+ )
+
+ if ($WindowsBuild -lt 26100) {
+ throw "Foundry Local's Windows/WinML path requires Windows 11 24H2 (build 26100) or later; detected build $WindowsBuild."
+ }
+
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ PackageId = 'Microsoft.FoundryLocal'
+ RequiresCuda = $false
+ }
+}
+
+function Resolve-LlamaCppInstallPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [ValidateSet('Auto', 'CUDA', 'ROCm', 'SYCL', 'OpenVINO', 'Vulkan', 'OpenCL', 'CPU')] [string] $Backend = 'Auto',
+ [bool] $HasNvidia = $false,
+ [version] $DriverVersion = [version]'0.0',
+ [version] $ComputeCapability = [version]'0.0',
+ [string] $NvidiaGpuName,
+ [string] $AmdGpuName,
+ [string] $AmdGfxTarget,
+ [string] $IntelGpuName,
+ [string] $QualcommGpuName,
+ [bool] $HasOpenCl = $false,
+ [bool] $HasVulkan = $false,
+ [string] $VulkanGpuName
+ )
+
+ $catalog = (Get-AiCatalogData).Components.LlamaCppRolling
+ $assets = $catalog.BackendAssets
+ $cudaAsset = $null
+ if ($HasNvidia) {
+ if ($Architecture -eq 'Arm64') {
+ if ($DriverVersion -ge [version]'616.0' -and $ComputeCapability.Major -ge 12) {
+ $cudaAsset = $assets.Cuda134Arm64
+ }
+ } elseif ($DriverVersion -ge [version]'580.0' -and $ComputeCapability -ge [version]'7.5') {
+ $cudaAsset = $assets.Cuda133X64
+ } elseif ($DriverVersion -ge [version]'551.61' -and
+ $ComputeCapability -ge [version]'5.0' -and
+ $ComputeCapability.Major -lt 10) {
+ $cudaAsset = $assets.Cuda124X64
+ }
+ }
+ $rocmSupported = $Architecture -eq 'X64' -and [bool]$AmdGpuName -and [bool]$AmdGfxTarget
+ $syclSupported = $Architecture -eq 'X64' -and (Test-IntelXpuGpuSupported -GpuName $IntelGpuName)
+ $openClSupported = $Architecture -eq 'Arm64' -and [bool]$QualcommGpuName -and $HasOpenCl
+
+ $selectedBackend = if ($Backend -eq 'Auto') {
+ if ($cudaAsset) {
+ 'CUDA'
+ } elseif ($rocmSupported) {
+ 'ROCm'
+ } elseif ($syclSupported) {
+ 'SYCL'
+ } elseif ($openClSupported) {
+ 'OpenCL'
+ } elseif ($Architecture -eq 'X64' -and $HasVulkan) {
+ 'Vulkan'
+ } else {
+ 'CPU'
+ }
+ } else {
+ $Backend
+ }
+
+ $selectedAsset = switch ($selectedBackend) {
+ 'CUDA' {
+ if (-not $cudaAsset) {
+ if ($Architecture -eq 'Arm64') {
+ throw 'llama.cpp CUDA on Windows ARM64 requires an RTX Spark-class NVIDIA GPU, compute capability 12.x, and driver branch 616 or newer.'
+ }
+ if ($HasNvidia -and $ComputeCapability.Major -ge 10 -and $DriverVersion -lt [version]'580.0') {
+ throw "llama.cpp CUDA 13.3 is required for NVIDIA compute capability $ComputeCapability, but driver $DriverVersion is below branch 580."
+ }
+ throw 'llama.cpp CUDA on Windows x64 requires an NVIDIA GPU with compute capability 5.0 or newer and driver 551.61 or newer.'
+ }
+ $cudaAsset
+ }
+ 'ROCm' {
+ if (-not $rocmSupported) {
+ throw "llama.cpp ROCm requires Windows x64 and an AMD GPU in the ROCm 10.0 Windows support matrix. Detected: '$AmdGpuName'."
+ }
+ $assets.Rocm10X64
+ }
+ 'SYCL' {
+ if (-not $syclSupported) {
+ throw "llama.cpp SYCL requires Windows x64 and a supported Intel GPU. Detected: '$IntelGpuName'."
+ }
+ $assets.SyclX64
+ }
+ 'OpenVINO' {
+ if ($Architecture -ne 'X64') {
+ throw 'llama.cpp OpenVINO is not published for native Windows ARM64.'
+ }
+ $assets.OpenVinoX64
+ }
+ 'Vulkan' {
+ if ($Architecture -ne 'X64') {
+ throw 'llama.cpp Vulkan is not selected on Windows ARM64; use CUDA, OpenCL, or CPU.'
+ }
+ if (-not $HasVulkan) {
+ throw 'llama.cpp Vulkan was requested, but no Vulkan loader and usable display adapter were detected.'
+ }
+ $assets.VulkanX64
+ }
+ 'OpenCL' {
+ if (-not $openClSupported) {
+ throw "llama.cpp OpenCL is published here only for Qualcomm Adreno on Windows ARM64 with a working OpenCL loader. Detected: '$QualcommGpuName'; OpenCL loader: $HasOpenCl."
+ }
+ $assets.OpenClAdrenoArm64
+ }
+ 'CPU' {
+ if ($Architecture -eq 'Arm64') { $assets.CpuArm64 } else { $assets.CpuX64 }
+ }
+ }
+ $deviceName = switch ($selectedBackend) {
+ 'CUDA' { $NvidiaGpuName }
+ 'ROCm' { $AmdGpuName }
+ 'SYCL' { $IntelGpuName }
+ 'OpenVINO' { if ($IntelGpuName) { $IntelGpuName } else { 'OpenVINO-selected device' } }
+ 'Vulkan' { $VulkanGpuName }
+ 'OpenCL' { $QualcommGpuName }
+ default { 'CPU' }
+ }
+ return [pscustomobject]@{
+ Method = 'GitHubRelease'
+ PackageId = $null
+ AssetPatterns = @($selectedAsset.Patterns)
+ Backend = $selectedAsset.Backend
+ Runtime = $selectedAsset.Runtime
+ Vendor = $selectedAsset.Vendor
+ DeviceName = $deviceName
+ Maturity = $(if ($selectedAsset.ContainsKey('Maturity')) { $selectedAsset.Maturity } else { $catalog.Maturity })
+ VersionPolicy = $(if ($selectedAsset.ContainsKey('VersionPolicy')) { $selectedAsset.VersionPolicy } else { $catalog.VersionPolicy })
+ AmdGfxTarget = $(if ($selectedBackend -eq 'ROCm') { $AmdGfxTarget } else { $null })
+ }
+}
+
+function Get-QualcommGpuName {
+ $names = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.PNPDeviceID -match 'VEN_(17CB|QCOM)' -or $_.Name -match 'Qualcomm|Adreno' } |
+ ForEach-Object Name)
+ return $names | Sort-Object | Select-Object -First 1
+}
+
+function Get-VulkanGpuName {
+ $names = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.Name -and $_.Name -notmatch 'Microsoft Basic|Remote Display|Indirect Display' } |
+ ForEach-Object Name)
+ return $names | Sort-Object | Select-Object -First 1
+}
+
+function Test-AiOpenClRuntimeAvailable {
+ [CmdletBinding()]
+ param()
+ return Test-Path -LiteralPath (Join-Path $env:WINDIR 'System32\OpenCL.dll')
+}
+
+function Test-AiVulkanRuntimeAvailable {
+ [CmdletBinding()]
+ param([AllowNull()] [string] $GpuName)
+ if (-not $GpuName) { return $false }
+ return Test-Path -LiteralPath (Join-Path $env:WINDIR 'System32\vulkan-1.dll')
+}
+
+function Resolve-OllamaInstallPlan {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture)
+
+ if ($Architecture -eq 'X64') {
+ return [pscustomobject]@{
+ Method = 'WinGet'
+ PackageId = 'Ollama.Ollama'
+ LaunchMode = 'Desktop'
+ }
+ }
+
+ return [pscustomobject]@{
+ Method = 'GitHubRelease'
+ PackageId = $null
+ LaunchMode = 'ManagedStartup'
+ InstallType = 'native-arm64-managed-archive'
+ }
+}
+
+function Get-NvidiaGpu {
+ [CmdletBinding()]
+ param()
+
+ $controllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue)
+ return $controllers |
+ Where-Object { $_.PNPDeviceID -match 'VEN_10DE' -or $_.Name -match 'NVIDIA' } |
+ Select-Object -First 1
+}
+
+function Get-AmdGfxTarget {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $GpuName)
+
+ $normalizedGpuName = ($GpuName -replace '\((TM|R)\)', '' -replace '\s+', ' ').Trim()
+ $map = @(
+ @{ Pattern = 'R9700|R9600D|RX 9070'; Gfx = 'gfx1201' }
+ @{ Pattern = 'RX 9060|RX 9050'; Gfx = 'gfx1200' }
+ @{ Pattern = 'W7900|W7800|RX 7900'; Gfx = 'gfx1100' }
+ @{ Pattern = 'W7700|RX 7800|RX 7700'; Gfx = 'gfx1101' }
+ @{ Pattern = 'RX (7600|7650)'; Gfx = 'gfx1102' }
+ @{ Pattern = 'Ryzen AI Max|Radeon 8060S'; Gfx = 'gfx1151' }
+ @{ Pattern = 'Ryzen AI 9.*(475|470|375|370|465|365)|Radeon (890M|880M)'; Gfx = 'gfx1150' }
+ @{ Pattern = 'Ryzen AI (7|5).*(450|350|345|440|340|330)|Radeon 860M'; Gfx = 'gfx1152' }
+ @{ Pattern = 'Ryzen AI (7|5).*(445|435|430)|Radeon 840M'; Gfx = 'gfx1153' }
+ @{ Pattern = 'Ryzen (9 270|7 (260|250)|5 (240|230|220)|3 210)|Radeon (780M|760M|740M)'; Gfx = 'gfx1103' }
+ )
+ $entry = $map | Where-Object { $normalizedGpuName -match $_.Pattern } | Select-Object -First 1
+ if (-not $entry) {
+ return $null
+ }
+ return $entry.Gfx
+}
+
+function Get-AiDetectedVendor {
+ $controllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue)
+ if ($controllers | Where-Object { $_.PNPDeviceID -match 'VEN_10DE' -or $_.Name -match 'NVIDIA' }) { return 'NVIDIA' }
+ if ($controllers | Where-Object { $_.PNPDeviceID -match 'VEN_1002' -or $_.Name -match 'AMD|Radeon' }) { return 'AMD' }
+ if ($controllers | Where-Object { $_.PNPDeviceID -match 'VEN_8086' -or $_.Name -match 'Intel' }) { return 'Intel' }
+ return 'None'
+}
+
+function Get-AiProcessId {
+ [CmdletBinding()]
+ param([AllowNull()] $ProcessObject)
+ if ($null -eq $ProcessObject) { return $null }
+ foreach ($name in @('ProcessId', 'Id')) {
+ $property = $ProcessObject.PSObject.Properties[$name]
+ if ($property -and $null -ne $property.Value) {
+ return [int]$property.Value
+ }
+ }
+ return $null
+}
+
+function Get-AiProcessIds {
+ [CmdletBinding()]
+ param([AllowEmptyCollection()] [object[]] $ProcessObjects = @())
+ return @($ProcessObjects |
+ ForEach-Object { Get-AiProcessId -ProcessObject $_ } |
+ Where-Object { $null -ne $_ })
+}
+
+function Get-AiPeArchitecture {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Path)
+
+ $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
+ $reader = [IO.BinaryReader]::new($stream)
+ try {
+ if ($reader.ReadUInt16() -ne 0x5A4D) {
+ throw "'$Path' is not a PE executable."
+ }
+ $stream.Position = 0x3C
+ $peOffset = $reader.ReadInt32()
+ $stream.Position = $peOffset
+ if ($reader.ReadUInt32() -ne 0x00004550) {
+ throw "'$Path' has an invalid PE signature."
+ }
+ $machine = $reader.ReadUInt16()
+ switch ($machine) {
+ 43620 { return 'Arm64' }
+ 34404 { return 'X64' }
+ 332 { return 'X86' }
+ default { return ('Unknown-0x{0:X4}' -f $machine) }
+ }
+ } finally {
+ $reader.Dispose()
+ $stream.Dispose()
+ }
+}
+
+function Get-OllamaManagedPaths {
+ [CmdletBinding()]
+ param([string] $LocalAppData = $env:LOCALAPPDATA)
+
+ $installRoot = Join-Path $LocalAppData 'Programs\Ollama'
+ return [pscustomobject]@{
+ InstallRoot = $installRoot
+ Executable = Join-Path $installRoot 'ollama.exe'
+ InstallManifest = Join-Path $installRoot '.devconfig-install.json'
+ VersionMarker = '.devconfig-version'
+ CacheDirectory = Join-Path $LocalAppData 'DevConfig\ollama\asset-cache'
+ LegacyRoot = Join-Path $LocalAppData 'DevConfig\ollama\runtime'
+ StartupRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
+ StartupValueName = 'WindowsDeveloperConfig.Ollama'
+ }
+}
+
+function Get-OllamaStartupCommand {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Executable)
+ return '"' + $Executable.Replace('"', '\"') + '" serve'
+}
+
+function Get-OllamaManagedProcesses {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $InstallRoot)
+
+ $resolvedRoot = [IO.Path]::GetFullPath($InstallRoot).TrimEnd('\')
+ $rootPrefix = "$resolvedRoot\"
+ return @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
+ Where-Object {
+ if (-not $_.ExecutablePath) {
+ $false
+ } else {
+ $executablePath = [IO.Path]::GetFullPath([string]$_.ExecutablePath)
+ $executablePath.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)
+ }
+ })
+}
+
+function Stop-OllamaManagedProcesses {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $InstallRoot,
+ [int] $TimeoutSeconds = 30
+ )
+
+ $processes = @(Get-OllamaManagedProcesses -InstallRoot $InstallRoot)
+ $ids = @(Get-AiProcessIds -ProcessObjects $processes)
+ foreach ($processId in $ids) {
+ Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue
+ }
+ foreach ($processId in $ids) {
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+ while (Get-Process -Id $processId -ErrorAction SilentlyContinue) {
+ if ((Get-Date) -ge $deadline) {
+ throw "Managed Ollama process $processId did not exit within $TimeoutSeconds seconds."
+ }
+ Start-Sleep -Milliseconds 250
+ }
+ }
+ return $ids
+}
+
+function Set-OllamaStartupRegistration {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $RegistryPath,
+ [Parameter(Mandatory)] [string] $ValueName,
+ [Parameter(Mandatory)] [string] $Executable
+ )
+ New-Item -Path $RegistryPath -Force | Out-Null
+ $command = Get-OllamaStartupCommand -Executable $Executable
+ Set-ItemProperty -LiteralPath $RegistryPath -Name $ValueName -Value $command -Type String
+ return $command
+}
+
+function Remove-OllamaStartupRegistration {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $RegistryPath,
+ [Parameter(Mandatory)] [string] $ValueName
+ )
+ if (Test-Path -LiteralPath $RegistryPath) {
+ Remove-ItemProperty -LiteralPath $RegistryPath -Name $ValueName -ErrorAction SilentlyContinue
+ }
+}
+
+function Remove-OllamaManagedDirectory {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Path,
+ [ValidateRange(1, 30)] [int] $Attempts = 10
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) { return }
+ $lastError = $null
+ foreach ($attempt in 1..$Attempts) {
+ try {
+ Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop
+ if (-not (Test-Path -LiteralPath $Path)) { return }
+ } catch {
+ $lastError = $_
+ }
+ if ($attempt -lt $Attempts) { Start-Sleep -Milliseconds 500 }
+ }
+ throw "Could not remove the Dev Config-managed Ollama path '$Path' after $Attempts attempts: $($lastError.Exception.Message)"
+}
+
+function Remove-OllamaManagedInstallation {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] $Paths,
+ [Parameter(Mandatory)] [string] $ModelRoot,
+ [switch] $RemoveModels
+ )
+
+ $stopped = @(Stop-OllamaManagedProcesses -InstallRoot $Paths.InstallRoot)
+ Remove-OllamaStartupRegistration `
+ -RegistryPath $Paths.StartupRegistryPath `
+ -ValueName $Paths.StartupValueName
+ Remove-UserPathEntry -Path $Paths.InstallRoot
+ Remove-UserPathEntry -Path $Paths.LegacyRoot
+ Remove-OllamaManagedDirectory -Path $Paths.InstallRoot
+ Remove-OllamaManagedDirectory -Path $Paths.LegacyRoot
+ Remove-OllamaManagedDirectory -Path $Paths.CacheDirectory
+ if ($RemoveModels) {
+ Remove-OllamaManagedDirectory -Path $ModelRoot
+ }
+ return [pscustomobject]@{
+ StoppedManagedProcessIds = $stopped
+ StartupRemoved = $true
+ PathRemoved = $true
+ RuntimeRemoved = -not (Test-Path -LiteralPath $Paths.InstallRoot)
+ ModelsPreserved = -not $RemoveModels
+ }
+}
+
+function Get-AiFreeTcpPort {
+ $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+ try {
+ $listener.Start()
+ return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
+ } finally {
+ $listener.Stop()
+ }
+}
+
+function Get-AmdGpuName {
+ param([Nullable[int]] $DeviceIndex = $null)
+ $names = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.PNPDeviceID -match 'VEN_1002' -or $_.Name -match 'AMD|Radeon' } |
+ ForEach-Object Name)
+ if ($null -ne $DeviceIndex) {
+ if ($DeviceIndex -ge $names.Count) { return $null }
+ return $names[$DeviceIndex]
+ }
+ return Select-AmdGpuName -GpuNames $names
+}
+
+function Select-AmdGpuName {
+ [CmdletBinding()]
+ param([AllowEmptyCollection()] [string[]] $GpuNames = @())
+ $supported = $GpuNames | Where-Object { Get-AmdGfxTarget -GpuName $_ } | Sort-Object | Select-Object -First 1
+ if ($supported) { return $supported }
+ return $GpuNames | Sort-Object | Select-Object -First 1
+}
+
+function Resolve-RocmInstallPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [AllowNull()] [string] $GpuName
+ )
+
+ if ($Architecture -ne 'X64') {
+ throw 'AMD ROCm Core SDK 10.0 does not publish native Windows ARM64 packages.'
+ }
+ $gfx = if ($GpuName) { Get-AmdGfxTarget -GpuName $GpuName } else { $null }
+ if (-not $gfx) {
+ throw "No AMD GPU supported by the ROCm 10.0 Windows matrix was detected. Detected GPU: '$GpuName'."
+ }
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ GpuName = $GpuName
+ GfxTarget = $gfx
+ Requirement = "rocm[libraries,devel,device-$gfx]==10.0.0"
+ }
+}
+
+function Get-IntelGpuName {
+ param([Nullable[int]] $DeviceIndex = $null)
+ $names = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.PNPDeviceID -match 'VEN_8086' -or $_.Name -match 'Intel' } |
+ ForEach-Object Name)
+ if ($null -ne $DeviceIndex) {
+ if ($DeviceIndex -ge $names.Count) { return $null }
+ return $names[$DeviceIndex]
+ }
+ return Select-IntelGpuName -GpuNames $names
+}
+
+function Select-IntelGpuName {
+ [CmdletBinding()]
+ param([AllowEmptyCollection()] [string[]] $GpuNames = @())
+ $supported = $GpuNames | Where-Object { Test-IntelXpuGpuSupported -GpuName $_ } | Sort-Object | Select-Object -First 1
+ if ($supported) { return $supported }
+ return $GpuNames | Sort-Object | Select-Object -First 1
+}
+
+function Resolve-IntelAiPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [Parameter(Mandatory)] [ValidateSet('Auto', 'CPU', 'GPU', 'NPU')] [string] $Device,
+ [Parameter(Mandatory)] [ValidateSet('OpenVINO', 'SYCL', 'Full')] [string] $Profile,
+ [bool] $IntelGpuPresent = $false,
+ [bool] $IntelNpuPresent = $false
+ )
+
+ if ($Architecture -ne 'X64') {
+ throw 'Intel oneAPI, OpenVINO, PyTorch XPU, and Triton XPU do not publish native Windows ARM64 artifacts.'
+ }
+ $selectedDevice = if ($Profile -eq 'SYCL' -and $Device -eq 'Auto') {
+ 'GPU'
+ } elseif ($Device -eq 'Auto') {
+ if ($IntelNpuPresent) { 'NPU' } elseif ($IntelGpuPresent) { 'GPU' } else { 'CPU' }
+ } else { $Device }
+ if ($selectedDevice -eq 'GPU' -and -not $IntelGpuPresent) {
+ throw 'Intel GPU was requested, but no Intel display adapter was detected.'
+ }
+ if ($selectedDevice -eq 'NPU' -and -not $IntelNpuPresent) {
+ throw 'Intel NPU was requested, but no Intel AI Boost/NPU device was detected.'
+ }
+ if ($Profile -in @('SYCL', 'Full') -and -not $IntelGpuPresent) {
+ throw 'The SYCL profile requires a detected Intel GPU because its acceptance kernel uses gpu_selector_v.'
+ }
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ Device = $selectedDevice
+ Profile = $Profile
+ InstallOpenVino = $Profile -in @('OpenVINO', 'Full')
+ InstallOneApi = $Profile -in @('SYCL', 'Full')
+ }
+}
+
+function Test-IntelXpuGpuSupported {
+ [CmdletBinding()]
+ param([AllowNull()] [string] $GpuName)
+ if (-not $GpuName) { return $false }
+ $normalized = ($GpuName -replace '\((TM|R)\)', '' -replace '\s+', ' ').Trim()
+ return $normalized -match 'Arc.*(A|B)[0-9]|Arc.*(130V|140V)|Arc.*Graphics|Meteor Lake|Arrow Lake|Lunar Lake|Panther Lake|Core Ultra'
+}
+
+function Get-NvidiaDriverInfo {
+ [CmdletBinding()]
+ param([ValidateRange(0, 63)] [int] $DeviceIndex = 0)
+
+ if (-not (Get-Command nvidia-smi -ErrorAction SilentlyContinue)) {
+ return $null
+ }
+
+ $result = Invoke-DevConfigNativeCommand -FilePath 'nvidia-smi' -Arguments @(
+ '--query-gpu=name,driver_version,compute_cap', '--format=csv,noheader,nounits'
+ )
+ $allOutput = @($result.Output -split '\r?\n' | Where-Object { $_ })
+ if ($result.ExitCode -ne 0 -or $allOutput.Count -eq 0) {
+ return $null
+ }
+ if ($DeviceIndex -ge $allOutput.Count) {
+ throw "NVIDIA device index $DeviceIndex was requested, but nvidia-smi reported $($allOutput.Count) device(s)."
+ }
+ $output = $allOutput[$DeviceIndex]
+
+ $parts = @($output -split ',' | ForEach-Object { $_.Trim() })
+ if ($parts.Count -lt 3) {
+ throw "nvidia-smi returned an unexpected result: $output"
+ }
+
+ $driver = [version]$parts[1]
+ $compute = [version]$parts[2]
+ return [pscustomobject]@{
+ Name = $parts[0]
+ DriverVersion = $driver
+ DriverMajor = $driver.Major
+ ComputeCapability = $compute
+ }
+}
+
+function Get-CudaReadiness {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [bool] $ToolkitAvailable,
+ [Parameter(Mandatory)] [bool] $NvidiaGpuPresent,
+ [Parameter(Mandatory)] [bool] $DriverAvailable
+ )
+
+ return [pscustomobject]@{
+ ToolkitReady = $ToolkitAvailable
+ GpuReady = $NvidiaGpuPresent -and $DriverAvailable
+ Status = if (-not $ToolkitAvailable) {
+ 'ToolkitMissing'
+ } elseif (-not $NvidiaGpuPresent) {
+ 'ToolkitOnlyNoGpu'
+ } elseif (-not $DriverAvailable) {
+ 'ToolkitOnlyDriverUnavailable'
+ } else {
+ 'Ready'
+ }
+ }
+}
+
+function Resolve-PyTorchPlan {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [Parameter(Mandatory)] [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $Backend,
+ [Parameter(Mandatory)] [version] $PythonVersion,
+ [bool] $HasNvidia = $false,
+ [int] $DriverMajor = 0,
+ [version] $ComputeCapability = [version]'0.0',
+ [ValidateSet('NVIDIA', 'AMD', 'Intel', 'None')] [string] $GpuVendor = 'None',
+ [string] $GpuName,
+ [string] $AmdGpuName,
+ [string] $IntelGpuName,
+ [string] $AmdGfxTarget,
+ [bool] $HasAmd = $false,
+ [bool] $HasIntel = $false,
+ [switch] $SkipTriton
+ )
+
+ if ($Architecture -eq 'Arm64') {
+ if ($PythonVersion -lt [version]'3.11' -or $PythonVersion -ge [version]'3.14') {
+ throw "PyTorch 2.14 Windows ARM64 wheels require CPython 3.11-3.13; detected $PythonVersion."
+ }
+
+ $canUseCudaPreview = $PythonVersion.Major -eq 3 -and
+ $PythonVersion.Minor -eq 13 -and
+ $HasNvidia -and
+ $DriverMajor -ge 616 -and
+ $ComputeCapability.Major -ge 12
+ if ($Backend -in @('ROCm', 'XPU')) {
+ throw "$Backend is not published for native Windows ARM64."
+ }
+ if ($Backend -eq 'CUDA' -and -not $canUseCudaPreview) {
+ throw 'Windows ARM64 CUDA PyTorch requires CPython 3.13, an RTX Spark-class NVIDIA GPU (compute capability 12.x), and developer driver branch 616 or newer.'
+ }
+ if ($Backend -eq 'Auto' -and $HasNvidia -and -not $canUseCudaPreview) {
+ throw 'An NVIDIA GPU is present on Windows ARM64, but it does not meet the CUDA 13.4 PyTorch Developer Preview requirements. Use -Backend CPU to explicitly accept CPU-only PyTorch.'
+ }
+ if ($Backend -eq 'Auto' -and -not $canUseCudaPreview -and ($HasAmd -or $GpuVendor -eq 'AMD')) {
+ throw 'AMD ROCm PyTorch is not published for native Windows ARM64. Use -Backend CPU to explicitly accept CPU-only PyTorch.'
+ }
+ if ($Backend -eq 'Auto' -and -not $canUseCudaPreview -and ($HasIntel -or $GpuVendor -eq 'Intel')) {
+ throw 'Intel XPU PyTorch is not published for native Windows ARM64. Use -Backend CPU to explicitly accept CPU-only PyTorch.'
+ }
+ $selectedBackend = if ($Backend -eq 'Auto') {
+ if ($canUseCudaPreview) { 'CUDA' } else { 'CPU' }
+ } else {
+ $Backend
+ }
+ } else {
+ if ($PythonVersion -lt [version]'3.10' -or $PythonVersion -ge [version]'3.15') {
+ throw "PyTorch 2.14 Windows x64 wheels require CPython 3.10-3.14; detected $PythonVersion."
+ }
+
+ if ($Backend -eq 'CUDA' -and -not $HasNvidia) {
+ throw "CUDA backend was requested, but nvidia-smi did not report a usable NVIDIA GPU and driver."
+ }
+ if ($Backend -eq 'CUDA' -and $DriverMajor -lt 525) {
+ throw "CUDA backend was requested, but NVIDIA driver branch $DriverMajor is too old. Install a branch 525 or newer driver."
+ }
+ if ($Backend -eq 'CUDA' -and $ComputeCapability -lt [version]'5.0') {
+ throw "CUDA backend was requested, but NVIDIA compute capability $ComputeCapability is below the supported Windows CUDA wheel minimum of 5.0."
+ }
+ $amdPresent = $HasAmd -or $GpuVendor -eq 'AMD'
+ $amdRocmSupported = $amdPresent -and [bool]$AmdGfxTarget
+ $intelPresent = $HasIntel -or $GpuVendor -eq 'Intel'
+ if ($Backend -eq 'ROCm' -and -not $amdRocmSupported) {
+ throw 'ROCm backend was requested, but no supported Windows AMD GPU/gfx target was detected.'
+ }
+ $intelCandidateName = if ($IntelGpuName) { $IntelGpuName } else { $GpuName }
+ $intelXpuSupported = $intelPresent -and (Test-IntelXpuGpuSupported -GpuName $intelCandidateName)
+ if ($Backend -eq 'XPU' -and -not $intelXpuSupported) {
+ throw "XPU backend was requested, but the detected Intel GPU '$intelCandidateName' is not in the validated Windows PyTorch XPU families."
+ }
+ if ($Backend -eq 'Auto' -and $amdPresent -and -not $amdRocmSupported -and -not $HasNvidia -and -not $intelXpuSupported) {
+ throw "An AMD GPU is present, but '$AmdGpuName' is not in the ROCm 10.0 Windows support matrix. Use -Backend CPU to explicitly accept CPU-only PyTorch."
+ }
+ if ($Backend -eq 'Auto' -and $intelPresent -and -not $intelXpuSupported -and -not $HasNvidia -and -not $amdRocmSupported) {
+ throw "An Intel GPU is present, but '$intelCandidateName' is not in the validated Windows PyTorch XPU families. Use -Backend CPU to explicitly accept CPU-only PyTorch."
+ }
+ $cudaSupported = $HasNvidia -and $DriverMajor -ge 525 -and
+ $ComputeCapability -ge [version]'5.0' -and
+ -not ($ComputeCapability.Major -ge 10 -and $DriverMajor -lt 580)
+ $selectedBackend = if ($Backend -eq 'Auto') {
+ if ($cudaSupported) {
+ 'CUDA'
+ } elseif ($amdRocmSupported) {
+ 'ROCm'
+ } elseif ($intelXpuSupported) {
+ 'XPU'
+ } else {
+ 'CPU'
+ }
+ } else {
+ $Backend
+ }
+ if ($Backend -eq 'Auto' -and $HasNvidia -and -not $cudaSupported -and
+ -not $amdRocmSupported -and -not $intelXpuSupported) {
+ throw "An NVIDIA GPU is present, but driver branch $DriverMajor and compute capability $ComputeCapability do not match a supported Windows CUDA wheel. Use -Backend CPU to explicitly accept CPU-only PyTorch."
+ }
+ }
+
+ $indexUrl = 'https://download.pytorch.org/whl/cpu'
+ $runtime = 'cpu'
+ $torchRequirement = 'torch==2.14.0+cpu'
+ $torchVersion = '2.14.0+cpu'
+ $directWheelUrl = $null
+ $directWheelSha256 = $null
+ $directWheelFileName = $null
+ $preview = $false
+ $catalog = (Get-AiCatalogData).Components
+ $additionalRequirements = @()
+ if ($selectedBackend -eq 'CUDA') {
+ if ($Architecture -eq 'Arm64') {
+ $runtime = 'cu134'
+ $indexUrl = $null
+ $preview = $true
+ $torchVersion = $catalog.NvidiaPyTorchArm64.Version
+ $directWheelUrl = $catalog.NvidiaPyTorchArm64.Uri
+ $directWheelSha256 = $catalog.NvidiaPyTorchArm64.Sha256
+ $directWheelFileName = 'torch-2.15.0.dev20260904+cu134-cp313-cp313-win_arm64.whl'
+ $torchRequirement = "torch @ $directWheelUrl#sha256=$directWheelSha256"
+ } elseif ($ComputeCapability.Major -ge 10 -and $DriverMajor -lt 580) {
+ throw "This NVIDIA GPU reports compute capability $ComputeCapability and needs a CUDA 13 wheel, but driver branch $DriverMajor is below 580. Update the NVIDIA driver."
+ } elseif ($DriverMajor -ge 580 -and $ComputeCapability -ge [version]'7.5') {
+ $runtime = 'cu130'
+ $indexUrl = 'https://download.pytorch.org/whl/cu130'
+ $torchVersion = '2.14.0+cu130'
+ $torchRequirement = 'torch==2.14.0+cu130'
+ } else {
+ $runtime = 'cu126'
+ $indexUrl = 'https://download.pytorch.org/whl/cu126'
+ $torchVersion = '2.14.0+cu126'
+ $torchRequirement = 'torch==2.14.0+cu126'
+ }
+ } elseif ($selectedBackend -eq 'ROCm') {
+ $runtime = 'rocm10.0.0'
+ $indexUrl = 'https://stable.repo.amd.com/rocm/whl-next/'
+ $torchVersion = '2.13.0+rocm10.0.0'
+ $torchRequirement = "torch[device-$AmdGfxTarget]==2.13.0+rocm10.0.0"
+ $additionalRequirements = @(
+ "torchvision[device-$AmdGfxTarget]==0.28.0+rocm10.0.0",
+ 'torchaudio==2.11.0.2+rocm10.0.0'
+ )
+ } elseif ($selectedBackend -eq 'XPU') {
+ $runtime = 'xpu'
+ $indexUrl = 'https://download.pytorch.org/whl/xpu'
+ $torchVersion = '2.14.0+xpu'
+ $torchRequirement = 'torch==2.14.0+xpu'
+ $additionalRequirements = @('torchvision==0.29.0+xpu')
+ }
+
+ $installTriton = -not $SkipTriton -and (
+ ($selectedBackend -eq 'CUDA' -and $ComputeCapability.Major -ge 8) -or
+ $selectedBackend -eq 'XPU')
+ $tritonRequirement = if (-not $installTriton) {
+ $null
+ } elseif ($selectedBackend -eq 'XPU') {
+ 'triton-xpu==3.8.0'
+ } else {
+ $catalog.TritonWindows.Package
+ }
+ $tritonVersion = if (-not $installTriton) {
+ $null
+ } elseif ($selectedBackend -eq 'XPU') {
+ '3.8.0'
+ } else {
+ '3.8.0.post28'
+ }
+
+ return [pscustomobject]@{
+ Architecture = $Architecture
+ Backend = $selectedBackend
+ Vendor = switch ($selectedBackend) {
+ 'CUDA' { 'NVIDIA' }
+ 'ROCm' { 'AMD' }
+ 'XPU' { 'Intel' }
+ default { 'CPU' }
+ }
+ DeviceName = switch ($selectedBackend) {
+ 'CUDA' { $GpuName }
+ 'ROCm' { $AmdGpuName }
+ 'XPU' { $intelCandidateName }
+ default { 'CPU' }
+ }
+ AmdGfxTarget = if ($selectedBackend -eq 'ROCm') { $AmdGfxTarget } else { $null }
+ TorchRequirement = $torchRequirement
+ TorchVersion = $torchVersion
+ AdditionalRequirements = $additionalRequirements
+ IndexUrl = $indexUrl
+ Runtime = $runtime
+ Preview = $preview
+ DirectWheelUrl = $directWheelUrl
+ DirectWheelSha256 = $directWheelSha256
+ DirectWheelFileName = $directWheelFileName
+ NumpyRequirement = 'numpy==2.5.2'
+ NumpyVersion = '2.5.2'
+ InstallTriton = $installTriton
+ TritonRequirement = $tritonRequirement
+ TritonVersion = $tritonVersion
+ TritonReason = if ($SkipTriton) {
+ 'Triton installation and verification were disabled by the caller.'
+ } elseif ($installTriton) {
+ "Compatible PyTorch $selectedBackend stack detected."
+ } elseif ($selectedBackend -ne 'CUDA') {
+ "No supported native-Windows Triton package is selected for $selectedBackend."
+ } elseif ($ComputeCapability.Major -lt 8) {
+ "Triton Windows requires NVIDIA compute capability 8.0 or newer; detected $ComputeCapability."
+ } else {
+ 'Triton installation was disabled by the caller.'
+ }
+ }
+}
+
+function Test-PyTorchStateCompatible {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $DesiredStateJson,
+ [AllowNull()] [string] $CurrentStateJson
+ )
+
+ if ([string]::IsNullOrWhiteSpace($CurrentStateJson)) {
+ return $false
+ }
+
+ $desired = $DesiredStateJson | ConvertFrom-Json
+ try {
+ $current = $CurrentStateJson | ConvertFrom-Json
+ } catch {
+ return $false
+ }
+ foreach ($property in @('architecture', 'backend', 'index', 'python')) {
+ if ($current.$property -ne $desired.$property) {
+ return $false
+ }
+ }
+ $legacyStableTorch = $current.torch -eq 'torch==2.14.0' -and
+ $desired.torch -match '^torch==2\.14\.0\+(cpu|cu126|cu130)$'
+ if ($current.torch -ne $desired.torch -and -not $legacyStableTorch) {
+ return $false
+ }
+ if (-not $desired.tritonVersion -and $current.triton) {
+ return $false
+ }
+ return $true
+}
+
+function Test-PyTorchEnvironmentMatches {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $DesiredStateJson,
+ [AllowNull()] [string] $CurrentStateJson,
+ [AllowNull()] $InstalledVersions
+ )
+
+ if (-not (Test-PyTorchStateCompatible `
+ -DesiredStateJson $DesiredStateJson `
+ -CurrentStateJson $CurrentStateJson) -or
+ $null -eq $InstalledVersions) {
+ return $false
+ }
+
+ $desired = $DesiredStateJson | ConvertFrom-Json
+ if ($InstalledVersions.torch -ne $desired.torchVersion -or
+ $InstalledVersions.numpy -ne $desired.numpyVersion) {
+ return $false
+ }
+ if ($desired.tritonVersion) {
+ if ($InstalledVersions.triton -ne $desired.tritonVersion) {
+ return $false
+ }
+ } elseif (-not [string]::IsNullOrEmpty($InstalledVersions.triton)) {
+ return $false
+ }
+
+ $additionalRequirements = if ($desired.PSObject.Properties.Name -contains 'additionalRequirements') {
+ @($desired.additionalRequirements)
+ } else {
+ @()
+ }
+ foreach ($requirement in $additionalRequirements) {
+ if ($requirement -match '^torchvision(?:\[[^\]]+\])?==(.+)$' -and $InstalledVersions.torchvision -ne $Matches[1]) {
+ return $false
+ }
+ if ($requirement -match '^torchaudio==(.+)$' -and $InstalledVersions.torchaudio -ne $Matches[1]) {
+ return $false
+ }
+ }
+ return $true
+}
+
+function Get-PyTorchPackageAction {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $DesiredStateJson,
+ [AllowNull()] [string] $CurrentStateJson,
+ [AllowNull()] $InstalledVersions
+ )
+
+ if (Test-PyTorchEnvironmentMatches `
+ -DesiredStateJson $DesiredStateJson `
+ -CurrentStateJson $CurrentStateJson `
+ -InstalledVersions $InstalledVersions) {
+ return 'VerifyOnly'
+ }
+ return 'Install'
+}
+
+function Test-PyTorchEnvironmentRequiresRecreation {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $DesiredStateJson,
+ [AllowNull()] [string] $CurrentStateJson,
+ [AllowNull()] $InstalledVersions
+ )
+
+ if (-not (Test-PyTorchStateCompatible `
+ -DesiredStateJson $DesiredStateJson `
+ -CurrentStateJson $CurrentStateJson)) {
+ return $true
+ }
+ $desired = $DesiredStateJson | ConvertFrom-Json
+ return $null -ne $InstalledVersions -and
+ -not $desired.tritonVersion -and
+ -not [string]::IsNullOrEmpty($InstalledVersions.triton)
+}
+
+function Get-PythonEnvironmentVersions {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $PythonPath)
+
+ if (-not (Test-Path -LiteralPath $PythonPath)) {
+ return $null
+ }
+ $script = @'
+import importlib.metadata
+import json
+import numpy
+import torch
+
+versions = {}
+for distribution in ("triton-windows", "triton-xpu", "torchvision", "torchaudio"):
+ try:
+ versions[distribution] = importlib.metadata.version(distribution)
+ except importlib.metadata.PackageNotFoundError:
+ versions[distribution] = None
+
+triton_distribution = next(
+ (name for name in ("triton-windows", "triton-xpu") if versions[name]),
+ None,
+)
+
+print(json.dumps({
+ "torch": torch.__version__,
+ "numpy": numpy.__version__,
+ "triton": versions[triton_distribution] if triton_distribution else None,
+ "triton_distribution": triton_distribution,
+ "torchvision": versions["torchvision"],
+ "torchaudio": versions["torchaudio"],
+}, sort_keys=True))
+'@
+ $result = Invoke-DevConfigNativeCommand -FilePath $PythonPath -Arguments @('-c', $script)
+ $json = @($result.Output -split '\r?\n' | Where-Object { $_ }) | Select-Object -Last 1
+ if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($json)) {
+ return $null
+ }
+ try {
+ return $json | ConvertFrom-Json
+ } catch {
+ return $null
+ }
+}
+
+function Test-PythonDistributionVersions {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $PythonPath,
+ [Parameter(Mandatory)] [hashtable] $Expected
+ )
+
+ if (-not (Test-Path -LiteralPath $PythonPath)) { return $false }
+ $namesJson = @($Expected.Keys) | ConvertTo-Json -Compress
+ $script = @"
+import importlib.metadata
+import json
+names = json.loads(r'''$namesJson''')
+result = {}
+for name in names:
+ try:
+ result[name] = importlib.metadata.version(name)
+ except importlib.metadata.PackageNotFoundError:
+ result[name] = None
+print(json.dumps(result, sort_keys=True))
+"@
+ $result = Invoke-DevConfigNativeCommand -FilePath $PythonPath -Arguments @('-c', $script)
+ $json = @($result.Output -split '\r?\n' | Where-Object { $_ }) | Select-Object -Last 1
+ if ($result.ExitCode -ne 0 -or -not $json) { return $false }
+ $installed = $json | ConvertFrom-Json
+ foreach ($name in $Expected.Keys) {
+ if ($installed.$name -ne $Expected[$name]) { return $false }
+ }
+ return $true
+}
+
+function Assert-PythonArchitecture {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [Parameter(Mandatory)] [string] $PythonMachine
+ )
+
+ $normalized = switch -Regex ($PythonMachine) {
+ '^(AMD64|x86_64)$' { 'X64'; break }
+ '^(ARM64|aarch64)$' { 'Arm64'; break }
+ default { $PythonMachine }
+ }
+ if ($normalized -ne $Architecture) {
+ throw "Python architecture '$PythonMachine' does not match Windows architecture '$Architecture'. Remove emulated or conflicting Python installations and rerun the flow."
+ }
+}
+
+function Get-Python313Path {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture)
+
+ $candidates = [System.Collections.Generic.List[string]]::new()
+ $launcher = Get-Command py -ErrorAction SilentlyContinue
+ if ($launcher) {
+ $selector = if ($Architecture -eq 'Arm64') { '-3.13-arm64' } else { '-3.13-64' }
+ $launcherResult = Invoke-DevConfigNativeCommand -FilePath $launcher.Source -Arguments @(
+ $selector, '-c', 'import sys; print(sys.executable)'
+ )
+ $launcherPath = [string](@($launcherResult.Output -split '\r?\n' | Where-Object { $_ }) | Select-Object -First 1)
+ if ($launcherResult.ExitCode -eq 0 -and $launcherPath) {
+ [void]$candidates.Add($launcherPath.Trim())
+ }
+ }
+ foreach ($commandName in @('python3.13', 'python')) {
+ $command = Get-Command $commandName -ErrorAction SilentlyContinue
+ if ($command) {
+ [void]$candidates.Add($command.Source)
+ }
+ }
+ foreach ($candidate in $candidates | Select-Object -Unique) {
+ $versionResult = Invoke-DevConfigNativeCommand -FilePath $candidate -Arguments @(
+ '-c', 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")'
+ )
+ $version = [string](@($versionResult.Output -split '\r?\n' | Where-Object { $_ }) | Select-Object -First 1)
+ if ($versionResult.ExitCode -eq 0 -and $version.Trim() -eq '3.13') {
+ return $candidate
+ }
+ }
+ throw "Native $Architecture CPython 3.13 was installed but could not be resolved. Disable conflicting App Execution Aliases or run the Python 3.13 installer repair."
+}
+
+function Get-PipInstallArguments {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Requirement,
+ [string] $IndexUrl,
+ [switch] $DryRun
+ )
+
+ $arguments = [System.Collections.Generic.List[string]]::new()
+ [void]$arguments.Add('-m')
+ [void]$arguments.Add('pip')
+ [void]$arguments.Add('install')
+ if ($DryRun) {
+ [void]$arguments.Add('--dry-run')
+ }
+
+ [void]$arguments.Add('--only-binary=:all:')
+ [void]$arguments.Add($Requirement)
+ if ($IndexUrl) {
+ [void]$arguments.Add('--index-url')
+ [void]$arguments.Add($IndexUrl)
+ }
+ return $arguments.ToArray()
+}
+
+function Get-PipLocalWheelInstallArguments {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $WheelPath)
+
+ return @('-m', 'pip', 'install', '--only-binary=:all:', $WheelPath)
+}
+
+function Get-FoundryModelSmokePlan {
+ return [pscustomobject]@{
+ Model = 'qwen3-0.6b'
+ Marker = 'DEVCONFIG_FOUNDRY_READY'
+ ApproximateDownloadMb = 593
+ License = 'Apache-2.0'
+ }
+}
+
+function Get-FoundryModelSmokeCommands {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Model,
+ [Parameter(Mandatory)] [string] $Marker
+ )
+
+ return [pscustomobject]@{
+ Download = @('model', 'download', $Model)
+ Complete = @('complete', $Model, "Reply with exactly $Marker and nothing else. /no_think")
+ }
+}
+
+function Get-OllamaModelSmokePlan {
+ return [pscustomobject]@{
+ Model = 'qwen3:0.6b'
+ Marker = 'DEVCONFIG_OLLAMA_READY'
+ ModelBlobSha256 = '7f4030143c1c477224c5434f8272c662a8b042079a0a584f0a27a1684fe2e1fa'
+ ApproximateDownloadMb = 522
+ License = 'Apache-2.0'
+ }
+}
+
+function Get-OllamaModelManifestPath {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $ModelRoot,
+ [Parameter(Mandatory)] [string] $Model
+ )
+
+ $parts = $Model.Split(':', 2)
+ $name = $parts[0]
+ $tag = if ($parts.Count -eq 2) { $parts[1] } else { 'latest' }
+ return Join-Path $ModelRoot "manifests\registry.ollama.ai\library\$name\$tag"
+}
+
+function Get-LlamaModelSmokePlan {
+ return [pscustomobject]@{
+ Repository = 'Qwen/Qwen3-0.6B-GGUF'
+ Revision = 'ef4088322893040952513f532f736ddeab518403'
+ FileName = 'Qwen3-0.6B-Q4_K_M.gguf'
+ Url = 'https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/ef4088322893040952513f532f736ddeab518403/Qwen3-0.6B-Q4_K_M.gguf?download=true'
+ Sha256 = 'b0638f08417a2d3c8652760462eb5407c6e30173cf9608ad0820757a281eea0e'
+ Size = 396704416
+ Marker = 'DEVCONFIG_LLAMA_READY'
+ License = 'Apache-2.0'
+ }
+}
+
+function Get-LlamaCodingDemoPlan {
+ return [pscustomobject]@{
+ Repository = 'Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF'
+ Revision = 'f86cb2c1fa58255f8052cc32aeede1b7482d4361'
+ FileName = 'qwen2.5-coder-1.5b-instruct-q4_k_m.gguf'
+ Url = 'https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/f86cb2c1fa58255f8052cc32aeede1b7482d4361/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf?download=true'
+ Sha256 = 'cc324af070c2ecbfd324a30884d2f951a7ff756aba85cb811a6ec436933bb046'
+ Size = 1117320768
+ License = 'Apache-2.0'
+ }
+}
+
+function New-OllamaGenerateRequest {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Model,
+ [Parameter(Mandatory)] [string] $Marker
+ )
+
+ return [ordered]@{
+ model = $Model
+ prompt = "Return a JSON object whose marker is $Marker. Nothing else."
+ think = $false
+ stream = $false
+ format = [ordered]@{
+ type = 'object'
+ properties = [ordered]@{
+ marker = [ordered]@{ type = 'string'; enum = @($Marker) }
+ }
+ required = @('marker')
+ additionalProperties = $false
+ }
+ options = [ordered]@{
+ seed = 42
+ temperature = 0.7
+ top_p = 0.8
+ top_k = 20
+ num_predict = 32
+ }
+ }
+}
+
+function Get-LlamaInferenceArguments {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $ModelPath,
+ [Parameter(Mandatory)] [string] $Marker
+ )
+
+ return @(
+ '--model', $ModelPath,
+ '--single-turn',
+ '--prompt', "Reply with exactly $Marker and nothing else.",
+ '--reasoning', 'off',
+ '--grammar', "root ::= `"$Marker`"",
+ '--seed', '42',
+ '--temperature', '0.7',
+ '--top-p', '0.8',
+ '--top-k', '20',
+ '--threads', '1',
+ '--threads-batch', '1',
+ '--predict', '32',
+ '--no-display-prompt',
+ '--simple-io',
+ '--log-disable'
+ )
+}
+
+function Invoke-CheckedCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $FilePath,
+ [string[]] $ArgumentList = @(),
+ [string] $DisplayName = $FilePath
+ )
+
+ $result = Invoke-DevConfigNativeCommand -FilePath $FilePath -Arguments $ArgumentList
+ if ($result.Output) { Write-Host $result.Output.TrimEnd() }
+ if ($result.ExitCode -ne 0) {
+ throw "$DisplayName failed with exit code $($result.ExitCode)."
+ }
+}
+
+function Assert-CommandAvailable {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $CommandName,
+ [Parameter(Mandatory)] [string] $Remediation
+ )
+
+ $command = Get-Command $CommandName -ErrorAction SilentlyContinue
+ if (-not $command) {
+ throw "$CommandName was not found. $Remediation"
+ }
+ return $command
+}
+
+function Get-AiUpdatedPathValue {
+ [CmdletBinding()]
+ param(
+ [AllowEmptyString()] [string] $CurrentValue,
+ [Parameter(Mandatory)] [string] $Path,
+ [switch] $Prepend
+ )
+
+ $entries = @($CurrentValue -split ';' | Where-Object { $_ -and $_ -ne $Path })
+ if ($Prepend) {
+ return (@($Path) + $entries) -join ';'
+ }
+ return (@($entries) + $Path) -join ';'
+}
+
+function Add-UserPathEntry {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Path,
+ [switch] $Prepend
+ )
+
+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
+ [Environment]::SetEnvironmentVariable(
+ 'Path',
+ (Get-AiUpdatedPathValue -CurrentValue $current -Path $Path -Prepend:$Prepend),
+ 'User')
+ $env:Path = Get-AiUpdatedPathValue -CurrentValue $env:Path -Path $Path -Prepend:$Prepend
+}
+
+function Install-VerifiedDirectorySwap {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Source,
+ [Parameter(Mandatory)] [string] $Destination
+ )
+
+ $parent = Split-Path -Parent $Destination
+ New-Item -ItemType Directory -Path $parent -Force | Out-Null
+ $newPath = "$Destination.new-$([guid]::NewGuid().ToString('N'))"
+ $oldPath = "$Destination.old-$([guid]::NewGuid().ToString('N'))"
+ Move-Item -LiteralPath $Source -Destination $newPath
+ try {
+ if (Test-Path -LiteralPath $Destination) {
+ Move-Item -LiteralPath $Destination -Destination $oldPath
+ }
+ Move-Item -LiteralPath $newPath -Destination $Destination
+ if (Test-Path -LiteralPath $oldPath) {
+ Remove-Item -LiteralPath $oldPath -Recurse -Force
+ }
+ } catch {
+ if (-not (Test-Path -LiteralPath $Destination) -and (Test-Path -LiteralPath $oldPath)) {
+ Move-Item -LiteralPath $oldPath -Destination $Destination -ErrorAction SilentlyContinue
+ }
+ throw
+ } finally {
+ Remove-Item -LiteralPath $newPath -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
+
+function Remove-UserPathEntry {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $Path)
+
+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
+ $entries = @($current -split ';' | Where-Object { $_ -and $_ -ne $Path })
+ [Environment]::SetEnvironmentVariable('Path', ($entries -join ';'), 'User')
+ $processEntries = @($env:Path -split ';' | Where-Object { $_ -and $_ -ne $Path })
+ $env:Path = $processEntries -join ';'
+}
+
+function Remove-TemporaryFileWithRetry {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Path,
+ [int] $MaxAttempts = 12,
+ [int] $DelayMilliseconds = 5000,
+ [scriptblock] $RemoveAction = {
+ param([string] $Target)
+ Remove-Item -LiteralPath $Target -Force -ErrorAction Stop
+ }
+ )
+
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return $true
+ }
+
+ $lastError = $null
+ for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
+ try {
+ & $RemoveAction $Path
+ if (-not (Test-Path -LiteralPath $Path)) {
+ return $true
+ }
+ $lastError = "the file still exists after removal attempt $attempt"
+ } catch {
+ $lastError = $_.Exception.Message
+ }
+ if ($attempt -lt $MaxAttempts -and $DelayMilliseconds -gt 0) {
+ Start-Sleep -Milliseconds $DelayMilliseconds
+ }
+ }
+
+ Write-Warning `
+ -Message "Could not remove temporary file '$Path' after $MaxAttempts attempts. It may remain until the installer releases it or Windows cleans the temporary directory. Last error: $lastError" `
+ -WarningAction Continue
+ return $false
+}
+
+function Install-VerifiedDownload {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [uri] $Uri,
+ [Parameter(Mandatory)] [string] $Destination,
+ [Parameter(Mandatory)] [ValidatePattern('^[0-9a-fA-F]{64}$')] [string] $Sha256,
+ [long] $ExpectedSize = 0
+ )
+
+ if (Test-Path -LiteralPath $Destination) {
+ $existing = Get-Item -LiteralPath $Destination
+ $existingHash = (Get-FileHash -LiteralPath $Destination -Algorithm SHA256).Hash
+ if ($existingHash -eq $Sha256 -and ($ExpectedSize -eq 0 -or $existing.Length -eq $ExpectedSize)) {
+ return
+ }
+ }
+
+ $parent = Split-Path -Parent $Destination
+ New-Item -ItemType Directory -Path $parent -Force | Out-Null
+ $temporary = "$Destination.download-$([guid]::NewGuid().ToString('N'))"
+ try {
+ Invoke-WebRequest -Uri $Uri -OutFile $temporary -UseBasicParsing
+ $download = Get-Item -LiteralPath $temporary
+ if ($ExpectedSize -gt 0 -and $download.Length -ne $ExpectedSize) {
+ throw "Download size mismatch for '$Uri'. Expected $ExpectedSize bytes; got $($download.Length)."
+ }
+ $actualHash = (Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash
+ if ($actualHash -ne $Sha256) {
+ throw "SHA-256 mismatch for '$Uri'. Expected $Sha256; got $actualHash."
+ }
+ Move-Item -LiteralPath $temporary -Destination $Destination -Force
+ } finally {
+ if (Test-Path -LiteralPath $temporary) {
+ [void](Remove-TemporaryFileWithRetry -Path $temporary)
+ }
+ }
+}
+
+function Invoke-VerifiedInstaller {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [uri] $Uri,
+ [Parameter(Mandatory)] [ValidatePattern('^[0-9a-fA-F]{64}$')] [string] $Sha256,
+ [Parameter(Mandatory)] [string] $SignerPattern,
+ [string[]] $ArgumentList = @(),
+ [int[]] $SuccessExitCodes = @(0)
+ )
+
+ $temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-$([guid]::NewGuid().ToString('N')).exe"
+ try {
+ Install-VerifiedDownload -Uri $Uri -Destination $temporary -Sha256 $Sha256
+ $signature = Get-AuthenticodeSignature -LiteralPath $temporary
+ if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch $SignerPattern) {
+ throw "Installer signature validation failed for '$Uri'. Expected a valid signer matching '$SignerPattern'; got '$($signature.Status)' from '$($signature.SignerCertificate.Subject)'."
+ }
+
+ $process = Start-Process -FilePath $temporary -ArgumentList $ArgumentList -Wait -PassThru
+ if ($process.ExitCode -notin $SuccessExitCodes) {
+ throw "Installer '$Uri' failed with exit code $($process.ExitCode)."
+ }
+ } finally {
+ if (Test-Path -LiteralPath $temporary) {
+ [void](Remove-TemporaryFileWithRetry -Path $temporary)
+ }
+ }
+}
+
+function Invoke-VerifiedLocalInstaller {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Path,
+ [Parameter(Mandatory)] [ValidatePattern('^[0-9a-fA-F]{64}$')] [string] $Sha256,
+ [Parameter(Mandatory)] [string] $SignerPattern,
+ [string[]] $ArgumentList = @(),
+ [int[]] $SuccessExitCodes = @(0),
+ [int] $TimeoutSeconds = 7200
+ )
+
+ $actualHash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
+ if ($actualHash -ne $Sha256) {
+ throw "Installer SHA-256 mismatch for '$Path'. Expected $Sha256; got $actualHash."
+ }
+ $signature = Get-AuthenticodeSignature -LiteralPath $Path
+ if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch $SignerPattern) {
+ throw "Installer signature validation failed for '$Path'."
+ }
+ $exitCode = Invoke-DevConfigProcess -FilePath $Path -Arguments $ArgumentList -TimeoutSeconds $TimeoutSeconds
+ if ($exitCode -notin $SuccessExitCodes) {
+ throw "Installer '$Path' failed with exit code $exitCode."
+ }
+}
+
+function Get-CudaNvccPath {
+ [CmdletBinding()]
+ param([AllowNull()] [AllowEmptyString()] [string] $ToolkitVersion)
+
+ $cudaPath = [Environment]::GetEnvironmentVariable('CUDA_PATH', 'Machine')
+ $pathCommand = Get-Command nvcc -ErrorAction SilentlyContinue
+ $versionedCandidate = if ($ToolkitVersion) {
+ Join-Path $env:ProgramFiles "NVIDIA GPU Computing Toolkit\CUDA\v$ToolkitVersion\bin\nvcc.exe"
+ } else { $null }
+ $cudaRoot = Join-Path $env:ProgramFiles 'NVIDIA GPU Computing Toolkit\CUDA'
+ $installedCandidates = @(Get-ChildItem -LiteralPath $cudaRoot -Directory -ErrorAction SilentlyContinue |
+ Where-Object { $_.Name -match '^v[0-9]+\.[0-9]+$' } |
+ Sort-Object { [version]$_.Name.Substring(1) } -Descending |
+ ForEach-Object { Join-Path $_.FullName 'bin\nvcc.exe' })
+ $candidates = @(
+ $versionedCandidate,
+ $(if ($cudaPath) { Join-Path $cudaPath 'bin\nvcc.exe' }),
+ $(if ($pathCommand) { $pathCommand.Source }),
+ $installedCandidates
+ ) | Where-Object { $_ }
+
+ $nvcc = $null
+ foreach ($candidate in $candidates | Select-Object -Unique) {
+ if (-not (Test-Path -LiteralPath $candidate)) {
+ continue
+ }
+ $versionResult = Invoke-DevConfigNativeCommand -FilePath $candidate -Arguments @('--version')
+ $versionOutput = $versionResult.Output
+ if ($versionResult.ExitCode -eq 0 -and
+ (-not $ToolkitVersion -or $versionOutput -match "release $([regex]::Escape($ToolkitVersion))")) {
+ $nvcc = $candidate
+ break
+ }
+ }
+ if (-not $nvcc) {
+ throw 'A matching CUDA nvcc.exe was not found. Reopen the terminal and verify CUDA_PATH.'
+ }
+ Add-UserPathEntry -Path (Split-Path -Parent $nvcc)
+ return $nvcc
+}
+
+function Get-MsvcCompilerPath {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture)
+
+ $vsDevCmd = Get-VsDevCmdPath -Architecture $Architecture
+ $installationPath = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $vsDevCmd))
+ $toolsRoot = Join-Path $installationPath 'VC\Tools\MSVC'
+ $toolset = Get-ChildItem -LiteralPath $toolsRoot -Directory -ErrorAction SilentlyContinue |
+ Sort-Object Name -Descending |
+ Select-Object -First 1
+ if (-not $toolset) {
+ throw "No MSVC toolset was found under '$toolsRoot'."
+ }
+
+ $relativeCandidates = if ($Architecture -eq 'Arm64') {
+ @('bin\Hostarm64\arm64\cl.exe', 'bin\Hostx64\arm64\cl.exe')
+ } else {
+ @('bin\Hostx64\x64\cl.exe')
+ }
+ $compiler = $relativeCandidates |
+ ForEach-Object { Join-Path $toolset.FullName $_ } |
+ Where-Object { Test-Path -LiteralPath $_ } |
+ Select-Object -First 1
+ if (-not $compiler) {
+ throw "The MSVC compiler for $Architecture was not found. Re-run the C++ Build Tools configuration."
+ }
+ return $compiler
+}
+
+function Get-VsDevCmdPath {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture)
+
+ $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
+ if (-not (Test-Path -LiteralPath $vswhere)) {
+ throw 'Visual Studio Installer vswhere.exe was not found after installing the C++ Build Tools workload.'
+ }
+ $vswhereResult = Invoke-DevConfigNativeCommand -FilePath $vswhere -Arguments @(
+ '-all', '-products', 'Microsoft.VisualStudio.Product.BuildTools', '-property', 'installationPath'
+ )
+ $installationOutput = @($vswhereResult.Output -split '\r?\n' | Where-Object { $_ })
+ if ($vswhereResult.ExitCode -ne 0) {
+ throw "vswhere.exe failed while locating Visual Studio Build Tools (exit $($vswhereResult.ExitCode))."
+ }
+ return Resolve-VsDevCmdPath -InstallationPaths $installationOutput -Architecture $Architecture
+}
+
+function Resolve-VsDevCmdPath {
+ [CmdletBinding()]
+ param(
+ [AllowEmptyCollection()] [string[]] $InstallationPaths = @(),
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture
+ )
+
+ $relativeCompilers = if ($Architecture -eq 'Arm64') {
+ @('bin\Hostarm64\arm64\cl.exe', 'bin\Hostx64\arm64\cl.exe')
+ } else {
+ @('bin\Hostx64\x64\cl.exe')
+ }
+
+ foreach ($rawPath in $InstallationPaths) {
+ if ([string]::IsNullOrWhiteSpace($rawPath)) {
+ continue
+ }
+ $installationPath = $rawPath.Trim()
+ $vsDevCmd = Join-Path $installationPath 'Common7\Tools\VsDevCmd.bat'
+ if (-not (Test-Path -LiteralPath $vsDevCmd)) {
+ continue
+ }
+ $toolsets = Get-ChildItem -LiteralPath (Join-Path $installationPath 'VC\Tools\MSVC') `
+ -Directory -ErrorAction SilentlyContinue |
+ Sort-Object Name -Descending
+ foreach ($toolset in $toolsets) {
+ foreach ($relativeCompiler in $relativeCompilers) {
+ if (Test-Path -LiteralPath (Join-Path $toolset.FullName $relativeCompiler)) {
+ return $vsDevCmd
+ }
+ }
+ }
+ }
+
+ throw "No Visual Studio Build Tools installation with an $Architecture MSVC compiler was found. Re-run the architecture-specific C++ Build Tools configuration."
+}
+
+function Import-MsvcEnvironment {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture)
+
+ $vsDevCmd = Get-VsDevCmdPath -Architecture $Architecture
+ $target = if ($Architecture -eq 'Arm64') { 'arm64' } else { 'x64' }
+ $vsInstaller = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer'
+ $command = "set `"PATH=$vsInstaller;%PATH%`" && call `"$vsDevCmd`" -arch=$target -host_arch=$target >nul && set"
+ $environmentResult = Invoke-DevConfigNativeCommand -FilePath $env:ComSpec -Arguments @('/d', '/s', '/c', $command)
+ $environmentLines = @($environmentResult.Output -split '\r?\n')
+ if ($environmentResult.ExitCode -ne 0) {
+ throw "VsDevCmd failed to initialize the $Architecture compiler environment."
+ }
+ foreach ($line in $environmentLines) {
+ if ($line -match '^([^=]+)=(.*)$') {
+ [Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], 'Process')
+ }
+ }
+
+ $compiler = Get-MsvcCompilerPath -Architecture $Architecture
+ $env:CC = $compiler
+ return $compiler
+}
+
+function Get-CudaKernelCompileCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [Parameter(Mandatory)] [string] $VsDevCmd,
+ [Parameter(Mandatory)] [string] $Nvcc,
+ [Parameter(Mandatory)] [string] $Source,
+ [Parameter(Mandatory)] [string] $Output
+ )
+
+ $target = if ($Architecture -eq 'Arm64') { 'arm64' } else { 'x64' }
+ $vsInstaller = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer'
+ return 'set "PATH={0};%PATH%" && call "{1}" -arch={2} -host_arch={2} >nul && "{3}" -arch=native -o "{4}" "{5}"' -f `
+ $vsInstaller, $VsDevCmd, $target, $Nvcc, $Output, $Source
+}
+
+function Find-GitHubReleaseAssetSet {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Repository,
+ [Parameter(Mandatory)] [string[]] $AssetPatterns,
+ [Parameter(Mandatory)] [hashtable] $Headers,
+ [int] $MaxPages = 5
+ )
+
+ for ($page = 1; $page -le $MaxPages; $page++) {
+ try {
+ $releaseResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repository/releases?per_page=100&page=$page" -Headers $Headers
+ $releases = @($releaseResponse | ForEach-Object { $_ })
+ } catch {
+ throw "Could not query official releases for $Repository. GitHub may be unavailable or rate-limiting this network. Wait for the rate limit to reset, or set GITHUB_TOKEN for authenticated API access. $($_.Exception.Message)"
+ }
+ foreach ($candidate in $releases | Where-Object { -not $_.draft -and $_.tag_name -match '^b[0-9]+$' }) {
+ $candidateAssets = @($candidate.assets | ForEach-Object { $_ })
+ if ($candidateAssets.Count -ge 30) {
+ $assetResponse = Invoke-RestMethod -Uri "$($candidate.assets_url)?per_page=100" -Headers $Headers
+ $candidateAssets = @($assetResponse | ForEach-Object { $_ })
+ }
+ $selectedAssets = @()
+ $complete = $true
+ foreach ($pattern in $AssetPatterns) {
+ $patternMatches = @($candidateAssets | Where-Object { $_.name -match $pattern })
+ if ($patternMatches.Count -ne 1) {
+ $complete = $false
+ break
+ }
+ $selectedAssets += $patternMatches[0]
+ }
+ if ($complete) {
+ return [pscustomobject]@{
+ Release = $candidate
+ Assets = $selectedAssets
+ }
+ }
+ }
+ if ($releases.Count -lt 100) {
+ break
+ }
+ }
+
+ throw "No rolling $Repository release in the newest $MaxPages API pages contains the complete asset set: $($AssetPatterns -join ', ')."
+}
+
+function Install-VerifiedGitHubReleaseAsset {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Repository,
+ [Parameter(Mandatory)] [string] $AssetPattern,
+ [Parameter(Mandatory)] [string] $Destination,
+ [Parameter(Mandatory)] [string] $VersionMarker,
+ [Parameter(Mandatory)] [string[]] $RequiredFile
+ )
+
+ return Install-VerifiedGitHubReleaseAssets `
+ -Repository $Repository `
+ -AssetPatterns @($AssetPattern) `
+ -Destination $Destination `
+ -VersionMarker $VersionMarker `
+ -RequiredFile $RequiredFile
+}
+
+function Install-VerifiedGitHubReleaseAssets {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Repository,
+ [Parameter(Mandatory)] [string[]] $AssetPatterns,
+ [Parameter(Mandatory)] [string] $Destination,
+ [Parameter(Mandatory)] [string] $VersionMarker,
+ [Parameter(Mandatory)] [string[]] $RequiredFile,
+ [string] $CacheDirectory = '',
+ [int] $MaxPages = 5
+ )
+
+ $headers = @{
+ Accept = 'application/vnd.github+json'
+ 'User-Agent' = 'WindowsDeveloperConfig'
+ 'X-GitHub-Api-Version' = '2022-11-28'
+ }
+ if ($env:GITHUB_TOKEN) {
+ $headers.Authorization = "Bearer $env:GITHUB_TOKEN"
+ }
+ if ($env:GITHUB_TOKEN) {
+ $headers['Authorization'] = [string]::Concat('Bea', 'rer ', $env:GITHUB_TOKEN)
+ }
+ $assetSet = Find-GitHubReleaseAssetSet `
+ -Repository $Repository `
+ -AssetPatterns $AssetPatterns `
+ -Headers $headers `
+ -MaxPages $MaxPages
+ $release = $assetSet.Release
+ $assets = @($assetSet.Assets)
+
+ foreach ($asset in $assets) {
+ if ($asset.digest -notmatch '^sha256:([0-9a-fA-F]{64})$') {
+ throw "GitHub did not publish a SHA-256 digest for asset '$($asset.name)'; refusing an unverified download."
+ }
+ }
+
+ $markerPath = Join-Path $Destination $VersionMarker
+ $selection = "$($release.tag_name)|$(@($assets | ForEach-Object { "$($_.name)=$($_.digest)" }) -join '|')"
+ $requiredFilesPresent = @($RequiredFile | Where-Object {
+ Test-Path -LiteralPath (Join-Path $Destination $_)
+ }).Count -eq $RequiredFile.Count
+ if ((Test-Path -LiteralPath $markerPath) -and
+ $requiredFilesPresent -and
+ ((Get-Content -LiteralPath $markerPath -Raw).Trim() -eq $selection)) {
+ return [pscustomobject]@{
+ Tag = $release.tag_name
+ Assets = $assets
+ Action = 'already-current'
+ Destination = $Destination
+ CacheDirectory = $CacheDirectory
+ }
+ }
+
+ $hadExistingRuntime = Test-Path -LiteralPath $Destination
+ $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-$([guid]::NewGuid().ToString('N'))"
+ $extractPath = Join-Path $tempRoot 'expanded'
+ New-Item -ItemType Directory -Path $extractPath -Force | Out-Null
+ try {
+ foreach ($asset in $assets) {
+ $expectedHash = $asset.digest.Substring(7)
+ if ($CacheDirectory) {
+ $releaseCache = Join-Path $CacheDirectory $release.tag_name
+ New-Item -ItemType Directory -Path $releaseCache -Force | Out-Null
+ $archivePath = Join-Path $releaseCache $asset.name
+ } else {
+ $archivePath = Join-Path $tempRoot $asset.name
+ }
+ $cacheValid = (Test-Path -LiteralPath $archivePath) -and
+ ((Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash -eq $expectedHash)
+ if (-not $cacheValid) {
+ $downloadPath = "$archivePath.download-$([guid]::NewGuid().ToString('N'))"
+ try {
+ Invoke-WebRequest -Uri $asset.browser_download_url -Headers $headers -OutFile $downloadPath -UseBasicParsing
+ $downloadHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash
+ if ($downloadHash -ne $expectedHash) {
+ throw "SHA-256 mismatch for '$($asset.name)'. Expected $expectedHash; got $downloadHash."
+ }
+ Move-Item -LiteralPath $downloadPath -Destination $archivePath -Force
+ } finally {
+ if (Test-Path -LiteralPath $downloadPath) {
+ Remove-Item -LiteralPath $downloadPath -Force
+ }
+ }
+ }
+ $actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash
+ if ($actualHash -ne $expectedHash) {
+ throw "SHA-256 mismatch for '$($asset.name)'. Expected $expectedHash; got $actualHash."
+ }
+ Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force
+ }
+ $missingFiles = @($RequiredFile | Where-Object {
+ -not (Test-Path -LiteralPath (Join-Path $extractPath $_))
+ })
+ if ($missingFiles.Count -gt 0) {
+ throw "Verified release $($release.tag_name) did not contain required files: $($missingFiles -join ', ')."
+ }
+ Install-VerifiedDirectorySwap -Source $extractPath -Destination $Destination
+ Set-Content -LiteralPath $markerPath -Value $selection -Encoding ascii
+ } finally {
+ if (Test-Path -LiteralPath $tempRoot) {
+ Remove-Item -LiteralPath $tempRoot -Recurse -Force
+ }
+ }
+
+ return [pscustomobject]@{
+ Tag = $release.tag_name
+ Assets = $assets
+ Action = $(if ($hadExistingRuntime) { 'upgraded' } else { 'installed' })
+ Destination = $Destination
+ CacheDirectory = $CacheDirectory
+ }
+}
+
+function Install-VerifiedGitHubLatestAsset {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Repository,
+ [Parameter(Mandatory)] [string] $AssetPattern,
+ [Parameter(Mandatory)] [string] $Destination,
+ [Parameter(Mandatory)] [string] $VersionMarker,
+ [Parameter(Mandatory)] [string] $RequiredFile,
+ [string] $CacheDirectory = ''
+ )
+
+ $headers = @{
+ Accept = 'application/vnd.github+json'
+ 'User-Agent' = 'WindowsDeveloperConfig'
+ 'X-GitHub-Api-Version' = '2022-11-28'
+ }
+ if ($env:GITHUB_TOKEN) {
+ $headers.Authorization = ('{0} {1}' -f 'Bearer', $env:GITHUB_TOKEN)
+ }
+ $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repository/releases/latest" -Headers $headers
+ if ($release.draft -or $release.prerelease) {
+ throw "The latest $Repository release '$($release.tag_name)' is not stable."
+ }
+ $assets = @($release.assets | Where-Object { $_.name -match $AssetPattern })
+ if ($assets.Count -ne 1) {
+ throw "Expected one stable $Repository asset matching '$AssetPattern'; found $($assets.Count)."
+ }
+ $asset = $assets[0]
+ if ($asset.digest -notmatch '^sha256:([0-9a-fA-F]{64})$') {
+ throw "GitHub did not publish a SHA-256 digest for '$($asset.name)'."
+ }
+ $selection = "$($release.tag_name)|$($asset.name)|$($asset.digest)"
+ $markerPath = Join-Path $Destination $VersionMarker
+ if ((Test-Path -LiteralPath $markerPath) -and
+ (Test-Path -LiteralPath (Join-Path $Destination $RequiredFile)) -and
+ ((Get-Content -LiteralPath $markerPath -Raw).Trim() -eq $selection)) {
+ $cachedAsset = if ($CacheDirectory) {
+ Join-Path (Join-Path $CacheDirectory $release.tag_name) $asset.name
+ } else { $null }
+ return [pscustomobject]@{
+ Tag = $release.tag_name
+ Asset = $asset
+ Action = 'already-current'
+ CachePath = $cachedAsset
+ }
+ }
+
+ $tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-$([guid]::NewGuid().ToString('N'))"
+ $expected = $asset.digest.Substring(7)
+ $archive = if ($CacheDirectory) {
+ $releaseCache = Join-Path $CacheDirectory $release.tag_name
+ New-Item -ItemType Directory -Path $releaseCache -Force | Out-Null
+ Join-Path $releaseCache $asset.name
+ } else {
+ Join-Path $tempRoot $asset.name
+ }
+ $expanded = Join-Path $tempRoot 'expanded'
+ New-Item -ItemType Directory -Path $expanded -Force | Out-Null
+ try {
+ $cacheValid = (Test-Path -LiteralPath $archive) -and
+ ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -eq $expected)
+ if (-not $cacheValid) {
+ $downloadPath = "$archive.download-$([guid]::NewGuid().ToString('N'))"
+ try {
+ Invoke-WebRequest -Uri $asset.browser_download_url -Headers $headers -OutFile $downloadPath -UseBasicParsing
+ $downloadHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash
+ if ($downloadHash -ne $expected) {
+ throw "SHA-256 mismatch for '$($asset.name)'. Expected $expected; got $downloadHash."
+ }
+ Move-Item -LiteralPath $downloadPath -Destination $archive -Force
+ } finally {
+ Remove-Item -LiteralPath $downloadPath -Force -ErrorAction SilentlyContinue
+ }
+ }
+ $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash
+ if ($actual -ne $expected) {
+ throw "SHA-256 mismatch for '$($asset.name)'. Expected $expected; got $actual."
+ }
+ Expand-Archive -LiteralPath $archive -DestinationPath $expanded -Force
+ if (-not (Test-Path -LiteralPath (Join-Path $expanded $RequiredFile))) {
+ throw "Verified asset '$($asset.name)' did not contain '$RequiredFile'."
+ }
+ Install-VerifiedDirectorySwap -Source $expanded -Destination $Destination
+ Set-Content -LiteralPath $markerPath -Value $selection -Encoding ascii
+ } finally {
+ if (Test-Path -LiteralPath $tempRoot) {
+ Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+ return [pscustomobject]@{
+ Tag = $release.tag_name
+ Asset = $asset
+ Action = 'installed-or-upgraded'
+ CachePath = $archive
+ }
+}
+
+function Wait-JsonEndpoint {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [uri] $Uri,
+ [int] $TimeoutSeconds = 30
+ )
+
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+ do {
+ try {
+ return Invoke-RestMethod -Uri $Uri -TimeoutSec 5
+ } catch {
+ Start-Sleep -Seconds 1
+ }
+ } while ((Get-Date) -lt $deadline)
+
+ throw "Endpoint '$Uri' did not become ready within $TimeoutSeconds seconds."
+}
diff --git a/src/Workloads/_common/apply-configuration.ps1 b/src/Workloads/_common/apply-configuration.ps1
index 9f8b9494..26d055c1 100644
--- a/src/Workloads/_common/apply-configuration.ps1
+++ b/src/Workloads/_common/apply-configuration.ps1
@@ -39,7 +39,8 @@ param(
# bound to Mandatory parameters. Some flows (e.g. mac-comfort-shell)
# have no post-install CLI to verify - the DSC only installs a font
# and pwsh - so they legitimately pass @() here.
- [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $RequireCommands
+ [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $RequireCommands,
+ [switch] $DeferSentinel
)
$ErrorActionPreference = 'Stop'
@@ -120,4 +121,6 @@ foreach ($cmd in $RequireCommands) {
Write-Host "$cmd : $(& $cmd --version 2>&1 | Select-Object -First 1)"
}
-Write-Host "INSTALL_OK: $Id"
+if (-not $DeferSentinel) {
+ Write-Host "INSTALL_OK: $Id"
+}
diff --git a/src/Workloads/_common/collect-ai-hardware.ps1 b/src/Workloads/_common/collect-ai-hardware.ps1
new file mode 100644
index 00000000..020e17f6
--- /dev/null
+++ b/src/Workloads/_common/collect-ai-hardware.ps1
@@ -0,0 +1,20 @@
+<#
+.SYNOPSIS
+ Emit a portable JSON hardware inventory for AI workload planning.
+#>
+[CmdletBinding()]
+param(
+ [string] $OutputPath = (Join-Path $env:LOCALAPPDATA 'DevConfig\reports\hardware-latest.json')
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot 'direct-setup.ps1')
+. (Join-Path $PSScriptRoot 'ai-report.ps1')
+
+$report = New-AiWorkloadReport -Id 'hardware-inventory' -Request @{ PlanOnly = $true }
+$report.result.ready = $true
+$report.completedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
+Write-DevConfigTextFile -Path $OutputPath -Content ($report | ConvertTo-Json -Depth 20)
+Write-Host "AI_HARDWARE_REPORT: $OutputPath"
diff --git a/src/Workloads/_common/content-hashes.ps1 b/src/Workloads/_common/content-hashes.ps1
new file mode 100644
index 00000000..1e485a23
--- /dev/null
+++ b/src/Workloads/_common/content-hashes.ps1
@@ -0,0 +1,84 @@
+$Script:DevConfigWorkloadContentHashes = @{
+ '_common\ai-catalog.psd1' = '6665b7cf7259fc0837fe4786f519c5043885eded3304dc0fe788837b937f7804'
+ 'cuda\smoke.cu' = '6252383bda8856daa14c4f315961e17d4de3bdba1cba6bf4c09a5d0aa52a2a6f'
+ 'dotnet\configuration.winget' = 'cba2c6873cee7eff241b6d7698d773f8575cfc60a6e9d7dbb8986f4ecea5c048'
+ 'go\configuration.winget' = '552e6fe17baa47df8d1f429735cdd718bdb61b052a86cc47e8f8c6c2cd22232c'
+ 'intel-ai\openvino-smoke.py' = '0b71b5f5351dd7536c6352dc02ffd523fa89f2fa60a23ff27d45fd3e8f9c1326'
+ 'intel-ai\sycl-smoke.cpp' = '6ed0361d8374bcae060b84469b3b167738f016abc9f3b46ba5f19ad870bd74b7'
+ 'java\configuration.winget' = 'f36edd7ca81b9389ba490505892d6ca5068d5942a249a98f3997356300b8447e'
+ 'php\configuration.winget' = '34e040193202a8cb0e4cadcefaf5bd678e9fb775bf5ad428d94c78f326604c06'
+ 'powershell\configuration.winget' = 'b86d0cc7dea26309a14478e5a94304dc9b037fe32220bdc83da8318ed60e220d'
+ 'python\configuration.winget' = 'bfec7eaad24ba3c5913e7713dafef31d6ac62c53005cc3fb6688db33dbf66f13'
+ 'pytorch\smoke.py' = 'f28b8ca06e0a2832684f1342626c74904c13d575ce96282f86cf66f44d3eba68'
+ 'pytorch\triton-smoke.py' = 'a2d2a538b1b30315f249753ccad68d6977bf85d711e3e3541c52ae16268814f4'
+ 'pytorch\xpu-smoke.py' = '7cba1b04e7ecbce0c24a9188ff847cd3c2fd9792810b5fd4324728ad21786c50'
+ 'rocm\hip-smoke.cpp' = 'ece1ed905b3afc7ceb918cc3ff751d624b8d681444b1c507a74cfef3d326034d'
+ 'rust\configuration.winget' = 'ea58a4b6dfe1aedcc1af4674b69dd376609c67140ee6a60dba0060edb266c701'
+ 'sql\configuration.winget' = '99472e573c10316a17be12d45da834e22be4b1b774afc5c15fdefe3d63bb98e7'
+ 'typescript\configuration.winget' = '826e1755d85798e376baa00a9891c58fd2dd6da67d0f54486ad957202ee7c8b1'
+ 'winappcli\configuration.winget' = 'd61b53240fbdef910ea7a92250add1793ae599f57217c83052bd6aed490ead4c'
+ 'winforms\configuration.winget' = '39de8aee958e1e4989fd5b484a45a36537a7f8cb72dc966fa3afa4d0e8b9e34f'
+ 'winui\configuration.winget' = '0c39e677f5e44b90eb51cc19e529aed8251d90d59f6fbea9ee58bcbb3d5a11f2'
+}
+
+function Get-DevConfigCanonicalWorkloadHash {
+ param([Parameter(Mandatory)] [string] $Path)
+
+ $bytes = [IO.File]::ReadAllBytes($Path)
+ $stream = [IO.MemoryStream]::new()
+ try {
+ for ($index = 0; $index -lt $bytes.Length; $index++) {
+ if ($bytes[$index] -eq 13 -and
+ $index + 1 -lt $bytes.Length -and
+ $bytes[$index + 1] -eq 10) {
+ $stream.WriteByte(10)
+ $index++
+ } else {
+ $stream.WriteByte($bytes[$index])
+ }
+ }
+ $sha = [Security.Cryptography.SHA256]::Create()
+ try {
+ return ([BitConverter]::ToString($sha.ComputeHash($stream.ToArray()))).Replace('-', '').ToLowerInvariant()
+ } finally {
+ $sha.Dispose()
+ }
+ } finally {
+ $stream.Dispose()
+ }
+}
+
+function Assert-DevConfigWorkloadContent {
+ [CmdletBinding()]
+ param([Parameter(Mandatory)] [string] $WorkloadsRoot)
+
+ $root = (Get-Item -LiteralPath $WorkloadsRoot -Force).FullName.TrimEnd('\')
+ $actualFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -Force |
+ Where-Object { $_.Extension -ne '.ps1' })
+ $actualPaths = @($actualFiles | ForEach-Object {
+ $_.FullName.Substring($root.Length).TrimStart([char]'\')
+ })
+
+ $failures = [System.Collections.Generic.List[string]]::new()
+ foreach ($entry in $Script:DevConfigWorkloadContentHashes.GetEnumerator()) {
+ $path = Join-Path $root $entry.Key
+ if (-not (Test-Path -LiteralPath $path)) {
+ [void]$failures.Add("$($entry.Key) [missing]")
+ continue
+ }
+ $actualHash = Get-DevConfigCanonicalWorkloadHash -Path $path
+ if ($actualHash -ne $entry.Value) {
+ [void]$failures.Add("$($entry.Key) [SHA-256 mismatch]")
+ }
+ }
+ foreach ($path in $actualPaths) {
+ if (-not $Script:DevConfigWorkloadContentHashes.ContainsKey($path)) {
+ [void]$failures.Add("$path [not declared by signed content manifest]")
+ }
+ }
+
+ if ($failures.Count -gt 0) {
+ throw "The Workloads content manifest failed verification: $($failures -join '; ')"
+ }
+ Write-Host " Verified $($actualFiles.Count) non-PowerShell workload files." -ForegroundColor DarkGray
+}
diff --git a/src/Workloads/_common/direct-setup.ps1 b/src/Workloads/_common/direct-setup.ps1
new file mode 100644
index 00000000..be624559
--- /dev/null
+++ b/src/Workloads/_common/direct-setup.ps1
@@ -0,0 +1,229 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$stepsRoot = Join-Path $PSScriptRoot '..\..\windows-dev-config\steps'
+. (Join-Path $PSScriptRoot 'ai-support.ps1')
+. (Join-Path $stepsRoot '_environment.ps1')
+. (Join-Path $stepsRoot '_elevation.ps1')
+. (Join-Path $stepsRoot '_retry.ps1')
+. (Join-Path $stepsRoot '_step-runner.ps1')
+. (Join-Path $stepsRoot '_winget.ps1')
+
+Enable-AiUtf8Console
+
+function Write-AiPhase {
+ param(
+ [Parameter(Mandatory)] [string] $Name,
+ [string] $Detail = ''
+ )
+ Write-Host ''
+ Write-Host "=== $Name ===" -ForegroundColor Cyan
+ if ($Detail) {
+ Write-Host $Detail -ForegroundColor DarkGray
+ }
+}
+
+function Assert-AiAdministrator {
+ if (-not (Test-DevConfigIsAdmin)) {
+ throw 'This setup needs Administrator rights. Re-run it from an elevated PowerShell window, or launch it from Command Palette and accept the UAC prompt.'
+ }
+}
+
+function Ensure-AiWingetPackage {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [string] $Id,
+ [switch] $PlanOnly
+ )
+
+ if ($PlanOnly) {
+ return [pscustomobject]@{ Id = $Id; Action = 'install-or-upgrade'; Source = 'winget' }
+ }
+
+ Initialize-DevConfigWinGet
+ $action = Ensure-DevConfigWingetPackage -Id $Id
+ Update-DevConfigSessionPath
+ $evidence = Get-AiWingetPackageEvidence -Id $Id
+ return [pscustomobject]@{ Id = $Id; Action = $action; Source = 'winget'; Evidence = $evidence }
+}
+
+function Get-AiWingetPackageAction {
+ param([Parameter(Mandatory)] [ValidateSet('Absent', 'UpgradeAvailable', 'Current')] [string] $State)
+ switch ($State) {
+ 'Absent' { return 'install' }
+ 'UpgradeAvailable' { return 'upgrade' }
+ 'Current' { return 'skip' }
+ }
+}
+
+function Get-AiWingetPackageEvidence {
+ param([Parameter(Mandatory)] [string] $Id)
+
+ try {
+ if ($Script:DevConfigWinGetMode -eq 'Cli') {
+ return (Invoke-DevConfigWingetCli -Arguments @(
+ 'list', '--id', $Id, '--exact', '--source', 'winget', '--accept-source-agreements'
+ )).Output
+ }
+ $package = Get-WinGetPackage -Id $Id -Source winget -MatchOption EqualsCaseInsensitive
+ if (-not $package) { return $null }
+ return ConvertTo-AiWingetPackageEvidence -Package $package -RequestedId $Id
+ } catch {
+ Write-Warning "Could not collect WinGet evidence for '$Id': $($_.Exception.Message)"
+ return [ordered]@{ id = $Id; source = 'winget'; evidenceUnavailable = $true }
+ }
+}
+
+function Get-AiObjectPropertyValue {
+ param(
+ [Parameter(Mandatory)] $InputObject,
+ [Parameter(Mandatory)] [string[]] $Names
+ )
+ foreach ($name in $Names) {
+ $property = $InputObject.PSObject.Properties[$name]
+ if ($property) { return $property.Value }
+ }
+ return $null
+}
+
+function ConvertTo-AiWingetPackageEvidence {
+ param(
+ [Parameter(Mandatory)] $Package,
+ [Parameter(Mandatory)] [string] $RequestedId
+ )
+ $resolvedId = Get-AiObjectPropertyValue -InputObject $Package -Names @('Id', 'PackageIdentifier', 'PackageId')
+ if (-not $resolvedId) { $resolvedId = $RequestedId }
+ return [ordered]@{
+ id = [string]$resolvedId
+ name = [string](Get-AiObjectPropertyValue -InputObject $Package -Names @('Name', 'PackageName'))
+ installedVersion = [string](Get-AiObjectPropertyValue -InputObject $Package -Names @('InstalledVersion', 'Version'))
+ availableVersion = [string](Get-AiObjectPropertyValue -InputObject $Package -Names @('AvailableVersion', 'LatestVersion'))
+ updateAvailable = [bool](Get-AiObjectPropertyValue -InputObject $Package -Names @('IsUpdateAvailable', 'UpdateAvailable'))
+ source = 'winget'
+ }
+}
+
+function Ensure-AiVisualCppTools {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [switch] $PlanOnly
+ )
+
+ $package = Ensure-AiWingetPackage -Id 'Microsoft.VisualStudio.2022.BuildTools' -PlanOnly:$PlanOnly
+ if ($PlanOnly) {
+ return [pscustomobject]@{
+ Package = $package
+ Action = 'ensure-vctools-workload'
+ Architecture = $Architecture
+ }
+ }
+
+ try {
+ $compiler = Get-MsvcCompilerPath -Architecture $Architecture
+ return [pscustomobject]@{ Package = $package; Action = 'already-current'; Compiler = $compiler }
+ } catch {
+ Write-Host " Adding the $Architecture C++ Build Tools workload..." -ForegroundColor DarkCyan
+ }
+
+ $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
+ $installPath = [string](& $vswhere -latest -products Microsoft.VisualStudio.Product.BuildTools -property installationPath |
+ Select-Object -First 1)
+ $installPath = $installPath.Trim()
+ if (-not $installPath) {
+ throw 'Visual Studio Build Tools installation path could not be determined.'
+ }
+
+ $stagingRoot = Join-Path $env:ProgramFiles 'WindowsDeveloperConfig\Installers'
+ New-Item -ItemType Directory -Path $stagingRoot -Force | Out-Null
+ $bootstrapper = Join-Path $stagingRoot "vs_BuildTools-$([guid]::NewGuid().ToString('N')).exe"
+ Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_BuildTools.exe' -OutFile $bootstrapper -UseBasicParsing
+ $signature = Get-AuthenticodeSignature -LiteralPath $bootstrapper
+ $signerName = $signature.SignerCertificate.GetNameInfo(
+ [System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)
+ if ($signature.Status -ne 'Valid' -or $signerName -ne 'Microsoft Corporation') {
+ throw "Visual Studio bootstrapper signature validation failed: $($signature.Status), $signerName"
+ }
+ $arguments = @(
+ 'modify', '--installPath', "`"$installPath`"",
+ '--channelId', 'VisualStudio.17.Release',
+ '--productId', 'Microsoft.VisualStudio.Product.BuildTools',
+ '--add', 'Microsoft.VisualStudio.Workload.VCTools'
+ )
+ if ($Architecture -eq 'Arm64') {
+ $arguments += @('--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64')
+ }
+ $arguments += @('--includeRecommended', '--quiet', '--wait', '--norestart', '--nocache')
+ try {
+ $exitCode = Invoke-DevConfigProcess -FilePath $bootstrapper -Arguments $arguments -TimeoutSeconds 5400
+ if ($exitCode -notin @(0, 3010)) {
+ throw "Visual Studio Build Tools bootstrapper exited with code $exitCode. Review $env:TEMP\dd_*.log."
+ }
+ } finally {
+ [void](Remove-TemporaryFileWithRetry -Path $bootstrapper)
+ }
+
+ $compiler = Get-MsvcCompilerPath -Architecture $Architecture
+ return [pscustomobject]@{ Package = $package; Action = 'workload-added'; Compiler = $compiler }
+}
+
+function Ensure-AiCudaToolkit {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)] [ValidateSet('X64', 'Arm64')] [string] $Architecture,
+ [switch] $PlanOnly
+ )
+
+ $plan = Resolve-CudaInstallPlan -Architecture $Architecture -WindowsBuild (Get-WindowsBuildNumber)
+ if ($Architecture -eq 'X64') {
+ $package = Ensure-AiWingetPackage -Id 'Nvidia.CUDA' -PlanOnly:$PlanOnly
+ $nvcc = if ($PlanOnly) { $null } else { Get-CudaNvccPath -ToolkitVersion $null }
+ $versionOutput = if ($nvcc) { (& $nvcc --version 2>&1 | Out-String).Trim() } else { $null }
+ return [pscustomobject]@{
+ Action = $package.Action
+ ToolkitVersion = $null
+ Source = 'winget'
+ Nvcc = $nvcc
+ VersionEvidence = $versionOutput
+ PackageEvidence = $(if ($PlanOnly) { $null } else { $package.Evidence })
+ }
+ }
+ if ($PlanOnly) {
+ return [pscustomobject]@{
+ Action = 'install-or-verify-preview'
+ ToolkitVersion = $plan.ToolkitVersion
+ Source = 'direct'
+ Uri = $plan.InstallerUrl
+ Sha256 = $plan.InstallerSha256
+ }
+ }
+
+ try {
+ $existingNvcc = Get-CudaNvccPath -ToolkitVersion $plan.ToolkitVersion
+ return [pscustomobject]@{
+ Action = 'already-current'
+ ToolkitVersion = $plan.ToolkitVersion
+ Source = 'direct'
+ Nvcc = $existingNvcc
+ }
+ } catch {
+ Write-Host ' Installing NVIDIA CUDA Toolkit 13.4 Developer Preview for Windows ARM64...' -ForegroundColor DarkCyan
+ }
+
+ $cacheRoot = Join-Path $env:ProgramData 'WindowsDeveloperConfig\cache\nvidia-cuda\13.4.0'
+ $installer = Join-Path $cacheRoot 'cuda_13.4.0_windows_arm64.exe'
+ Install-VerifiedDownload -Uri $plan.InstallerUrl -Destination $installer -Sha256 $plan.InstallerSha256
+ Invoke-VerifiedLocalInstaller `
+ -Path $installer `
+ -Sha256 $plan.InstallerSha256 `
+ -SignerPattern 'NVIDIA' `
+ -ArgumentList @('-s') `
+ -SuccessExitCodes @(0, 3010)
+ $nvcc = Get-CudaNvccPath -ToolkitVersion $plan.ToolkitVersion
+ return [pscustomobject]@{
+ Action = 'installed'
+ ToolkitVersion = $plan.ToolkitVersion
+ Source = 'direct'
+ Nvcc = $nvcc
+ }
+}
diff --git a/src/Workloads/cuda/install.ps1 b/src/Workloads/cuda/install.ps1
new file mode 100644
index 00000000..9dc9b689
--- /dev/null
+++ b/src/Workloads/cuda/install.ps1
@@ -0,0 +1,222 @@
+<#
+.SYNOPSIS
+ Install the NVIDIA CUDA Toolkit and compile/execute a minimal GPU kernel.
+
+.PARAMETER ToolkitOnly
+ Permit installing/verifying nvcc when no usable NVIDIA GPU and driver are
+ present. By default the flow fails before installation when no NVIDIA GPU is
+ detected and fails after installation when nvidia-smi is not usable.
+
+.PARAMETER SkipWorkloadSmoke
+ Skip compiling and executing the CUDA kernel. The default proves that the
+ compiler, host toolchain, driver, and GPU work together.
+
+.PARAMETER DeviceIndex
+ Zero-based NVIDIA device index used for nvidia-smi qualification and kernel
+ execution on same-vendor multi-adapter systems.
+#>
+[CmdletBinding()]
+param(
+ [switch] $ToolkitOnly,
+ [switch] $SkipWorkloadSmoke,
+ [ValidateRange(0, 63)] [int] $DeviceIndex = 0,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$catalog = (Get-AiCatalog).Components
+$component = if ($architecture -eq 'Arm64') { $catalog.CudaArm64 } else { $catalog.CudaX64 }
+$report = New-AiWorkloadReport -Id 'cuda' -Request @{
+ ToolkitOnly = [bool]$ToolkitOnly
+ SkipWorkloadSmoke = [bool]$SkipWorkloadSmoke
+ DeviceIndex = $DeviceIndex
+ PlanOnly = [bool]$PlanOnly
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'cuda' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+try {
+ $plan = Resolve-CudaInstallPlan -Architecture $architecture -WindowsBuild (Get-WindowsBuildNumber)
+} catch {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($_.Exception.Message)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: cuda'
+ return
+ }
+ throw
+}
+if (-not $PlanOnly) { Assert-AiAdministrator }
+
+$gpu = Get-NvidiaGpu
+$driver = $null
+$driverError = $null
+if ($gpu) {
+ try {
+ $driver = Get-NvidiaDriverInfo -DeviceIndex $DeviceIndex
+ } catch {
+ $driverError = $_.Exception.Message
+ }
+}
+if (-not $gpu -and -not $ToolkitOnly) {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add('No NVIDIA GPU detected; default kernel acceptance would fail. Use -ToolkitOnly for compiler-only planning.')
+ } else {
+ throw "No NVIDIA GPU was detected. CUDA Toolkit can be installed without a GPU only with -ToolkitOnly; GPU execution requires supported NVIDIA hardware and a current driver."
+ }
+}
+if ($driverError) {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($driverError)
+ } else {
+ throw $driverError
+ }
+}
+if ($gpu -and -not $driver -and -not $driverError -and -not $ToolkitOnly) {
+ $message = "NVIDIA device index $DeviceIndex is present, but nvidia-smi did not report a usable driver. Install/update the NVIDIA driver and rerun."
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($message)
+ } else {
+ throw $message
+ }
+}
+if ($driver -and -not $ToolkitOnly) {
+ $hardwareError = if ($architecture -eq 'Arm64' -and
+ ($driver.DriverVersion -lt [version]'616.0' -or $driver.ComputeCapability.Major -lt 12)) {
+ "CUDA 13.4 ARM64 Developer Preview requires driver 616+ and compute capability 12.x; device index $DeviceIndex reports driver $($driver.DriverVersion), capability $($driver.ComputeCapability)."
+ } elseif ($architecture -eq 'X64' -and
+ ($driver.DriverVersion -lt [version]'580.0' -or $driver.ComputeCapability -lt [version]'7.5')) {
+ "The current stable CUDA 13 x64 flow requires driver 580+ and compute capability 7.5+; device index $DeviceIndex reports driver $($driver.DriverVersion), capability $($driver.ComputeCapability). Use -ToolkitOnly for compiler-only setup."
+ } else {
+ $null
+ }
+ if ($hardwareError) {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($hardwareError)
+ } else {
+ throw $hardwareError
+ }
+ }
+}
+
+Write-AiPhase -Name 'Plan' -Detail "$architecture / NVIDIA CUDA $($plan.ToolkitVersion)"
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $component.Vendor
+ architecture = $architecture
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ packageId = Get-AiCatalogValue -Entry $component -Name 'PackageId'
+ version = Get-AiCatalogValue -Entry $component -Name 'Version'
+ uri = Get-AiCatalogValue -Entry $component -Name 'Uri'
+ sha256 = Get-AiCatalogValue -Entry $component -Name 'Sha256'
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $component.CachePath
+ installPath = $component.InstallPath
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ promotionCandidate = Get-AiCatalogValue -Entry $component -Name 'PromotionCandidate'
+ action = if ($PlanOnly) { 'planned' } else { 'pending' }
+})
+
+$toolchain = Ensure-AiVisualCppTools -Architecture $architecture -PlanOnly:$PlanOnly
+Add-AiReportPhase -Report $report -Name 'host-compiler' -Status $(if ($PlanOnly) { 'planned' } else { 'ready' }) -Evidence $toolchain
+
+$cudaAcquisition = Ensure-AiCudaToolkit -Architecture $architecture -PlanOnly:$PlanOnly
+$report.acquisitions[0].action = $cudaAcquisition.Action
+if (-not $PlanOnly -and $architecture -eq 'X64') {
+ $report.acquisitions[0].packageEvidence = $cudaAcquisition.PackageEvidence
+}
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'cuda-kernel' -Status 'planned' -Evidence @{
+ source = (Join-Path $PSScriptRoot 'smoke.cu')
+ target = "NVIDIA device index $DeviceIndex"
+ driver = $driver
+ }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host $(if ($report.result.blockers.Count) { 'PLAN_UNSUPPORTED: cuda' } else { 'PLAN_OK: cuda' })
+ return
+}
+
+$nvcc = Get-CudaNvccPath -ToolkitVersion $plan.ToolkitVersion
+Invoke-CheckedCommand -FilePath $nvcc -ArgumentList @('--version') -DisplayName 'CUDA compiler verification'
+$nvccVersionEvidence = (& $nvcc --version 2>&1 | Out-String).Trim()
+if ($nvccVersionEvidence -match 'release\s+([0-9]+\.[0-9]+)') {
+ $report.acquisitions[0].version = $Matches[1]
+}
+$readiness = Get-CudaReadiness `
+ -ToolkitAvailable $true `
+ -NvidiaGpuPresent ([bool]$gpu) `
+ -DriverAvailable ([bool]$driver)
+
+Write-Host 'CUDA_TOOLKIT_READY: nvcc is installed and runnable.'
+if ($readiness.GpuReady) {
+ Write-Host "CUDA_GPU_READY: $($driver.Name), driver $($driver.DriverVersion), compute capability $($driver.ComputeCapability)."
+} elseif ($ToolkitOnly) {
+ Write-Warning "CUDA toolkit is ready, but GPU execution is not: $($readiness.Status). Install/update the NVIDIA driver and confirm 'nvidia-smi' succeeds."
+} else {
+ throw "CUDA Toolkit is installed, but no usable NVIDIA driver/GPU was reported by nvidia-smi. Update the NVIDIA driver, reboot if requested, and rerun this flow."
+}
+
+$kernelReady = $false
+if ($SkipWorkloadSmoke -or -not $readiness.GpuReady) {
+ Write-Warning 'CUDA_WORKLOAD_SMOKE_SKIPPED: the toolkit is installed, but a compiled GPU kernel was not executed.'
+} else {
+ $vsDevCmd = Get-VsDevCmdPath -Architecture $architecture
+ $temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-cuda-$([guid]::NewGuid().ToString('N'))"
+ New-Item -ItemType Directory -Path $temporary -Force | Out-Null
+ try {
+ $executable = Join-Path $temporary 'cuda-smoke.exe'
+ $compileCommand = Get-CudaKernelCompileCommand `
+ -Architecture $architecture `
+ -VsDevCmd $vsDevCmd `
+ -Nvcc $nvcc `
+ -Source (Join-Path $PSScriptRoot 'smoke.cu') `
+ -Output $executable
+ & $env:ComSpec /d /s /c $compileCommand
+ if ($LASTEXITCODE -ne 0) {
+ throw "CUDA smoke kernel compilation failed with exit code $LASTEXITCODE."
+ }
+ $output = (& $executable $DeviceIndex 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $output -notmatch '^CUDA_KERNEL_READY') {
+ throw "CUDA smoke kernel failed on the GPU (exit $LASTEXITCODE, output '$output')."
+ }
+ Write-Host 'CUDA_WORKLOAD_READY: compiled and executed a CUDA kernel on the detected GPU.'
+ $kernelReady = $true
+ $report.acceptance.kernel = [ordered]@{
+ compiled = $true
+ executed = $true
+ marker = 'CUDA_KERNEL_READY'
+ deviceIndex = $DeviceIndex
+ device = $driver.Name
+ computeCapability = $driver.ComputeCapability.ToString()
+ }
+ } finally {
+ if (Test-Path -LiteralPath $temporary) {
+ Remove-Item -LiteralPath $temporary -Recurse -Force
+ }
+ }
+}
+
+Add-AiReportPhase -Report $report -Name 'cuda-toolkit' -Status 'ready' -Evidence @{
+ nvcc = $nvcc
+ nvccVersion = $nvccVersionEvidence
+ driver = $driver
+}
+if ($plan.Preview) {
+ Write-Warning 'CUDA 13.4 for Windows ARM64 is an NVIDIA Developer Preview and is not intended for production certification or benchmarking.'
+}
+Complete-AiWorkloadReport -Report $report -Ready $kernelReady -Path $ReportPath
+Write-Host 'INSTALL_OK: cuda'
diff --git a/src/Workloads/cuda/smoke.cu b/src/Workloads/cuda/smoke.cu
new file mode 100644
index 00000000..4191a01c
--- /dev/null
+++ b/src/Workloads/cuda/smoke.cu
@@ -0,0 +1,45 @@
+#include
+
+#include
+#include
+
+__global__ void write_marker(int* value) {
+ *value = 42;
+}
+
+int main(int argc, char** argv) {
+ const int device_index = argc > 1 ? std::atoi(argv[1]) : 0;
+ int device_count = 0;
+ if (cudaGetDeviceCount(&device_count) != cudaSuccess || device_index < 0 || device_index >= device_count) {
+ return 5;
+ }
+ if (cudaSetDevice(device_index) != cudaSuccess) {
+ return 6;
+ }
+ cudaDeviceProp properties{};
+ if (cudaGetDeviceProperties(&properties, device_index) != cudaSuccess) {
+ return 7;
+ }
+ int* device_value = nullptr;
+ int host_value = 0;
+
+ if (cudaMalloc(&device_value, sizeof(int)) != cudaSuccess) {
+ return 1;
+ }
+ write_marker<<<1, 1>>>(device_value);
+ if (cudaDeviceSynchronize() != cudaSuccess) {
+ cudaFree(device_value);
+ return 2;
+ }
+ if (cudaMemcpy(&host_value, device_value, sizeof(int), cudaMemcpyDeviceToHost) != cudaSuccess) {
+ cudaFree(device_value);
+ return 3;
+ }
+ cudaFree(device_value);
+
+ if (host_value != 42) {
+ return 4;
+ }
+ std::printf("CUDA_KERNEL_READY device_index=%d device=%s\n", device_index, properties.name);
+ return 0;
+}
diff --git a/src/Workloads/foundry/install.ps1 b/src/Workloads/foundry/install.ps1
new file mode 100644
index 00000000..9340c095
--- /dev/null
+++ b/src/Workloads/foundry/install.ps1
@@ -0,0 +1,140 @@
+<#
+.SYNOPSIS
+ Install Foundry Local, acquire a small catalog model, and run text inference.
+
+.PARAMETER SkipModelSmoke
+ Skip the default qwen3-0.6b download and inference. The install then verifies
+ only the CLI and server and does not claim workload readiness.
+#>
+[CmdletBinding()]
+param(
+ [switch] $SkipModelSmoke,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$component = (Get-AiCatalog).Components.FoundryLocal
+$report = New-AiWorkloadReport -Id 'foundry' -Request @{
+ SkipModelSmoke = [bool]$SkipModelSmoke
+ PlanOnly = [bool]$PlanOnly
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'foundry' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+try {
+ $plan = Resolve-FoundryInstallPlan -Architecture $architecture -WindowsBuild (Get-WindowsBuildNumber)
+} catch {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($_.Exception.Message)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: foundry'
+ return
+ }
+ throw
+}
+if (-not $PlanOnly) { Assert-AiAdministrator }
+Write-Host "Foundry Local plan: $($plan.Architecture), WinML, CUDA dependency: $($plan.RequiresCuda)"
+
+$package = Ensure-AiWingetPackage -Id 'Microsoft.FoundryLocal' -PlanOnly:$PlanOnly
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $component.Vendor
+ architecture = $architecture
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ packageId = $component.PackageId
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $component.CachePath
+ installPath = $component.InstallPath
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ promotionCandidate = $component.PromotionCandidate
+ action = $package.Action
+ packageEvidence = $(if ($PlanOnly) { $null } else { $package.Evidence })
+})
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'model-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'planned' }) -Evidence @{
+ model = 'qwen3-0.6b'
+ selection = 'Foundry alias resolves the highest-priority hardware variant'
+ }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: foundry'
+ return
+}
+
+Invoke-CheckedCommand -FilePath 'foundry' -ArgumentList @('--version') -DisplayName 'Foundry Local CLI verification'
+$serverStatus = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments @('server', 'status')
+if ($serverStatus.ExitCode -ne 0) {
+ Write-Host 'Foundry Local server is not ready; restarting it once.'
+ Invoke-CheckedCommand -FilePath 'foundry' -ArgumentList @('server', 'restart') -DisplayName 'Foundry Local server restart'
+ Invoke-CheckedCommand -FilePath 'foundry' -ArgumentList @('server', 'status') -DisplayName 'Foundry Local server readiness'
+}
+
+$modelPlan = Get-FoundryModelSmokePlan
+$inferenceEvidence = $null
+if ($SkipModelSmoke) {
+ Write-Warning 'FOUNDRY_MODEL_SMOKE_SKIPPED: CLI and server are ready, but no model inference was performed.'
+} else {
+ $commands = Get-FoundryModelSmokeCommands -Model $modelPlan.Model -Marker $modelPlan.Marker
+ Write-Host "Downloading Foundry catalog model $($modelPlan.Model) (approximately $($modelPlan.ApproximateDownloadMb) MB, $($modelPlan.License))."
+ Invoke-CheckedCommand -FilePath 'foundry' -ArgumentList $commands.Download -DisplayName 'Foundry Local model download'
+ $modelInfoResult = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments @('model', 'info', $modelPlan.Model)
+ $modelInfo = $modelInfoResult.Output.Trim()
+ if ($modelInfoResult.ExitCode -ne 0 -or -not $modelInfo) {
+ throw "Foundry Local could not report the selected $($modelPlan.Model) hardware variant."
+ }
+ Write-Host $modelInfo
+ $logsBeforeResult = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments @('server', 'logs', '-n', '200')
+ if ($logsBeforeResult.ExitCode -ne 0) {
+ throw "Foundry Local could not capture the pre-inference server log boundary: $($logsBeforeResult.Output)"
+ }
+ $completeArguments = @($commands.Complete)
+ $completionResult = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments $completeArguments
+ $completion = $completionResult.Output
+ if ($completionResult.ExitCode -ne 0 -or $completion -notmatch [regex]::Escape($modelPlan.Marker)) {
+ throw "Foundry Local model inference did not produce marker '$($modelPlan.Marker)'. Output: $completion"
+ }
+ $cacheResult = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments @('cache', 'location')
+ if ($cacheResult.ExitCode -ne 0) {
+ throw "Foundry Local cache location failed: $($cacheResult.Output)"
+ }
+ $cache = Get-AiWindowsPathFromOutput -Text $cacheResult.Output
+ $logsResult = Invoke-DevConfigNativeCommand -FilePath 'foundry' -Arguments @('server', 'logs', '-n', '200')
+ if ($logsResult.ExitCode -ne 0) {
+ throw "Foundry Local could not capture post-inference provider evidence: $($logsResult.Output)"
+ }
+ $logs = $logsResult.Output.Trim()
+ $currentInferenceLogs = Get-AiAppendedLogText -Before $logsBeforeResult.Output -After $logsResult.Output
+ $providerEvidence = Get-FoundryExecutionProviderEvidence -ModelInfo $modelInfo -ServerLogs $currentInferenceLogs
+ $report.acceptance.inference = [ordered]@{
+ modelAlias = $modelPlan.Model
+ modelInfo = $modelInfo
+ marker = $modelPlan.Marker
+ outputMatched = $true
+ cache = $cache
+ serverLogTail = $logs
+ currentInferenceProviderLogs = $currentInferenceLogs
+ selectedExecutionProvider = $providerEvidence.SelectedProvider
+ selectedDevice = $providerEvidence.SelectedDevice
+ observedExecutionProviders = $providerEvidence.ObservedProviders
+ evidenceClass = 'resolved-variant-plus-successful-inference'
+ }
+ $inferenceEvidence = $report.acceptance.inference
+ $report.result.fallbackUsed = $providerEvidence.CpuFallback
+ Write-Host "FOUNDRY_READY: $($modelPlan.Model) downloaded to '$cache' and generated the deterministic marker using $($providerEvidence.SelectedProvider)."
+}
+Add-AiReportPhase -Report $report -Name 'foundry-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'ready' }) -Evidence $inferenceEvidence
+Complete-AiWorkloadReport -Report $report -Ready (-not $SkipModelSmoke) -Path $ReportPath
+Write-Host 'INSTALL_OK: foundry'
diff --git a/src/Workloads/intel-ai/install.ps1 b/src/Workloads/intel-ai/install.ps1
new file mode 100644
index 00000000..33e31bca
--- /dev/null
+++ b/src/Workloads/intel-ai/install.ps1
@@ -0,0 +1,198 @@
+<#
+.SYNOPSIS
+ Install and verify Intel OpenVINO acceleration, with optional oneAPI/SYCL tooling.
+
+.PARAMETER OpenVinoDeviceId
+ Optional exact OpenVINO device such as GPU.1 or NPU.0. Device still declares
+ the required class used for prerequisite validation.
+
+.PARAMETER SyclDeviceSelector
+ Optional ONEAPI_DEVICE_SELECTOR value such as level_zero:gpu:1 for
+ same-vendor multi-adapter SYCL execution.
+#>
+[CmdletBinding()]
+param(
+ [ValidateSet('Auto', 'CPU', 'GPU', 'NPU')] [string] $Device = 'Auto',
+ [ValidateSet('OpenVINO', 'SYCL', 'Full')] [string] $Profile = 'OpenVINO',
+ [string] $OpenVinoDeviceId = '',
+ [string] $SyclDeviceSelector = '',
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$intelGpu = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
+ Where-Object { $_.PNPDeviceID -match 'VEN_8086' -or $_.Name -match 'Intel' } |
+ Select-Object -First 1)
+$intelNpu = @(Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue |
+ Where-Object { $_.FriendlyName -match 'Intel.*(AI Boost|NPU)|Neural Processing Unit' } |
+ Select-Object -First 1)
+$intelPlan = $null
+$planError = $null
+try {
+ $intelPlan = Resolve-IntelAiPlan `
+ -Architecture $architecture `
+ -Device $Device `
+ -Profile $Profile `
+ -IntelGpuPresent ($intelGpu.Count -gt 0) `
+ -IntelNpuPresent ($intelNpu.Count -gt 0)
+} catch {
+ $planError = $_.Exception.Message
+}
+$selectedDevice = if ($intelPlan) { $intelPlan.Device } else { $Device }
+$openVinoTarget = if ($OpenVinoDeviceId) { $OpenVinoDeviceId } else { $selectedDevice }
+if (-not $planError -and $OpenVinoDeviceId -and $Profile -in @('OpenVINO', 'Full') -and
+ $OpenVinoDeviceId -notmatch "^$([regex]::Escape($selectedDevice))(\.|$)") {
+ $planError = "OpenVINO device '$OpenVinoDeviceId' does not match the requested $selectedDevice device class."
+}
+$catalog = (Get-AiCatalog).Components
+$component = $catalog.IntelOpenVino
+$report = New-AiWorkloadReport -Id 'intel-ai' -Request @{
+ Device = $Device
+ SelectedDevice = $selectedDevice
+ Profile = $Profile
+ OpenVinoDeviceId = $OpenVinoDeviceId
+ SyclDeviceSelector = $SyclDeviceSelector
+ PlanOnly = [bool]$PlanOnly
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'intel-ai' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+if (-not $PlanOnly) { Assert-AiAdministrator }
+$openVinoAcquisitionIndex = $null
+$oneApiAcquisitionIndex = $null
+if ($Profile -in @('OpenVINO', 'Full')) {
+ $openVinoAcquisitionIndex = $report.acquisitions.Count
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $component.Vendor
+ architecture = $architecture
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ packages = $component.Packages
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $component.CachePath
+ installPath = $component.InstallPath
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ action = $(if ($planError) { 'blocked' } else { 'planned' })
+ })
+}
+if ($Profile -in @('SYCL', 'Full')) {
+ $oneApiAcquisitionIndex = $report.acquisitions.Count
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $catalog.IntelOneApi.Component
+ maturity = $catalog.IntelOneApi.Maturity
+ sourceType = 'winget'
+ packageId = $catalog.IntelOneApi.PackageId
+ version = $catalog.IntelOneApi.Version
+ versionPolicy = $catalog.IntelOneApi.VersionPolicy
+ integrity = $catalog.IntelOneApi.Integrity
+ cachePath = $catalog.IntelOneApi.CachePath
+ installPath = $catalog.IntelOneApi.InstallPath
+ reasonNormalChannelInsufficient = $catalog.IntelOneApi.NormalChannelLimitation
+ expectedStableSource = $catalog.IntelOneApi.ExpectedStableSource
+ migrationTrigger = $catalog.IntelOneApi.MigrationTrigger
+ cleanupUpgrade = $catalog.IntelOneApi.CleanupUpgrade
+ action = $(if ($planError) { 'blocked' } else { 'planned' })
+ })
+}
+if ($planError) {
+ [void]$report.result.blockers.Add($planError)
+}
+if ($report.result.blockers.Count -gt 0) {
+ if ($PlanOnly) {
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: intel-ai'
+ return
+ }
+ throw ($report.result.blockers -join ' ')
+}
+if ($Profile -in @('OpenVINO', 'Full')) {
+ $pythonPackage = Ensure-AiWingetPackage -Id 'Python.Python.3.13' -PlanOnly:$PlanOnly
+ Set-AiAcquisitionAction -Report $report -Index $openVinoAcquisitionIndex -Action $(if ($PlanOnly) { 'planned' } else { 'pending' })
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = 'Python 3.13'
+ sourceType = 'winget'
+ packageId = 'Python.Python.3.13'
+ action = $pythonPackage.Action
+ packageEvidence = $(if ($PlanOnly) { $null } else { $pythonPackage.Evidence })
+ })
+}
+if ($Profile -in @('SYCL', 'Full')) {
+ $oneApi = Ensure-AiWingetPackage -Id 'Intel.OneAPI.Toolkit' -PlanOnly:$PlanOnly
+ Set-AiAcquisitionAction -Report $report -Index $oneApiAcquisitionIndex -Action $oneApi.Action
+}
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'openvino-inference' -Status $(if ($Profile -eq 'SYCL') { 'skipped' } else { 'planned' }) -Evidence @{ device = $openVinoTarget }
+ Add-AiReportPhase -Report $report -Name 'sycl-kernel' -Status $(if ($Profile -eq 'OpenVINO') { 'skipped' } else { 'planned' }) -Evidence @{ device = 'GPU'; selector = $SyclDeviceSelector }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: intel-ai'
+ return
+}
+
+if ($Profile -in @('OpenVINO', 'Full')) {
+ $python = Get-Python313Path -Architecture X64
+ $root = Join-Path $env:LOCALAPPDATA 'DevConfig\intel-ai\openvino'
+ $venv = Join-Path $root '.venv'
+ $statePath = Join-Path $root 'install-state.json'
+ $expectedPackages = @{
+ openvino = '2026.3.1'
+ 'openvino-tokenizers' = '2026.3.1.0'
+ 'openvino-genai' = '2026.3.1.0'
+ }
+ New-Item -ItemType Directory -Path $root -Force | Out-Null
+ if (-not (Test-Path (Join-Path $venv 'Scripts\python.exe'))) {
+ Invoke-CheckedCommand -FilePath $python -ArgumentList @('-m', 'venv', $venv) -DisplayName 'OpenVINO environment creation'
+ }
+ $venvPython = Join-Path $venv 'Scripts\python.exe'
+ $packagesCurrent = (Test-Path -LiteralPath $statePath) -and
+ (Test-PythonDistributionVersions -PythonPath $venvPython -Expected $expectedPackages)
+ if (-not $packagesCurrent) {
+ $openvinoArguments = @('-m', 'pip', 'install', '--only-binary=:all:') + @($component.Packages)
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList $openvinoArguments -DisplayName 'OpenVINO Runtime/GenAI installation'
+ Set-Content -LiteralPath $statePath -Value ($expectedPackages | ConvertTo-Json -Compress) -Encoding ascii
+ } else {
+ Write-Host 'OPENVINO_PACKAGES_CURRENT: skipping package resolution and installation.'
+ }
+ $openvinoEvidence = (& $venvPython (Join-Path $PSScriptRoot 'openvino-smoke.py') $openVinoTarget 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $openvinoEvidence -notmatch '^OPENVINO_SMOKE=') {
+ throw "OpenVINO $openVinoTarget inference failed: $openvinoEvidence"
+ }
+ $report.acceptance.openvino = $openvinoEvidence
+ Set-AiAcquisitionAction -Report $report -Index $openVinoAcquisitionIndex -Action $(if ($packagesCurrent) { 'already-current' } else { 'installed-or-upgraded' })
+}
+
+if ($Profile -in @('SYCL', 'Full')) {
+ [void](Ensure-AiVisualCppTools -Architecture X64)
+ $setvars = Join-Path ${env:ProgramFiles(x86)} 'Intel\oneAPI\setvars.bat'
+ if (-not (Test-Path $setvars)) { throw "oneAPI setvars.bat was not found at '$setvars'." }
+ $temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-sycl-$([guid]::NewGuid().ToString('N'))"
+ New-Item -ItemType Directory -Path $temporary -Force | Out-Null
+ try {
+ $output = Join-Path $temporary 'sycl-smoke.exe'
+ $selectorPrefix = if ($SyclDeviceSelector) { "set `"ONEAPI_DEVICE_SELECTOR=$SyclDeviceSelector`" && " } else { '' }
+ $command = "$selectorPrefix" + "call `"$setvars`" >nul && icpx -fsycl `"$PSScriptRoot\sycl-smoke.cpp`" -o `"$output`" && `"$output`""
+ $syclEvidence = (& $env:ComSpec /d /s /c $command 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $syclEvidence -notmatch 'SYCL_DEVICE_READY:') {
+ throw "oneAPI SYCL GPU kernel failed: $syclEvidence"
+ }
+ $report.acceptance.sycl = [ordered]@{ selector = $SyclDeviceSelector; evidence = $syclEvidence }
+ } finally {
+ Remove-Item -LiteralPath $temporary -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
+Complete-AiWorkloadReport -Report $report -Ready $true -Path $ReportPath
+Write-Host "INTEL_AI_READY: profile=$Profile device=$selectedDevice"
+Write-Host 'INSTALL_OK: intel-ai'
diff --git a/src/Workloads/intel-ai/openvino-smoke.py b/src/Workloads/intel-ai/openvino-smoke.py
new file mode 100644
index 00000000..bc007453
--- /dev/null
+++ b/src/Workloads/intel-ai/openvino-smoke.py
@@ -0,0 +1,34 @@
+import json
+import sys
+
+import numpy as np
+import openvino as ov
+
+
+requested = sys.argv[1]
+core = ov.Core()
+available = list(core.available_devices)
+if requested not in available:
+ raise RuntimeError(f"{requested} is unavailable; devices={available}")
+
+x = ov.opset13.parameter([4], np.float32, name="x")
+one = ov.opset13.constant(np.ones(4, dtype=np.float32))
+model = ov.Model([ov.opset13.add(x, one)], [x], "intel_ai_smoke")
+compiled = core.compile_model(model, requested)
+actual = compiled([np.arange(4, dtype=np.float32)])[0]
+expected = np.array([1, 2, 3, 4], dtype=np.float32)
+if not np.array_equal(actual, expected):
+ raise RuntimeError(f"unexpected output: {actual}")
+
+print(
+ "OPENVINO_SMOKE="
+ + json.dumps(
+ {
+ "requested_device": requested,
+ "full_device_name": core.get_property(requested, "FULL_DEVICE_NAME"),
+ "available_devices": available,
+ "output_verified": True,
+ },
+ sort_keys=True,
+ )
+)
diff --git a/src/Workloads/intel-ai/sycl-smoke.cpp b/src/Workloads/intel-ai/sycl-smoke.cpp
new file mode 100644
index 00000000..4b7423a0
--- /dev/null
+++ b/src/Workloads/intel-ai/sycl-smoke.cpp
@@ -0,0 +1,23 @@
+#include
+
+#include
+#include
+
+int main() {
+ sycl::queue queue{sycl::gpu_selector_v};
+ std::vector values(16, 0);
+ {
+ sycl::buffer buffer(values.data(), sycl::range<1>(values.size()));
+ queue.submit([&](sycl::handler& handler) {
+ auto output = buffer.get_access(handler);
+ handler.parallel_for(sycl::range<1>(values.size()), [=](sycl::id<1> i) {
+ output[i] = static_cast(i[0]) + 1;
+ });
+ });
+ }
+ for (std::size_t i = 0; i < values.size(); ++i) {
+ if (values[i] != static_cast(i) + 1) return 2;
+ }
+ std::cout << "SYCL_DEVICE_READY:" << queue.get_device().get_info() << '\n';
+ return 0;
+}
diff --git a/src/Workloads/llama.cpp/coding-demo.ps1 b/src/Workloads/llama.cpp/coding-demo.ps1
new file mode 100644
index 00000000..32d2db99
--- /dev/null
+++ b/src/Workloads/llama.cpp/coding-demo.ps1
@@ -0,0 +1,96 @@
+<#
+.SYNOPSIS
+ Run an optional coding demonstration with a pinned 1.5B Qwen coder model.
+
+.DESCRIPTION
+ This is intentionally separate from install.ps1. The default llama.cpp
+ validation model is about 397 MB; this optional coding model is about 1.04 GB.
+#>
+[CmdletBinding()]
+param([string] $ReportPath = '')
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$runtime = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\runtime'
+$llamaCli = Join-Path $runtime 'llama-cli.exe'
+$statePath = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\selected-backend.json'
+if (-not (Test-Path -LiteralPath $llamaCli) -or -not (Test-Path -LiteralPath $statePath)) {
+ throw 'Run llama.cpp\install.ps1 before the optional coding demonstration.'
+}
+$state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json
+$plan = Get-LlamaCodingDemoPlan
+$prompt = 'Write only valid Python code defining group_anagrams(words: list[str]) -> list[list[str]].'
+$modelDirectory = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\models'
+$modelPath = Join-Path $modelDirectory $plan.FileName
+
+Install-VerifiedDownload `
+ -Uri $plan.Url `
+ -Destination $modelPath `
+ -Sha256 $plan.Sha256 `
+ -ExpectedSize $plan.Size
+
+$arguments = @(
+ '--model', $modelPath,
+ '--single-turn',
+ '--prompt', $prompt,
+ '--reasoning', 'off',
+ '--seed', '42',
+ '--temperature', '0.2',
+ '--top-p', '0.8',
+ '--top-k', '20',
+ '--predict', '384',
+ '--no-display-prompt',
+ '--simple-io',
+ '--log-disable',
+ '-ngl', $(if ($state.backend -eq 'CPU') { '0' } else { '999' })
+)
+if ($state.backend -eq 'CPU') {
+ $arguments += @('--device', 'none')
+} elseif ($state.requestedDevice) {
+ $arguments += @('--device', [string]$state.requestedDevice)
+}
+
+$result = Invoke-AiNativeCommandSeparated `
+ -FilePath $llamaCli `
+ -Arguments $arguments `
+ -TimeoutSeconds 1800
+$output = $result.StandardOutput.Trim()
+if ($result.ExitCode -ne 0 -or $output -notmatch '(?m)^\s*(?:```python\s*)?def\s+group_anagrams\s*\(') {
+ throw "Coding model did not return the requested Python function. Output: $output"
+}
+
+if ($ReportPath) {
+ $report = New-AiWorkloadReport -Id 'llama.cpp-coding-demo' -Request @{
+ Prompt = $prompt
+ Backend = $state.backend
+ RequestedDevice = $state.requestedDevice
+ }
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = 'Qwen2.5-Coder-1.5B-Instruct GGUF'
+ vendor = 'Qwen'
+ maturity = 'stable-model'
+ sourceType = 'huggingface-immutable-revision'
+ repository = $plan.Repository
+ revision = $plan.Revision
+ file = $plan.FileName
+ sha256 = $plan.Sha256
+ bytes = $plan.Size
+ cachePath = $modelPath
+ action = 'already-current-or-downloaded'
+ })
+ $report.acceptance.coding = [ordered]@{
+ backend = $state.backend
+ expectedDevice = $state.expectedDevice
+ model = $plan.FileName
+ outputContainsFunction = $true
+ output = $output
+ }
+ Complete-AiWorkloadReport -Report $report -Ready $true -Path $ReportPath
+}
+
+Write-Host $output
+Write-Host "CODING_DEMO_READY: model=$($plan.FileName), backend=$($state.backend)"
diff --git a/src/Workloads/llama.cpp/install.ps1 b/src/Workloads/llama.cpp/install.ps1
new file mode 100644
index 00000000..c2dc821e
--- /dev/null
+++ b/src/Workloads/llama.cpp/install.ps1
@@ -0,0 +1,272 @@
+<#
+.SYNOPSIS
+ Install a hardware-selected llama.cpp backend, acquire a pinned small GGUF,
+ and prove the selected backend with benchmark and inference evidence.
+
+.PARAMETER Backend
+ Auto prefers supported NVIDIA CUDA, AMD ROCm, Intel SYCL, Qualcomm Adreno
+ OpenCL, x64 Vulkan, then CPU. OpenVINO is an explicit Windows x64 option.
+ Explicit backend requests fail instead of silently selecting another backend.
+
+.PARAMETER Device
+ Optional llama.cpp runtime device identifier such as CUDA0, Vulkan0, or SYCL0.
+ Use this to target a same-vendor secondary adapter. When omitted, the selected
+ backend chooses its default device and the actual device is recorded.
+
+.PARAMETER SkipModelSmoke
+ Skip the default Qwen3-0.6B GGUF download and inference. The install then
+ verifies only the CLI and does not claim workload readiness.
+#>
+[CmdletBinding()]
+param(
+ [ValidateSet('Auto', 'CUDA', 'ROCm', 'SYCL', 'OpenVINO', 'Vulkan', 'OpenCL', 'CPU')] [string] $Backend = 'Auto',
+ [string] $Device = '',
+ [switch] $SkipModelSmoke,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$driver = Get-NvidiaDriverInfo
+$amdGpuName = Get-AmdGpuName
+$amdGfxTarget = if ($amdGpuName) { Get-AmdGfxTarget -GpuName $amdGpuName } else { $null }
+$intelGpuName = Get-IntelGpuName
+$qualcommGpuName = Get-QualcommGpuName
+$hasOpenCl = Test-AiOpenClRuntimeAvailable
+$vulkanGpuName = Get-VulkanGpuName
+$hasVulkan = Test-AiVulkanRuntimeAvailable -GpuName $vulkanGpuName
+$component = (Get-AiCatalog).Components.LlamaCppRolling
+$report = New-AiWorkloadReport -Id 'llama.cpp' -Request @{
+ Backend = $Backend
+ Device = $Device
+ SkipModelSmoke = [bool]$SkipModelSmoke
+ PlanOnly = [bool]$PlanOnly
+ SelectedBackend = $null
+ DetectedNvidiaDevice = $(if ($driver) { $driver.Name } else { $null })
+ NvidiaDriverVersion = $(if ($driver) { $driver.DriverVersion.ToString() } else { $null })
+ NvidiaComputeCapability = $(if ($driver) { $driver.ComputeCapability.ToString() } else { $null })
+ DetectedAmdDevice = $amdGpuName
+ DetectedIntelDevice = $intelGpuName
+ DetectedQualcommDevice = $qualcommGpuName
+ OpenClAvailable = $hasOpenCl
+ VulkanAvailable = $hasVulkan
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'llama.cpp' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+try {
+ $plan = Resolve-LlamaCppInstallPlan `
+ -Architecture $architecture `
+ -Backend $Backend `
+ -HasNvidia ([bool]$driver) `
+ -DriverVersion $(if ($driver) { $driver.DriverVersion } else { [version]'0.0' }) `
+ -ComputeCapability $(if ($driver) { $driver.ComputeCapability } else { [version]'0.0' }) `
+ -NvidiaGpuName $(if ($driver) { $driver.Name } else { $null }) `
+ -AmdGpuName $amdGpuName `
+ -AmdGfxTarget $amdGfxTarget `
+ -IntelGpuName $intelGpuName `
+ -QualcommGpuName $qualcommGpuName `
+ -HasOpenCl $hasOpenCl `
+ -HasVulkan $hasVulkan `
+ -VulkanGpuName $vulkanGpuName
+} catch {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($_.Exception.Message)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: llama.cpp'
+ return
+ }
+ throw
+}
+$report.request.SelectedBackend = $plan.Backend
+$report.request.SelectedVendor = $plan.Vendor
+$report.request.SelectedDevice = $plan.DeviceName
+$report.request.SelectedRuntime = $plan.Runtime
+$report.result.fallbackUsed = $Backend -eq 'Auto' -and $plan.Backend -in @('Vulkan', 'CPU')
+if ($report.result.fallbackUsed) {
+ [void]$report.result.warnings.Add("Auto selected the compatibility fallback '$($plan.Backend)'; this is not reported as vendor-native acceleration.")
+}
+if (-not $PlanOnly) { Assert-AiAdministrator }
+$legacyDestination = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp'
+$destination = Join-Path $legacyDestination 'runtime'
+$assetCache = Join-Path $legacyDestination 'asset-cache'
+if ($PlanOnly) {
+ $acquisition = [pscustomobject]@{
+ Action = 'resolve-rolling-release'
+ Tag = $null
+ Assets = @()
+ Source = 'github'
+ CacheDirectory = $assetCache
+ }
+} else {
+ $acquisition = Install-VerifiedGitHubReleaseAssets `
+ -Repository $component.Repository `
+ -AssetPatterns $plan.AssetPatterns `
+ -Destination $destination `
+ -VersionMarker '.devconfig-version' `
+ -RequiredFile @('llama-cli.exe', 'llama-bench.exe') `
+ -CacheDirectory $assetCache
+ Remove-UserPathEntry -Path $legacyDestination
+ Add-UserPathEntry -Path $destination -Prepend
+ $llamaCli = Join-Path $destination 'llama-cli.exe'
+ $llamaBench = Join-Path $destination 'llama-bench.exe'
+ if (-not (Test-Path -LiteralPath $llamaCli) -or -not (Test-Path -LiteralPath $llamaBench)) {
+ throw "The verified $($acquisition.Tag) $($plan.Runtime) asset set was extracted to '$destination', but required llama.cpp executables were not found."
+ }
+}
+$assetIdentity = @($acquisition.Assets | ForEach-Object {
+ [ordered]@{
+ name = $_.name
+ sha256 = ([string]$_.digest).Substring(7)
+ bytes = $_.size
+ cachePath = Join-Path (Join-Path $assetCache $acquisition.Tag) $_.name
+ }
+})
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $plan.Vendor
+ architecture = $architecture
+ maturity = $plan.Maturity
+ sourceType = $component.SourceType
+ repository = $component.Repository
+ backend = $plan.Backend
+ runtime = $plan.Runtime
+ selectedDevice = $plan.DeviceName
+ requestedRuntimeDevice = $Device
+ driverPrecondition = $(if ($plan.Backend -eq 'CUDA' -and $driver) { "NVIDIA $($driver.DriverVersion), compute capability $($driver.ComputeCapability)" } else { 'Use the installed vendor display/compute driver reported in host.gpus; this flow does not replace GPU drivers.' })
+ amdGfxTarget = $plan.AmdGfxTarget
+ assetPatterns = $plan.AssetPatterns
+ resolvedTag = $acquisition.Tag
+ resolvedAssets = $assetIdentity
+ versionPolicy = $plan.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $assetCache
+ installPath = $destination
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ action = $acquisition.Action
+})
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'model-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'planned' }) -Evidence @{
+ model = 'Qwen3-0.6B-Q4_K_M.gguf'
+ backend = $plan.Backend
+ runtime = $plan.Runtime
+ device = $plan.DeviceName
+ }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: llama.cpp'
+ return
+}
+
+Invoke-CheckedCommand -FilePath $llamaCli -ArgumentList @('--version') -DisplayName 'llama.cpp CLI verification'
+Invoke-CheckedCommand -FilePath $llamaCli -ArgumentList @('--help') -DisplayName 'llama.cpp help verification'
+$statePath = Join-Path $legacyDestination 'selected-backend.json'
+Write-DevConfigTextFile -Path $statePath -Content ([ordered]@{
+ backend = $plan.Backend
+ runtime = $plan.Runtime
+ expectedDevice = $plan.DeviceName
+ requestedDevice = $Device
+} | ConvertTo-Json -Compress)
+
+$modelPlan = Get-LlamaModelSmokePlan
+$inferenceEvidence = $null
+if ($SkipModelSmoke) {
+ Write-Warning 'LLAMA_CPP_MODEL_SMOKE_SKIPPED: CLI is ready, but no model inference was performed.'
+} else {
+ $modelDirectory = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\models'
+ $modelPath = Join-Path $modelDirectory $modelPlan.FileName
+ Write-Host "Downloading pinned $($modelPlan.Repository) model $($modelPlan.FileName) (approximately $([math]::Round($modelPlan.Size / 1MB)) MB, $($modelPlan.License))."
+ Install-VerifiedDownload `
+ -Uri $modelPlan.Url `
+ -Destination $modelPath `
+ -Sha256 $modelPlan.Sha256 `
+ -ExpectedSize $modelPlan.Size
+ $benchArguments = @(
+ '-m', $modelPath,
+ '-ngl', $(if ($plan.Backend -eq 'CPU') { '0' } else { '999' }),
+ '-p', '32', '-n', '1', '-r', '1', '-o', 'json', '-v'
+ )
+ if ($plan.Backend -eq 'CPU') {
+ $benchArguments += @('--device', 'none')
+ } elseif ($Device) {
+ $benchArguments += @('--device', $Device)
+ }
+ $benchmarkResult = Invoke-AiNativeCommandSeparated `
+ -FilePath $llamaBench `
+ -Arguments $benchArguments `
+ -TimeoutSeconds 300
+ $benchmark = $benchmarkResult.StandardOutput.Trim()
+ if ($benchmarkResult.ExitCode -ne 0) {
+ throw "llama-bench failed while collecting backend evidence (exit $($benchmarkResult.ExitCode)): $($benchmarkResult.StandardError)"
+ }
+ $parsedBenchmark = ConvertFrom-AiJsonArrayWithDiagnostics -Json $benchmark -Diagnostics $benchmarkResult.StandardError
+ $backendEvidence = Get-LlamaBenchmarkBackendEvidence `
+ -Data @($parsedBenchmark.Data) `
+ -Diagnostics $parsedBenchmark.Diagnostics `
+ -Backend $plan.Backend `
+ -ExpectedDeviceName $(if ($Device) { $null } else { $plan.DeviceName }) `
+ -RequestedDevice $Device
+ $arguments = Get-LlamaInferenceArguments -ModelPath $modelPath -Marker $modelPlan.Marker
+ $arguments += @('-ngl', $(if ($plan.Backend -eq 'CPU') { '0' } else { '999' }))
+ if ($plan.Backend -eq 'CPU') {
+ $arguments += @('--device', 'none')
+ } elseif ($Device) {
+ $arguments += @('--device', $Device)
+ }
+ $inferenceResult = Invoke-AiNativeCommandSeparated `
+ -FilePath $llamaCli `
+ -Arguments $arguments `
+ -TimeoutSeconds 300
+ $output = @(
+ $inferenceResult.StandardOutput
+ $inferenceResult.StandardError
+ ) -join "`n"
+ $output = $output.Trim()
+ if ($inferenceResult.ExitCode -ne 0 -or $output -notmatch [regex]::Escape($modelPlan.Marker)) {
+ throw "llama.cpp model inference did not produce marker '$($modelPlan.Marker)' (exit $($inferenceResult.ExitCode)). Output: $output"
+ }
+ $report.acceptance.inference = [ordered]@{
+ model = $modelPlan.FileName
+ modelSha256 = $modelPlan.Sha256
+ modelBytes = $modelPlan.Size
+ modelLicense = $modelPlan.License
+ marker = $modelPlan.Marker
+ backendPlan = $plan.Backend
+ runtimePlan = $plan.Runtime
+ selectedVendor = $plan.Vendor
+ selectedDevice = $plan.DeviceName
+ requestedRuntimeDevice = $Device
+ requestedRuntimeDevices = $backendEvidence.RequestedDevices
+ actualRuntimeDevices = $backendEvidence.GpuInfo
+ amdGfxTarget = $plan.AmdGfxTarget
+ backendEvidence = $backendEvidence
+ benchmark = $parsedBenchmark.Data
+ benchmarkJson = $parsedBenchmark.Json
+ benchmarkDiagnostics = $parsedBenchmark.Diagnostics
+ benchmarkJsonRepaired = $parsedBenchmark.JsonRepaired
+ }
+ $inferenceEvidence = [ordered]@{
+ model = $modelPlan.FileName
+ marker = $modelPlan.Marker
+ backend = $plan.Backend
+ runtime = $plan.Runtime
+ device = $backendEvidence.GpuInfo
+ hardwareAccelerated = $backendEvidence.HardwareAccelerated
+ actualOffloadedLayers = $backendEvidence.ActualOffloadedLayers
+ benchmarkJsonRepaired = $parsedBenchmark.JsonRepaired
+ }
+ Write-Host "LLAMA_CPP_READY: architecture=$architecture, backend=$($plan.Backend), runtime=$($plan.Runtime), device=$($backendEvidence.GpuInfo -join ','), model=$($modelPlan.FileName), sha256=$($modelPlan.Sha256)."
+}
+Add-AiReportPhase -Report $report -Name 'llama-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'ready' }) -Evidence $inferenceEvidence
+Complete-AiWorkloadReport -Report $report -Ready (-not $SkipModelSmoke) -Path $ReportPath
+Write-Host 'INSTALL_OK: llama.cpp'
diff --git a/src/Workloads/local-ai/install.ps1 b/src/Workloads/local-ai/install.ps1
new file mode 100644
index 00000000..55f70181
--- /dev/null
+++ b/src/Workloads/local-ai/install.ps1
@@ -0,0 +1,69 @@
+<#
+.SYNOPSIS
+ Prepare the major dependencies for a runnable local AI development scenario.
+
+.DESCRIPTION
+ Detects hardware, installs a contained PyTorch backend, runs tensor and neural
+ model acceptance, and optionally installs one local model runtime. It does not
+ install every vendor SDK or every model runtime.
+#>
+[CmdletBinding()]
+param(
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $Backend = 'Auto',
+ [ValidateSet('None', 'LlamaCpp', 'Ollama', 'Foundry')] [string] $Runtime = 'None',
+ [switch] $RequireTriton,
+ [switch] $PlanOnly,
+ [string] $ReportRoot = (Join-Path $env:LOCALAPPDATA 'DevConfig\reports\local-ai')
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$sourceRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
+New-Item -ItemType Directory -Path $ReportRoot -Force | Out-Null
+
+& (Join-Path $PSScriptRoot '..\_common\collect-ai-hardware.ps1') `
+ -OutputPath (Join-Path $ReportRoot 'hardware.json')
+
+$pytorchReportPath = Join-Path $ReportRoot 'pytorch.json'
+$pytorchArguments = @{
+ Backend = $Backend
+ ReportPath = $pytorchReportPath
+}
+if ($RequireTriton) { $pytorchArguments.RequireTriton = $true }
+if ($PlanOnly) { $pytorchArguments.PlanOnly = $true }
+& (Join-Path $sourceRoot 'Workloads\pytorch\install.ps1') @pytorchArguments
+$pytorchReport = Get-Content -LiteralPath $pytorchReportPath -Raw | ConvertFrom-Json
+if ($pytorchReport.result.blockers.Count -gt 0) {
+ Write-Host "LOCAL_AI_SCENARIO_UNSUPPORTED: $($pytorchReport.result.blockers -join '; ')"
+ return
+}
+if (-not $PlanOnly -and -not $pytorchReport.result.ready) {
+ throw 'The PyTorch scenario step did not report result.ready=true.'
+}
+
+if ($Runtime -ne 'None') {
+ $runtimePath = switch ($Runtime) {
+ 'LlamaCpp' { Join-Path $sourceRoot 'Workloads\llama.cpp\install.ps1' }
+ 'Ollama' { Join-Path $sourceRoot 'Workloads\ollama\install.ps1' }
+ 'Foundry' { Join-Path $sourceRoot 'Workloads\foundry\install.ps1' }
+ }
+ $runtimeReportPath = Join-Path $ReportRoot "$($Runtime.ToLowerInvariant()).json"
+ $runtimeArguments = @{ ReportPath = $runtimeReportPath }
+ if ($PlanOnly) { $runtimeArguments.PlanOnly = $true }
+ & $runtimePath @runtimeArguments
+ $runtimeReport = Get-Content -LiteralPath $runtimeReportPath -Raw | ConvertFrom-Json
+ if ($runtimeReport.result.blockers.Count -gt 0) {
+ Write-Host "LOCAL_AI_SCENARIO_UNSUPPORTED: $($runtimeReport.result.blockers -join '; ')"
+ return
+ }
+ if (-not $PlanOnly -and -not $runtimeReport.result.ready) {
+ throw "The $Runtime scenario step did not report result.ready=true."
+ }
+}
+
+if ($PlanOnly) {
+ Write-Host "LOCAL_AI_SCENARIO_PLAN_OK: backend=$Backend, runtime=$Runtime, reports=$ReportRoot"
+} else {
+ Write-Host "LOCAL_AI_SCENARIO_READY: backend=$Backend, runtime=$Runtime, reports=$ReportRoot"
+}
diff --git a/src/Workloads/ollama/install.ps1 b/src/Workloads/ollama/install.ps1
new file mode 100644
index 00000000..5da93524
--- /dev/null
+++ b/src/Workloads/ollama/install.ps1
@@ -0,0 +1,372 @@
+<#
+.SYNOPSIS
+ Install and manage Ollama, then run verified model inference.
+
+.PARAMETER SkipModelSmoke
+ Verify the CLI and local API without pulling or running the quick model.
+
+.PARAMETER Uninstall
+ Remove Ollama registration and runtime. ARM64 models are preserved by default.
+
+.PARAMETER RemoveModels
+ With -Uninstall, also remove the configured Ollama model directory.
+#>
+[CmdletBinding()]
+param(
+ [switch] $SkipModelSmoke,
+ [switch] $Uninstall,
+ [switch] $RemoveModels,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+if ($RemoveModels -and -not $Uninstall) {
+ throw '-RemoveModels requires -Uninstall.'
+}
+
+$architecture = Get-DevConfigArchitecture
+$plan = Resolve-OllamaInstallPlan -Architecture $architecture
+$catalog = (Get-AiCatalog).Components
+$component = if ($architecture -eq 'Arm64') { $catalog.OllamaArm64 } else { $catalog.OllamaX64 }
+$paths = Get-OllamaManagedPaths
+$modelRoot = if ($env:OLLAMA_MODELS) { $env:OLLAMA_MODELS } else { Join-Path $HOME '.ollama\models' }
+$report = New-AiWorkloadReport -Id 'ollama' -Request @{
+ SkipModelSmoke = [bool]$SkipModelSmoke
+ Uninstall = [bool]$Uninstall
+ RemoveModels = [bool]$RemoveModels
+ PlanOnly = [bool]$PlanOnly
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'ollama' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+
+if ($Uninstall) {
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ architecture = $architecture
+ sourceType = $component.SourceType
+ installPath = $(if ($architecture -eq 'Arm64') { $paths.InstallRoot } else { $component.InstallPath })
+ modelsPreserved = -not $RemoveModels
+ action = $(if ($PlanOnly) { 'planned-uninstall' } else { 'pending-uninstall' })
+ })
+ if ($PlanOnly) {
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: ollama uninstall'
+ return
+ }
+
+ if ($architecture -eq 'X64') {
+ Assert-AiAdministrator
+ Invoke-CheckedCommand -FilePath 'winget' -ArgumentList @(
+ 'uninstall', '--id', 'Ollama.Ollama', '--exact', '--source', 'winget',
+ '--silent', '--disable-interactivity'
+ ) -DisplayName 'Ollama application uninstall'
+ } else {
+ $removed = Remove-OllamaManagedInstallation `
+ -Paths $paths `
+ -ModelRoot $modelRoot `
+ -RemoveModels:$RemoveModels
+ $report.acceptance.uninstall = $removed
+ }
+ if ($architecture -eq 'X64' -and $RemoveModels) {
+ Remove-Item -LiteralPath $modelRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ $report.acquisitions[0].action = 'uninstalled'
+ $report.acceptance.models = [ordered]@{
+ path = $modelRoot
+ preserved = -not $RemoveModels
+ }
+ Complete-AiWorkloadReport -Report $report -Ready $true -Path $ReportPath
+ Write-Host "OLLAMA_UNINSTALLED: models-preserved=$(-not $RemoveModels)"
+ return
+}
+
+if ($architecture -eq 'X64') {
+ if (-not $PlanOnly) { Assert-AiAdministrator }
+ $acquisition = Ensure-AiWingetPackage -Id 'Ollama.Ollama' -PlanOnly:$PlanOnly
+ if (-not $PlanOnly) {
+ Update-DevConfigSessionPath
+ $ollamaPath = (Get-Command ollama -ErrorAction Stop).Source
+ }
+} else {
+ if ($PlanOnly) {
+ $acquisition = [pscustomobject]@{
+ Action = 'resolve-latest-stable-native-arm64-archive'
+ Source = 'github'
+ InstallType = $plan.InstallType
+ }
+ } else {
+ $managedProcessIds = @(Stop-OllamaManagedProcesses -InstallRoot $paths.InstallRoot)
+ $managedProcessIds += @(Stop-OllamaManagedProcesses -InstallRoot $paths.LegacyRoot)
+ $resolved = Install-VerifiedGitHubLatestAsset `
+ -Repository $component.Repository `
+ -AssetPattern $component.AssetPattern `
+ -Destination $paths.InstallRoot `
+ -VersionMarker $paths.VersionMarker `
+ -RequiredFile 'ollama.exe' `
+ -CacheDirectory $paths.CacheDirectory
+ $ollamaPath = $paths.Executable
+ $binaryArchitecture = Get-AiPeArchitecture -Path $ollamaPath
+ if ($binaryArchitecture -ne 'Arm64') {
+ throw "Official ARM64 asset installed '$binaryArchitecture' ollama.exe; refusing emulated or incompatible runtime."
+ }
+ Add-UserPathEntry -Path $paths.InstallRoot -Prepend
+ Remove-UserPathEntry -Path $paths.LegacyRoot
+ Remove-OllamaManagedDirectory -Path $paths.LegacyRoot
+
+ $startupCommand = Set-OllamaStartupRegistration `
+ -RegistryPath $paths.StartupRegistryPath `
+ -ValueName $paths.StartupValueName `
+ -Executable $ollamaPath
+ $installedFiles = @(Get-ChildItem -LiteralPath $paths.InstallRoot -Recurse -File |
+ Where-Object Name -notin @('.devconfig-install.json') |
+ ForEach-Object { $_.FullName.Substring($paths.InstallRoot.Length).TrimStart('\') } |
+ Sort-Object)
+ $installManifest = [ordered]@{
+ schemaVersion = 1
+ installedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
+ source = 'official native ARM64 archive'
+ installType = $plan.InstallType
+ repository = $component.Repository
+ tag = $resolved.Tag
+ asset = $resolved.Asset.name
+ assetDigest = $resolved.Asset.digest
+ cachePath = $resolved.CachePath
+ installPath = $paths.InstallRoot
+ executable = $ollamaPath
+ architecture = $binaryArchitecture
+ startup = [ordered]@{
+ registryPath = $paths.StartupRegistryPath
+ valueName = $paths.StartupValueName
+ command = $startupCommand
+ }
+ installedFiles = $installedFiles
+ modelsPath = $modelRoot
+ }
+ Write-DevConfigTextFile -Path $paths.InstallManifest -Content ($installManifest | ConvertTo-Json -Depth 8)
+ $acquisition = [pscustomobject]@{
+ Action = $resolved.Action
+ Source = 'github'
+ InstallType = $plan.InstallType
+ Tag = $resolved.Tag
+ Asset = $resolved.Asset.name
+ Sha256 = $resolved.Asset.digest
+ CachePath = $resolved.CachePath
+ InstallManifest = $paths.InstallManifest
+ stoppedManagedProcesses = @($managedProcessIds | Select-Object -Unique)
+ }
+ }
+}
+
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $component.Vendor
+ architecture = $architecture
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ packageId = Get-AiCatalogValue -Entry $component -Name 'PackageId'
+ repository = Get-AiCatalogValue -Entry $component -Name 'Repository'
+ assetPattern = Get-AiCatalogValue -Entry $component -Name 'AssetPattern'
+ installType = $(if ($architecture -eq 'Arm64') { $plan.InstallType } else { 'official-x64-installer' })
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $(if ($architecture -eq 'Arm64') { $paths.CacheDirectory } else { $component.CachePath })
+ installPath = $(if ($architecture -eq 'Arm64') { $paths.InstallRoot } else { $component.InstallPath })
+ installManifest = $(if ($architecture -eq 'Arm64') { $paths.InstallManifest } else { $null })
+ startupRegistration = Get-AiCatalogValue -Entry $component -Name 'StartupRegistration'
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ action = $acquisition.Action
+ resolvedTag = $(if ($architecture -eq 'Arm64' -and -not $PlanOnly) { $acquisition.Tag } else { $null })
+ resolvedAsset = $(if ($architecture -eq 'Arm64' -and -not $PlanOnly) { $acquisition.Asset } else { $null })
+ resolvedSha256 = $(if ($architecture -eq 'Arm64' -and -not $PlanOnly) { $acquisition.Sha256 } else { $null })
+ packageEvidence = $(if ($architecture -eq 'X64' -and -not $PlanOnly) { $acquisition.Evidence } else { $null })
+})
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'managed-runtime' -Status 'planned' -Evidence @{
+ installPath = $(if ($architecture -eq 'Arm64') { $paths.InstallRoot } else { $component.InstallPath })
+ startup = $(if ($architecture -eq 'Arm64') { $component.StartupRegistration } else { 'official installer managed' })
+ modelsPreservedOnUninstall = $true
+ }
+ Add-AiReportPhase -Report $report -Name 'model-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'planned' }) -Evidence @{ model = 'qwen3:0.6b' }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: ollama'
+ return
+}
+
+Invoke-CheckedCommand -FilePath $ollamaPath -ArgumentList @('--version') -DisplayName 'Ollama CLI verification'
+$oldHost = $env:OLLAMA_HOST
+$validationServer = $null
+$validationStdout = $null
+$validationStderr = $null
+try {
+ if ($architecture -eq 'Arm64') {
+ $port = Get-AiFreeTcpPort
+ $env:OLLAMA_HOST = "127.0.0.1:$port"
+ $apiBase = "http://127.0.0.1:$port"
+ $validationStdout = Join-Path $paths.InstallRoot 'validation-server.stdout.log'
+ $validationStderr = Join-Path $paths.InstallRoot 'validation-server.stderr.log'
+ Remove-Item $validationStdout, $validationStderr -Force -ErrorAction SilentlyContinue
+ Write-Host "Starting managed ARM64 validation server at $apiBase."
+ $validationServer = Start-Process `
+ -FilePath $ollamaPath `
+ -ArgumentList 'serve' `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $validationStdout `
+ -RedirectStandardError $validationStderr `
+ -PassThru
+ $version = Wait-JsonEndpoint -Uri ([uri]"$apiBase/api/version") -TimeoutSeconds 30
+ $expectedVersion = ([string]$acquisition.Tag).TrimStart('v')
+ if ([string]$version.version -ne $expectedVersion) {
+ throw "Managed ARM64 API reported version '$($version.version)', expected '$expectedVersion'."
+ }
+ } else {
+ $apiBase = 'http://localhost:11434'
+ try {
+ $version = Invoke-RestMethod -Uri "$apiBase/api/version" -TimeoutSec 3
+ } catch {
+ Write-Host "Ollama API is not running; starting 'ollama serve'."
+ Start-Process -FilePath $ollamaPath -ArgumentList 'serve' -WindowStyle Hidden | Out-Null
+ $version = Wait-JsonEndpoint -Uri ([uri]"$apiBase/api/version") -TimeoutSeconds 30
+ }
+ }
+
+ if (-not $version.version) {
+ throw 'Ollama API responded without a version value.'
+ }
+ if ($architecture -eq 'Arm64') {
+ $report.acceptance.server = [ordered]@{
+ validationEndpoint = $apiBase
+ processId = $validationServer.Id
+ executable = $ollamaPath
+ executableArchitecture = Get-AiPeArchitecture -Path $ollamaPath
+ version = $version.version
+ installManifest = $paths.InstallManifest
+ }
+ }
+
+ $modelPlan = Get-OllamaModelSmokePlan
+ $inferenceEvidence = $null
+ if ($SkipModelSmoke) {
+ Write-Warning 'OLLAMA_MODEL_SMOKE_SKIPPED: CLI and API are ready, but no model inference was performed.'
+ } else {
+ Write-Host "Pulling official Ollama library model $($modelPlan.Model) (approximately $($modelPlan.ApproximateDownloadMb) MB, $($modelPlan.License))."
+ Invoke-CheckedCommand -FilePath $ollamaPath -ArgumentList @('pull', $modelPlan.Model) -DisplayName 'Ollama model pull'
+ $manifestPath = Get-OllamaModelManifestPath -ModelRoot $modelRoot -Model $modelPlan.Model
+ if (-not (Test-Path -LiteralPath $manifestPath)) {
+ throw "Ollama pulled $($modelPlan.Model), but its local manifest was not found at '$manifestPath'."
+ }
+ $manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
+ $modelLayer = @($manifest.layers | Where-Object { $_.mediaType -match 'model' } | Select-Object -First 1)
+ $expectedDigest = "sha256:$($modelPlan.ModelBlobSha256)"
+ if ($modelLayer.Count -ne 1 -or $modelLayer[0].digest -ne $expectedDigest) {
+ throw "Ollama library tag $($modelPlan.Model) no longer references pinned model digest $expectedDigest."
+ }
+ $blobPath = Join-Path $modelRoot "blobs\sha256-$($modelPlan.ModelBlobSha256)"
+ if (-not (Test-Path -LiteralPath $blobPath)) {
+ throw "Ollama pulled $($modelPlan.Model), but its pinned model blob was not found at '$blobPath'."
+ }
+ $blobHash = (Get-FileHash -LiteralPath $blobPath -Algorithm SHA256).Hash
+ if ($blobHash -ne $modelPlan.ModelBlobSha256) {
+ throw "Ollama model blob checksum mismatch. Expected $($modelPlan.ModelBlobSha256); got $blobHash."
+ }
+
+ $request = New-OllamaGenerateRequest -Model $modelPlan.Model -Marker $modelPlan.Marker
+ $response = Invoke-RestMethod `
+ -Method Post `
+ -Uri "$apiBase/api/generate" `
+ -ContentType 'application/json' `
+ -Body ($request | ConvertTo-Json -Depth 8) `
+ -TimeoutSec 300
+ $result = $response.response | ConvertFrom-Json
+ if ($result.marker -ne $modelPlan.Marker) {
+ throw "Ollama model inference did not produce marker '$($modelPlan.Marker)'."
+ }
+ $processor = (& $ollamaPath ps 2>&1 | Out-String).Trim()
+ $running = Invoke-RestMethod -Uri "$apiBase/api/ps" -TimeoutSec 30
+ $loaded = @($running.models | Where-Object { $_.name -eq $modelPlan.Model } | Select-Object -First 1)
+ $gpuFraction = if ($loaded.Count -eq 1 -and [double]$loaded[0].size -gt 0) {
+ [math]::Round(([double]$loaded[0].size_vram / [double]$loaded[0].size), 4)
+ } else { 0 }
+ $serverEvidence = if ($validationStderr -and (Test-Path -LiteralPath $validationStderr)) {
+ (Get-Content -LiteralPath $validationStderr -Tail 200 | Select-String 'inference compute|gpu memory|library=' | Out-String).Trim()
+ } else {
+ $serverLogPath = Join-Path $env:LOCALAPPDATA 'Ollama\server.log'
+ if (Test-Path -LiteralPath $serverLogPath) {
+ (Get-Content -LiteralPath $serverLogPath -Tail 200 | Select-String 'inference compute|gpu memory|library=' | Out-String).Trim()
+ }
+ }
+ $report.acceptance.inference = [ordered]@{
+ model = $modelPlan.Model
+ digest = $expectedDigest
+ marker = $modelPlan.Marker
+ sizeBytes = if ($loaded.Count) { $loaded[0].size } else { $null }
+ sizeVramBytes = if ($loaded.Count) { $loaded[0].size_vram } else { $null }
+ gpuFraction = $gpuFraction
+ processTable = $processor
+ backendLogEvidence = $serverEvidence
+ }
+ $inferenceEvidence = $report.acceptance.inference
+ $report.result.fallbackUsed = $gpuFraction -eq 0
+ Write-Host $processor
+ Write-Host "OLLAMA_READY: version=$($version.version), architecture=$architecture, model=$($modelPlan.Model), verified-blob=$($modelPlan.ModelBlobSha256)."
+ }
+} finally {
+ if ($validationServer -and -not $validationServer.HasExited) {
+ Stop-Process -Id $validationServer.Id -Force -ErrorAction SilentlyContinue
+ $validationServer.WaitForExit()
+ }
+ if ($architecture -eq 'Arm64' -and $paths) {
+ [void](Stop-OllamaManagedProcesses -InstallRoot $paths.InstallRoot)
+ }
+ $env:OLLAMA_HOST = $oldHost
+}
+
+if ($architecture -eq 'Arm64') {
+ $defaultApi = 'http://127.0.0.1:11434'
+ try {
+ $existingDefault = Invoke-RestMethod -Uri "$defaultApi/api/version" -TimeoutSec 3
+ $managedDefault = @(Get-OllamaManagedProcesses -InstallRoot $paths.InstallRoot)
+ if ($managedDefault.Count -eq 0) {
+ throw "Port 11434 is already served by an unmanaged Ollama instance. Stop it and rerun to activate the managed ARM64 installation."
+ }
+ $persistentVersion = $existingDefault
+ $persistentProcess = $managedDefault | Select-Object -First 1
+ } catch {
+ if ($_.Exception.Message -match 'unmanaged Ollama') { throw }
+ $oldHost = $env:OLLAMA_HOST
+ try {
+ $env:OLLAMA_HOST = '127.0.0.1:11434'
+ $persistentProcess = Start-Process -FilePath $ollamaPath -ArgumentList 'serve' -WindowStyle Hidden -PassThru
+ $persistentVersion = Wait-JsonEndpoint -Uri ([uri]"$defaultApi/api/version") -TimeoutSeconds 30
+ } finally {
+ $env:OLLAMA_HOST = $oldHost
+ }
+ }
+ $report.acceptance.managedRuntime = [ordered]@{
+ installType = $plan.InstallType
+ installPath = $paths.InstallRoot
+ executable = $ollamaPath
+ executableArchitecture = Get-AiPeArchitecture -Path $ollamaPath
+ startupRegistryPath = $paths.StartupRegistryPath
+ startupValueName = $paths.StartupValueName
+ persistentEndpoint = $defaultApi
+ persistentProcessId = Get-AiProcessId -ProcessObject $persistentProcess
+ version = $persistentVersion.version
+ modelsPath = $modelRoot
+ }
+}
+
+Add-AiReportPhase -Report $report -Name 'ollama-inference' -Status $(if ($SkipModelSmoke) { 'skipped' } else { 'ready' }) -Evidence $inferenceEvidence
+Complete-AiWorkloadReport -Report $report -Ready (-not $SkipModelSmoke) -Path $ReportPath
+Write-Host 'INSTALL_OK: ollama'
diff --git a/src/Workloads/pytorch/install.ps1 b/src/Workloads/pytorch/install.ps1
new file mode 100644
index 00000000..16b8af89
--- /dev/null
+++ b/src/Workloads/pytorch/install.ps1
@@ -0,0 +1,415 @@
+<#
+.SYNOPSIS
+ Install a self-contained Windows PyTorch backend and run device acceptance.
+
+.PARAMETER Backend
+ Auto deterministically selects supported NVIDIA CUDA, then AMD ROCm, then
+ Intel XPU, then CPU. Explicit selection can target a supported secondary GPU.
+ CUDA/ROCm/XPU runtime packages are installed inside the contained PyTorch
+ environment. The standalone CUDA, ROCm, and Intel AI flows are native
+ developer-toolkit/runtime flows, not prerequisites for ordinary tensor use.
+
+.PARAMETER SkipTriton
+ Do not install or verify Triton when supported. Native Windows Triton is
+ supported for qualified NVIDIA CUDA (`triton-windows`) and Intel XPU
+ (`triton-xpu`/`torch.compile`) paths, not AMD ROCm.
+
+.PARAMETER RequireTriton
+ Fail unless this host has a supported Triton combination. This is available
+ for qualified NVIDIA CUDA and Intel XPU paths; native Windows AMD ROCm has no
+ supported Triton package.
+
+.PARAMETER DeviceIndex
+ Zero-based device index for CUDA/ROCm/XPU acceptance. Use this to target a
+ same-vendor secondary adapter with an explicit backend. Auto requires index 0
+ so package resolution and execution cannot refer to different vendor devices.
+#>
+[CmdletBinding()]
+param(
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $Backend = 'Auto',
+ [switch] $SkipTriton,
+ [switch] $RequireTriton,
+ [ValidateRange(0, 63)] [int] $DeviceIndex = 0,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$gpuVendor = Get-AiDetectedVendor
+$amdGpuName = if ($Backend -in @('Auto', 'ROCm')) { Get-AmdGpuName -DeviceIndex $DeviceIndex } else { Get-AmdGpuName }
+$intelGpuName = if ($Backend -in @('Auto', 'XPU')) { Get-IntelGpuName -DeviceIndex $DeviceIndex } else { Get-IntelGpuName }
+$driver = $null
+$driverError = $null
+try {
+ $driver = Get-NvidiaDriverInfo -DeviceIndex $(if ($Backend -in @('Auto', 'CUDA')) { $DeviceIndex } else { 0 })
+} catch {
+ $driverError = $_.Exception.Message
+}
+$nvidiaGpu = if ($driver) { [pscustomobject]@{ Name = $driver.Name } } else { Get-NvidiaGpu }
+$gpuName = if ($Backend -eq 'ROCm') {
+ $amdGpuName
+} elseif ($Backend -eq 'XPU') {
+ $intelGpuName
+} elseif ($nvidiaGpu) {
+ $nvidiaGpu.Name
+} elseif ($amdGpuName) {
+ $amdGpuName
+} else {
+ $intelGpuName
+}
+$amdGfxTarget = if ($amdGpuName) { Get-AmdGfxTarget -GpuName $amdGpuName } else { $null }
+$report = New-AiWorkloadReport -Id 'pytorch' -Request @{
+ Backend = $Backend
+ SelectedBackend = $null
+ SkipTriton = [bool]$SkipTriton
+ RequireTriton = [bool]$RequireTriton
+ DeviceIndex = $DeviceIndex
+ PlanOnly = [bool]$PlanOnly
+ DetectedVendorPriority = $gpuVendor
+ DetectedNvidiaDevice = $(if ($nvidiaGpu) { $nvidiaGpu.Name } else { $null })
+ DetectedAmdDevice = $amdGpuName
+ DetectedIntelDevice = $intelGpuName
+ AmdGfxTarget = $amdGfxTarget
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'pytorch' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+if ($Backend -eq 'Auto' -and $DeviceIndex -ne 0) {
+ $message = 'PyTorch -Backend Auto supports only -DeviceIndex 0. Select CUDA, ROCm, or XPU explicitly to target a secondary same-vendor adapter.'
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($message)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: pytorch'
+ return
+ }
+ throw $message
+}
+if ($driverError) {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($driverError)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: pytorch'
+ return
+ }
+ throw $driverError
+}
+if ($SkipTriton -and $RequireTriton) {
+ throw '-SkipTriton and -RequireTriton cannot be used together.'
+}
+if ($PlanOnly) {
+ $pythonPackage = Ensure-AiWingetPackage -Id 'Python.Python.3.13' -PlanOnly
+ $pythonVersion = [version]'3.13'
+} else {
+ Assert-AiAdministrator
+ $pythonPackage = Ensure-AiWingetPackage -Id 'Python.Python.3.13'
+ $pythonPath = Get-Python313Path -Architecture $architecture
+ $pythonVersionResult = Invoke-DevConfigNativeCommand -FilePath $pythonPath -Arguments @(
+ '-c', 'import platform; print(platform.python_version())'
+ )
+ $pythonVersionText = $pythonVersionResult.Output.Trim()
+ if ($pythonVersionResult.ExitCode -ne 0) { throw 'Python failed while reporting its version.' }
+ $pythonVersion = [version]$pythonVersionText
+ $pythonMachineResult = Invoke-DevConfigNativeCommand -FilePath $pythonPath -Arguments @(
+ '-c', 'import platform; print(platform.machine())'
+ )
+ $pythonMachine = $pythonMachineResult.Output.Trim()
+ if ($pythonMachineResult.ExitCode -ne 0) { throw 'Python failed while reporting its architecture.' }
+ Assert-PythonArchitecture -Architecture $architecture -PythonMachine $pythonMachine
+}
+$hasNvidia = [bool]$driver
+try {
+ $plan = Resolve-PyTorchPlan `
+ -Architecture $architecture `
+ -Backend $Backend `
+ -PythonVersion $pythonVersion `
+ -HasNvidia $hasNvidia `
+ -DriverMajor $(if ($driver) { $driver.DriverMajor } else { 0 }) `
+ -ComputeCapability $(if ($driver) { $driver.ComputeCapability } else { [version]'0.0' }) `
+ -GpuVendor $gpuVendor `
+ -GpuName $gpuName `
+ -AmdGpuName $amdGpuName `
+ -IntelGpuName $intelGpuName `
+ -AmdGfxTarget $amdGfxTarget `
+ -HasAmd ([bool]$amdGpuName) `
+ -HasIntel ([bool]$intelGpuName) `
+ -SkipTriton:$SkipTriton
+} catch {
+ if ($PlanOnly) {
+ [void]$report.result.blockers.Add($_.Exception.Message)
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: pytorch'
+ return
+ }
+ throw
+}
+$report.request.SelectedBackend = $plan.Backend
+$report.request.SelectedVendor = $plan.Vendor
+$report.request.SelectedDevice = $plan.DeviceName
+if ($RequireTriton -and -not $plan.InstallTriton) {
+ throw "Triton Windows is required but unsupported: $($plan.TritonReason)"
+}
+$catalog = (Get-AiCatalog).Components
+$component = if ($plan.Backend -eq 'CUDA' -and $architecture -eq 'Arm64') {
+ $catalog.NvidiaPyTorchArm64
+} elseif ($plan.Backend -eq 'CUDA') {
+ $catalog.PyTorchCudaX64
+} elseif ($plan.Backend -eq 'ROCm') {
+ $catalog.PyTorchRocm
+} elseif ($plan.Backend -eq 'XPU') {
+ $catalog.PyTorchXpu
+} else {
+ $catalog.PyTorchCpu
+}
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = 'Python 3.13'
+ sourceType = 'winget'
+ packageId = 'Python.Python.3.13'
+ action = $pythonPackage.Action
+ packageEvidence = $(if ($PlanOnly) { $null } else { $pythonPackage.Evidence })
+})
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = "PyTorch $($plan.Backend)"
+ vendor = $component.Vendor
+ detectedVendorPriority = $gpuVendor
+ selectedDeviceName = $plan.DeviceName
+ selectedVendor = $plan.Vendor
+ selectedDevice = $plan.DeviceName
+ amdGfxTarget = $plan.AmdGfxTarget
+ architecture = $architecture
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ requirement = $plan.TorchRequirement
+ additionalRequirements = $plan.AdditionalRequirements
+ runtimePackageTuple = @($plan.TorchRequirement) + @($plan.AdditionalRequirements)
+ index = $plan.IndexUrl
+ version = $plan.TorchVersion
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $component.CachePath
+ installPath = '%LOCALAPPDATA%\DevConfig\pytorch\.venv'
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ promotionCandidate = Get-AiCatalogValue -Entry $component -Name 'PromotionCandidate'
+ nativeToolkitRequired = $component.NativeToolkitRequired
+ nativeToolkitRelationship = $component.NativeToolkitRelationship
+ action = $(if ($PlanOnly) { 'planned' } else { 'pending' })
+})
+if ($plan.InstallTriton) {
+ $tritonComponent = if ($plan.Backend -eq 'XPU') { $catalog.TritonXpu } else { $catalog.TritonWindows }
+ Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $tritonComponent.Component
+ vendor = $tritonComponent.Vendor
+ architecture = $architecture
+ maturity = $tritonComponent.Maturity
+ sourceType = $tritonComponent.SourceType
+ package = $tritonComponent.Package
+ versionPolicy = $tritonComponent.VersionPolicy
+ integrity = $tritonComponent.Integrity
+ cachePath = $tritonComponent.CachePath
+ installPath = $tritonComponent.InstallPath
+ reasonNormalChannelInsufficient = $tritonComponent.NormalChannelLimitation
+ expectedStableSource = $tritonComponent.ExpectedStableSource
+ migrationTrigger = $tritonComponent.MigrationTrigger
+ cleanupUpgrade = $tritonComponent.CleanupUpgrade
+ action = $(if ($PlanOnly) { 'planned' } else { 'pending' })
+ })
+}
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'tensor' -Status 'planned' -Evidence @{
+ backend = $plan.Backend
+ vendor = $plan.Vendor
+ device = $plan.DeviceName
+ deviceIndex = $DeviceIndex
+ amdGfxTarget = $plan.AmdGfxTarget
+ selfContainedRuntime = $true
+ separateToolkitRequired = $false
+ }
+ Add-AiReportPhase -Report $report -Name 'triton' -Status $(if ($plan.InstallTriton) { 'planned' } elseif ($SkipTriton) { 'skipped' } else { 'unsupported' }) -Evidence @{ reason = $plan.TritonReason }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host $(if ($report.result.blockers.Count) { 'PLAN_UNSUPPORTED: pytorch' } else { 'PLAN_OK: pytorch' })
+ return
+}
+
+$root = Join-Path $env:LOCALAPPDATA 'DevConfig\pytorch'
+$venv = Join-Path $root '.venv'
+$statePath = Join-Path $root 'install-state.json'
+$desiredState = [ordered]@{
+ architecture = $plan.Architecture
+ backend = $plan.Backend
+ torch = $plan.TorchRequirement
+ torchVersion = $plan.TorchVersion
+ index = $plan.IndexUrl
+ triton = $plan.TritonRequirement
+ tritonVersion = $plan.TritonVersion
+ numpy = $plan.NumpyRequirement
+ numpyVersion = $plan.NumpyVersion
+ additionalRequirements = @($plan.AdditionalRequirements)
+ python = "$($pythonVersion.Major).$($pythonVersion.Minor)"
+ deviceIndex = $DeviceIndex
+ selectedDevice = $plan.DeviceName
+}
+$desiredJson = $desiredState | ConvertTo-Json -Compress
+
+$currentJson = $null
+if (Test-Path -LiteralPath $statePath) {
+ $currentJson = (Get-Content -LiteralPath $statePath -Raw).Trim()
+}
+$existingVenvPython = Join-Path $venv 'Scripts\python.exe'
+$existingVersions = Get-PythonEnvironmentVersions -PythonPath $existingVenvPython
+if ((Test-Path -LiteralPath $venv) -and
+ (Test-PyTorchEnvironmentRequiresRecreation `
+ -DesiredStateJson $desiredJson `
+ -CurrentStateJson $currentJson `
+ -InstalledVersions $existingVersions)) {
+ Write-Host 'The requested PyTorch plan changed; recreating the contained environment.'
+ Remove-Item -LiteralPath $venv -Recurse -Force
+ $currentJson = $null
+ $existingVersions = $null
+}
+
+New-Item -ItemType Directory -Path $root -Force | Out-Null
+if (-not (Test-Path -LiteralPath (Join-Path $venv 'Scripts\python.exe'))) {
+ Invoke-CheckedCommand -FilePath $pythonPath -ArgumentList @('-m', 'venv', $venv) -DisplayName 'PyTorch virtual environment creation'
+}
+
+$venvPython = Join-Path $venv 'Scripts\python.exe'
+$installedVersions = if ($existingVersions) {
+ $existingVersions
+} else {
+ Get-PythonEnvironmentVersions -PythonPath $venvPython
+}
+$packageAction = Get-PyTorchPackageAction `
+ -DesiredStateJson $desiredJson `
+ -CurrentStateJson $currentJson `
+ -InstalledVersions $installedVersions
+
+if ($plan.InstallTriton -and $plan.Backend -in @('CUDA', 'XPU')) {
+ $cppTools = Ensure-AiVisualCppTools -Architecture $architecture
+ if ($plan.Backend -eq 'CUDA') {
+ [void](Ensure-AiCudaToolkit -Architecture $architecture)
+ }
+ $compiler = Import-MsvcEnvironment -Architecture $architecture
+ Write-Host "Triton JIT compiler: $compiler"
+}
+
+if ($packageAction -eq 'VerifyOnly') {
+ Write-Host "PYTORCH_PACKAGES_CURRENT: torch=$($installedVersions.torch), numpy=$($installedVersions.numpy), triton=$($installedVersions.triton). Skipping package resolution and installation."
+} else {
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList @('-m', 'pip', 'install', '--upgrade', 'pip') -DisplayName 'pip upgrade'
+ Invoke-CheckedCommand `
+ -FilePath $venvPython `
+ -ArgumentList (Get-PipInstallArguments -Requirement $plan.NumpyRequirement) `
+ -DisplayName 'NumPy installation from the configured Python index'
+
+ if ($plan.DirectWheelUrl) {
+ $wheelDirectory = Join-Path $root 'wheel-cache'
+ $wheelPath = Join-Path $wheelDirectory $plan.DirectWheelFileName
+ Install-VerifiedDownload `
+ -Uri $plan.DirectWheelUrl `
+ -Destination $wheelPath `
+ -Sha256 $plan.DirectWheelSha256
+ Invoke-CheckedCommand `
+ -FilePath $venvPython `
+ -ArgumentList (Get-PipLocalWheelInstallArguments -WheelPath $wheelPath) `
+ -DisplayName 'PyTorch installation from verified wheel cache'
+ } else {
+ $allRequirements = @($plan.TorchRequirement) + @($plan.AdditionalRequirements)
+ $binaryPolicy = if ($plan.Backend -eq 'ROCm') { @() } else { @('--only-binary=:all:') }
+ $torchDryRun = @('-m', 'pip', 'install', '--dry-run') + $binaryPolicy + $allRequirements
+ if ($plan.IndexUrl) { $torchDryRun += @('--index-url', $plan.IndexUrl) }
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList $torchDryRun -DisplayName 'PyTorch compatible-wheel check'
+ $torchInstall = @('-m', 'pip', 'install') + $binaryPolicy + $allRequirements
+ if ($plan.IndexUrl) { $torchInstall += @('--index-url', $plan.IndexUrl) }
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList $torchInstall -DisplayName 'PyTorch installation'
+ }
+
+ if ($plan.InstallTriton) {
+ $tritonDryRun = Get-PipInstallArguments -Requirement $plan.TritonRequirement -IndexUrl $(if ($plan.Backend -eq 'XPU') { $plan.IndexUrl } else { $null }) -DryRun
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList $tritonDryRun -DisplayName 'Triton Windows compatible-wheel check'
+ $tritonInstall = Get-PipInstallArguments -Requirement $plan.TritonRequirement -IndexUrl $(if ($plan.Backend -eq 'XPU') { $plan.IndexUrl } else { $null })
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList $tritonInstall -DisplayName 'Triton Windows installation'
+ }
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList @('-m', 'pip', 'check') -DisplayName 'PyTorch dependency check'
+}
+
+$tensorArguments = @(
+ (Join-Path $PSScriptRoot 'smoke.py'), '--backend', $plan.Backend, '--device-index', $DeviceIndex
+)
+$tensorResult = Invoke-DevConfigNativeCommand -FilePath $venvPython -Arguments $tensorArguments
+$tensorEvidence = $tensorResult.Output.Trim()
+if ($tensorResult.ExitCode -ne 0) {
+ throw "PyTorch $($plan.Backend) tensor smoke failed: $tensorEvidence"
+}
+$tensorRecord = ConvertFrom-AiKeyedJsonLine -Text $tensorEvidence -Prefix 'PYTORCH_SMOKE='
+if ($plan.Backend -ne 'CPU' -and $plan.DeviceName -and
+ -not (Test-AiDeviceNameMatch -Expected $plan.DeviceName -Actual $tensorRecord.device)) {
+ throw "PyTorch device index $DeviceIndex executed on '$($tensorRecord.device)', but the resolver selected '$($plan.DeviceName)'. Use the matching -DeviceIndex."
+}
+if ($plan.InstallTriton) {
+ $tritonSmoke = if ($plan.Backend -eq 'XPU') { 'xpu-smoke.py' } else { 'triton-smoke.py' }
+ $tritonResult = Invoke-DevConfigNativeCommand -FilePath $venvPython -Arguments @(
+ (Join-Path $PSScriptRoot $tritonSmoke), '--device-index', $DeviceIndex
+ )
+ $tritonEvidence = $tritonResult.Output.Trim()
+ if ($tritonResult.ExitCode -ne 0) {
+ throw "Triton $($plan.Backend) GPU kernel smoke failed: $tritonEvidence"
+ }
+ $tritonRecord = if ($plan.Backend -eq 'XPU') {
+ ConvertFrom-AiKeyedJsonLine -Text $tritonEvidence -Prefix 'TRITON_XPU_READY='
+ } else {
+ $null
+ }
+ Write-Host "TRITON_READY: $($plan.TritonRequirement)"
+} else {
+ $tritonEvidence = $plan.TritonReason
+ Write-Host "TRITON_SKIPPED: $($plan.TritonReason)"
+}
+
+Set-Content -LiteralPath $statePath -Value $desiredJson -Encoding ascii
+if ($plan.Preview) {
+ Write-Warning 'PyTorch CUDA on Windows ARM64 is an NVIDIA Developer Preview nightly, not a stable or production-supported release.'
+}
+Write-Host "PYTORCH_READY: backend=$($plan.Backend), runtime=$($plan.Runtime), environment=$venv"
+$versions = Get-PythonEnvironmentVersions -PythonPath $venvPython
+$report.acceptance.tensor = [ordered]@{
+ backend = $plan.Backend
+ vendor = $plan.Vendor
+ runtime = $plan.Runtime
+ device = $tensorRecord.device
+ deviceIndex = $DeviceIndex
+ amdGfxTarget = $plan.AmdGfxTarget
+ torch = $versions.torch
+ numpy = $versions.numpy
+ torchCudaRuntime = $tensorRecord.torch_cuda_runtime
+ torchHipRuntime = $tensorRecord.torch_hip_runtime
+ runtimePackageTuple = @($plan.TorchRequirement) + @($plan.AdditionalRequirements)
+ selfContainedRuntime = $true
+ separateNativeToolkitRequired = $false
+ deviceEvidence = $tensorEvidence
+}
+$report.acceptance.triton = [ordered]@{
+ supported = [bool]$plan.InstallTriton
+ version = $versions.triton
+ distribution = $versions.triton_distribution
+ reason = $plan.TritonReason
+ evidence = $tritonEvidence
+ device = $(if ($tritonRecord) { $tritonRecord.device } else { $tensorRecord.device })
+ torchCompileExecuted = $(if ($tritonRecord) { [bool]$tritonRecord.torch_compile_executed } else { $null })
+}
+$report.acquisitions[1].action = $packageAction.ToLowerInvariant()
+if ($plan.InstallTriton) {
+ $report.acquisitions[2].action = $(if ($packageAction -eq 'VerifyOnly') { 'already-current' } else { 'installed-or-upgraded' })
+}
+Complete-AiWorkloadReport -Report $report -Ready $true -Path $ReportPath
+Write-Host "Activate with: & '$venv\Scripts\Activate.ps1'"
+Write-Host 'INSTALL_OK: pytorch'
diff --git a/src/Workloads/pytorch/smoke.py b/src/Workloads/pytorch/smoke.py
new file mode 100644
index 00000000..4cdaded2
--- /dev/null
+++ b/src/Workloads/pytorch/smoke.py
@@ -0,0 +1,69 @@
+import argparse
+import json
+
+import numpy
+import torch
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--backend", choices=("CPU", "CUDA", "ROCm", "XPU"), required=True)
+parser.add_argument("--device-index", type=int, default=0)
+args = parser.parse_args()
+
+device_type = "xpu" if args.backend == "XPU" else ("cuda" if args.backend in ("CUDA", "ROCm") else "cpu")
+device = f"{device_type}:{args.device_index}" if device_type != "cpu" else "cpu"
+if device_type == "cuda" and not torch.cuda.is_available():
+ stack = "ROCm/HIP" if args.backend == "ROCm" else "CUDA"
+ raise RuntimeError(f"The {stack} PyTorch stack imported, but torch.cuda.is_available() is false.")
+if device_type == "xpu" and not torch.xpu.is_available():
+ raise RuntimeError("The XPU wheel imported, but torch.xpu.is_available() is false.")
+if args.backend == "ROCm" and not torch.version.hip:
+ raise RuntimeError("The ROCm PyTorch stack imported, but torch.version.hip is null.")
+if args.backend == "CUDA" and not torch.version.cuda:
+ raise RuntimeError("The CUDA PyTorch stack imported, but torch.version.cuda is null.")
+
+tensor = torch.tensor([1.0, 2.0], device=device)
+result = (tensor * 2).cpu().tolist()
+if result != [2.0, 4.0]:
+ raise RuntimeError(f"Unexpected tensor result: {result}")
+if device_type == "cuda":
+ torch.cuda.synchronize(args.device_index)
+elif device_type == "xpu":
+ torch.xpu.synchronize()
+array = (tensor * 2).cpu().numpy()
+if not numpy.array_equal(array, numpy.array([2.0, 4.0])):
+ raise RuntimeError(f"Unexpected NumPy bridge result: {array}")
+
+model = torch.nn.Sequential(
+ torch.nn.Linear(2, 4),
+ torch.nn.ReLU(),
+ torch.nn.Linear(4, 1),
+).to(device)
+with torch.no_grad():
+ model_result = model(torch.tensor([[1.0, 2.0]], device=device)).cpu().item()
+if not numpy.isfinite(model_result):
+ raise RuntimeError(f"Minimal neural-network forward pass was not finite: {model_result}")
+
+details = {
+ "backend": args.backend,
+ "vendor": {
+ "CUDA": "NVIDIA",
+ "ROCm": "AMD",
+ "XPU": "Intel",
+ "CPU": "CPU",
+ }[args.backend],
+ "device": (
+ torch.cuda.get_device_name(args.device_index)
+ if device_type == "cuda"
+ else (torch.xpu.get_device_name(args.device_index) if device_type == "xpu" else "CPU")
+ ),
+ "torch": torch.__version__,
+ "torch_cuda_runtime": torch.version.cuda,
+ "numpy": numpy.__version__,
+ "torch_hip_runtime": torch.version.hip,
+ "tensor_device_type": tensor.device.type,
+ "device_index": args.device_index,
+ "tensor_operation_verified": True,
+ "model_forward_verified": True,
+}
+print("PYTORCH_SMOKE=" + json.dumps(details, sort_keys=True))
diff --git a/src/Workloads/pytorch/triton-smoke.py b/src/Workloads/pytorch/triton-smoke.py
new file mode 100644
index 00000000..8ef3593b
--- /dev/null
+++ b/src/Workloads/pytorch/triton-smoke.py
@@ -0,0 +1,31 @@
+import argparse
+import torch
+import triton
+import triton.language as tl
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--device-index", type=int, default=0)
+args = parser.parse_args()
+torch.cuda.set_device(args.device_index)
+
+
+@triton.jit
+def add_kernel(x_ptr, y_ptr, output_ptr, size: tl.constexpr, block_size: tl.constexpr):
+ offsets = tl.arange(0, block_size)
+ mask = offsets < size
+ x = tl.load(x_ptr + offsets, mask=mask)
+ y = tl.load(y_ptr + offsets, mask=mask)
+ tl.store(output_ptr + offsets, x + y, mask=mask)
+
+
+size = 1024
+device = f"cuda:{args.device_index}"
+x = torch.arange(size, device=device, dtype=torch.float32)
+y = torch.full((size,), 2.0, device=device)
+output = torch.empty_like(x)
+add_kernel[(1,)](x, y, output, size=size, block_size=1024)
+torch.cuda.synchronize()
+if not torch.equal(output, x + y):
+ raise RuntimeError("Triton vector-add result did not match PyTorch.")
+print(f"TRITON_SMOKE=vector-add,device={torch.cuda.get_device_name(args.device_index)},index={args.device_index}")
diff --git a/src/Workloads/pytorch/xpu-smoke.py b/src/Workloads/pytorch/xpu-smoke.py
new file mode 100644
index 00000000..d7397863
--- /dev/null
+++ b/src/Workloads/pytorch/xpu-smoke.py
@@ -0,0 +1,41 @@
+import json
+import argparse
+import torch
+import triton
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--device-index", type=int, default=0)
+args = parser.parse_args()
+
+
+def fn(x):
+ return torch.sin(x) + torch.cos(x)
+
+
+if not torch.xpu.is_available():
+ raise RuntimeError("torch.xpu is unavailable")
+
+torch.xpu.set_device(args.device_index)
+device = f"xpu:{args.device_index}"
+x = torch.randn(4096, device=device)
+expected = fn(x)
+compiled = torch.compile(fn)
+actual = compiled(x)
+torch.xpu.synchronize()
+torch.testing.assert_close(actual, expected)
+print(
+ "TRITON_XPU_READY="
+ + json.dumps(
+ {
+ "backend": "XPU",
+ "vendor": "Intel",
+ "device": torch.xpu.get_device_name(args.device_index),
+ "device_index": args.device_index,
+ "torch": torch.__version__,
+ "triton_xpu": triton.__version__,
+ "torch_compile_executed": True,
+ },
+ sort_keys=True,
+ )
+)
diff --git a/src/Workloads/rocm/hip-smoke.cpp b/src/Workloads/rocm/hip-smoke.cpp
new file mode 100644
index 00000000..6df7dd04
--- /dev/null
+++ b/src/Workloads/rocm/hip-smoke.cpp
@@ -0,0 +1,42 @@
+#include
+
+#include
+#include
+
+#define CHECK(call) \
+ do { \
+ hipError_t error = (call); \
+ if (error != hipSuccess) { \
+ std::fprintf(stderr, "%s: %s\n", #call, hipGetErrorString(error)); \
+ return 1; \
+ } \
+ } while (0)
+
+__global__ void write_marker(int* value) {
+ if (blockIdx.x == 0 && threadIdx.x == 0) {
+ *value = 42;
+ }
+}
+
+int main(int argc, char** argv) {
+ const int device_index = argc > 1 ? std::atoi(argv[1]) : 0;
+ int device_count = 0;
+ CHECK(hipGetDeviceCount(&device_count));
+ if (device_index < 0 || device_index >= device_count) return 2;
+ CHECK(hipSetDevice(device_index));
+
+ hipDeviceProp_t properties{};
+ CHECK(hipGetDeviceProperties(&properties, device_index));
+
+ int* device_value = nullptr;
+ int host_value = 0;
+ CHECK(hipMalloc(reinterpret_cast(&device_value), sizeof(host_value)));
+ hipLaunchKernelGGL(write_marker, dim3(1), dim3(1), 0, 0, device_value);
+ CHECK(hipGetLastError());
+ CHECK(hipDeviceSynchronize());
+ CHECK(hipMemcpy(&host_value, device_value, sizeof(host_value), hipMemcpyDeviceToHost));
+ CHECK(hipFree(device_value));
+
+ std::printf("HIP_KERNEL_READY device_index=%d device=%s value=%d\n", device_index, properties.name, host_value);
+ return host_value == 42 ? 0 : 3;
+}
diff --git a/src/Workloads/rocm/install.ps1 b/src/Workloads/rocm/install.ps1
new file mode 100644
index 00000000..dfa1688e
--- /dev/null
+++ b/src/Workloads/rocm/install.ps1
@@ -0,0 +1,170 @@
+<#
+.SYNOPSIS
+ Install AMD ROCm Core SDK on supported Windows x64 hardware and execute a HIP kernel.
+
+.PARAMETER DeviceIndex
+ Zero-based AMD device index used for HIP kernel execution on same-vendor
+ multi-adapter systems.
+#>
+[CmdletBinding()]
+param(
+ [ValidateRange(0, 63)] [int] $DeviceIndex = 0,
+ [switch] $PlanOnly,
+ [string] $ReportPath = ''
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\_common\ai-report.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$gpuName = Get-AmdGpuName -DeviceIndex $DeviceIndex
+$rocmPlan = $null
+$planError = $null
+try {
+ $rocmPlan = Resolve-RocmInstallPlan -Architecture $architecture -GpuName $gpuName
+} catch {
+ $planError = $_.Exception.Message
+}
+$gfx = if ($rocmPlan) { $rocmPlan.GfxTarget } else { $null }
+
+$catalog = (Get-AiCatalog).Components
+$component = $catalog.AmdRocm
+$report = New-AiWorkloadReport -Id 'rocm' -Request @{
+ PlanOnly = [bool]$PlanOnly
+ GpuName = $gpuName
+ GfxTarget = $gfx
+ DeviceIndex = $DeviceIndex
+}
+if (-not $ReportPath) { $ReportPath = Get-AiDefaultReportPath -Id 'rocm' }
+trap {
+ Write-AiFailureReport -Report $report -Path $ReportPath -ErrorRecord $_
+ throw $_
+}
+if (-not $PlanOnly) { Assert-AiAdministrator }
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = $component.Component
+ vendor = $component.Vendor
+ architecture = $architecture
+ gpu = $gpuName
+ gfxTarget = $gfx
+ maturity = $component.Maturity
+ sourceType = $component.SourceType
+ index = $component.IndexUrl
+ requirement = $(if ($gfx) { $component.PackageTemplate -f $gfx } else { $null })
+ version = $component.Version
+ versionPolicy = $component.VersionPolicy
+ integrity = $component.Integrity
+ cachePath = $component.CachePath
+ installPath = $component.InstallPath
+ reasonNormalChannelInsufficient = $component.NormalChannelLimitation
+ expectedStableSource = $component.ExpectedStableSource
+ migrationTrigger = $component.MigrationTrigger
+ cleanupUpgrade = $component.CleanupUpgrade
+ action = $(if ($PlanOnly) { 'planned' } else { 'pending' })
+})
+if ($planError) {
+ [void]$report.result.blockers.Add($planError)
+ Set-AiAcquisitionAction -Report $report -Index 0 -Action 'blocked'
+}
+if ($report.result.blockers.Count -gt 0) {
+ if ($PlanOnly) {
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_UNSUPPORTED: rocm'
+ return
+ }
+ throw ($report.result.blockers -join ' ')
+}
+$pythonPackage = Ensure-AiWingetPackage -Id 'Python.Python.3.13' -PlanOnly:$PlanOnly
+$cppTools = Ensure-AiVisualCppTools -Architecture X64 -PlanOnly:$PlanOnly
+Add-AiReportPhase -Report $report -Name 'host-compiler' -Status $(if ($PlanOnly) { 'planned' } else { 'ready' }) -Evidence $cppTools
+$requirement = $rocmPlan.Requirement
+$report.acquisitions[0].requirement = $requirement
+Add-AiReportAcquisition -Report $report -Entry ([ordered]@{
+ component = 'Python 3.13'
+ sourceType = 'winget'
+ packageId = 'Python.Python.3.13'
+ action = $pythonPackage.Action
+ packageEvidence = $(if ($PlanOnly) { $null } else { $pythonPackage.Evidence })
+})
+if ($PlanOnly) {
+ Add-AiReportPhase -Report $report -Name 'hip-kernel' -Status 'planned' -Evidence @{ gpu = $gpuName; gfx = $gfx; deviceIndex = $DeviceIndex }
+ Complete-AiWorkloadReport -Report $report -Ready $false -Path $ReportPath
+ Write-Host 'PLAN_OK: rocm'
+ return
+}
+
+$compiler = Import-MsvcEnvironment -Architecture X64
+$python = Get-Python313Path -Architecture X64
+$root = Join-Path $env:LOCALAPPDATA 'DevConfig\rocm'
+$venv = Join-Path $root '.venv'
+$statePath = Join-Path $root 'install-state.json'
+$desired = [ordered]@{ requirement = $requirement; python = '3.13'; gfx = $gfx } | ConvertTo-Json -Compress
+if ((Test-Path $statePath) -and (Test-Path $venv) -and
+ ((Get-Content $statePath -Raw).Trim() -ne $desired)) {
+ Remove-Item -LiteralPath $venv -Recurse -Force
+}
+New-Item -ItemType Directory -Path $root -Force | Out-Null
+if (-not (Test-Path (Join-Path $venv 'Scripts\python.exe'))) {
+ Invoke-CheckedCommand -FilePath $python -ArgumentList @('-m', 'venv', $venv) -DisplayName 'ROCm environment creation'
+}
+$venvPython = Join-Path $venv 'Scripts\python.exe'
+$hipcc = Join-Path $venv 'Scripts\hipcc.exe'
+$expectedRocmPackages = @{
+ 'rocm-sdk-core' = '10.0.0'
+ 'rocm-sdk-devel' = '10.0.0'
+ 'rocm-sdk-libraries' = '10.0.0'
+ "rocm-sdk-device-$gfx" = '10.0.0'
+}
+$packagesCurrent = (Test-Path $hipcc) -and
+ (Test-PythonDistributionVersions -PythonPath $venvPython -Expected $expectedRocmPackages)
+if (-not $packagesCurrent) {
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList @(
+ '-m', 'pip', 'install', '--upgrade', 'pip'
+ ) -DisplayName 'pip upgrade'
+ Invoke-CheckedCommand -FilePath $venvPython -ArgumentList @(
+ '-m', 'pip', 'install', '--index-url', $component.IndexUrl, $requirement
+ ) -DisplayName 'AMD ROCm Core SDK installation'
+}
+Invoke-CheckedCommand -FilePath $venvPython -ArgumentList @('-m', 'pip', 'check') -DisplayName 'ROCm dependency check'
+
+$temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-hip-$([guid]::NewGuid().ToString('N'))"
+New-Item -ItemType Directory -Path $temporary -Force | Out-Null
+try {
+ $executable = Join-Path $temporary 'hip-smoke.exe'
+ Invoke-CheckedCommand -FilePath $hipcc -ArgumentList @(
+ (Join-Path $PSScriptRoot 'hip-smoke.cpp'), '-O2', '-o', $executable
+ ) -DisplayName 'HIP kernel compilation'
+ $evidence = (& $executable $DeviceIndex 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $evidence -notmatch '^HIP_KERNEL_READY') {
+ throw "HIP kernel acceptance failed (exit $LASTEXITCODE): $evidence"
+ }
+ $deviceMatch = [regex]::Match($evidence, '^HIP_KERNEL_READY device_index=([0-9]+) device=(.+?) value=42$')
+ if (-not $deviceMatch.Success) {
+ throw "HIP kernel evidence did not contain the selected device: $evidence"
+ }
+ $actualGpuName = $deviceMatch.Groups[2].Value
+ if (-not (Test-AiDeviceNameMatch -Expected $gpuName -Actual $actualGpuName)) {
+ throw "HIP device index $DeviceIndex executed on '$actualGpuName', but acquisition was resolved for '$gpuName' ($gfx). Use the matching -DeviceIndex."
+ }
+} finally {
+ Remove-Item -LiteralPath $temporary -Recurse -Force -ErrorAction SilentlyContinue
+}
+Set-Content -LiteralPath $statePath -Value $desired -Encoding ascii
+$report.acceptance.hipKernel = [ordered]@{
+ compiled = $true
+ executed = $true
+ evidence = $evidence
+ gpu = $gpuName
+ actualGpu = $actualGpuName
+ deviceIndex = $DeviceIndex
+ gfxTarget = $gfx
+ hostCompiler = $compiler
+}
+$report.acquisitions[0].action = $(if ($packagesCurrent) { 'already-current' } else { 'installed-or-upgraded' })
+Add-AiReportPhase -Report $report -Name 'hip-kernel' -Status 'ready' -Evidence $report.acceptance.hipKernel
+Complete-AiWorkloadReport -Report $report -Ready $true -Path $ReportPath
+Write-Host "ROCM_READY: $gpuName ($gfx)"
+Write-Host 'INSTALL_OK: rocm'
diff --git a/src/docs/ai-workload-report.schema.json b/src/docs/ai-workload-report.schema.json
new file mode 100644
index 00000000..e438d75c
--- /dev/null
+++ b/src/docs/ai-workload-report.schema.json
@@ -0,0 +1,46 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/microsoft/WindowsDeveloperConfig/blob/main/src/docs/ai-workload-report.schema.json",
+ "title": "Windows Developer Config AI workload report",
+ "type": "object",
+ "required": ["schemaVersion", "workload", "startedAtUtc", "host", "request", "acquisitions", "phases", "acceptance", "result"],
+ "properties": {
+ "schemaVersion": { "const": 1 },
+ "workload": { "type": "string" },
+ "startedAtUtc": { "type": "string", "format": "date-time" },
+ "completedAtUtc": { "type": ["string", "null"], "format": "date-time" },
+ "host": {
+ "type": "object",
+ "required": ["osBuild", "architecture", "gpus"],
+ "properties": {
+ "os": { "type": ["string", "null"] },
+ "osVersion": { "type": ["string", "null"] },
+ "osBuild": { "type": ["string", "null"] },
+ "architecture": { "enum": ["X64", "Arm64"] },
+ "powershell": { "type": "string" },
+ "gpus": { "type": "array" }
+ }
+ },
+ "request": { "type": "object" },
+ "acquisitions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["component", "sourceType", "action"]
+ }
+ },
+ "phases": { "type": "array" },
+ "acceptance": { "type": "object" },
+ "result": {
+ "type": "object",
+ "required": ["ready", "planOnly", "warnings", "blockers"],
+ "properties": {
+ "ready": { "type": "boolean" },
+ "planOnly": { "type": "boolean" },
+ "fallbackUsed": { "type": "boolean" },
+ "warnings": { "type": "array" },
+ "blockers": { "type": "array" }
+ }
+ }
+ }
+}
diff --git a/src/docs/development.md b/src/docs/development.md
index a25f1f88..db06eaf8 100644
--- a/src/docs/development.md
+++ b/src/docs/development.md
@@ -7,7 +7,7 @@
Opinionated, CI-validated configurations for bootstrapping developer
toolchains and Windows-desktop personalities.
-Most flows are built around a [winget DSC configuration
+Most language and desktop flows are built around a [winget DSC configuration
file](https://learn.microsoft.com/windows/package-manager/configuration/)
(`configuration.winget`) — a declarative, idempotent description of the
machine state required for that flow. Where winget alone is not enough
@@ -17,13 +17,21 @@ resource, so everything the flow needs lives in one YAML file. A small
`install.ps1` shim next to it applies the config with `winget configure`
and handles session-level glue (PATH refresh, CI sentinel).
-Two flows are **PowerShell-native** instead: Windows Dev Config
-(`src/windows-dev-config/`) and Comfort Shell (`src/wsl-comfort/`). They
-need work a configuration file doesn't express well — elevation, a reboot with an
-automatic resume, an interactive progress display — so they ship as
-PowerShell scripts with no configuration file at all. They keep the same
-idempotency contract: every step checks current state, acts only when
-needed, and verifies the result.
+The AI workloads are PowerShell-native. They follow Windows Dev Config's
+resumable check → apply → verify contracts and reuse its WinGet, retry, process,
+and PATH helpers through `Workloads/_common/direct-setup.ps1`. They need runtime
+hardware selection, contained Python environments, verified release assets, and
+real device/model acceptance that is awkward and misleading inside static DSC.
+The shared package state machine distinguishes absent, upgrade-available, and
+current packages: it installs only absent IDs, uses exact `winget upgrade` for
+outdated packages, and skips current packages. If the WinGet module operation
+fails but `winget.exe` is usable, it retries the same exact operation through
+the CLI and verifies the settled state.
+
+Windows Dev Config and Comfort Shell are also PowerShell-native because they
+need elevation, reboot/resume, or interactive orchestration. All
+PowerShell-native flows keep the same idempotency contract: every step checks
+current state, acts only when needed, and verifies the result.
Every automated flow is **exercised on a real GitHub-hosted runner** on every
push, pull request, and nightly: the flow is applied, then a canonical "hello
@@ -33,8 +41,8 @@ configuration actually produced a working toolchain.
## Supported flows
-Each flow's `configuration.winget` — or, for the two PowerShell-native
-flows, its entry script — is the source of truth for what gets installed;
+Each flow's `configuration.winget` or PowerShell-native entry script is the
+source of truth for what gets installed;
the table below summarizes it for quick scanning. Flows marked **manual**
are excluded from the automated CI matrix (they need an interactive
desktop session or pull multi-GB workloads we don't want to chew minutes
@@ -56,6 +64,14 @@ Command Palette extension.
| WinAppCLI | ✅ automated | Developer Mode + `Microsoft.DotNet.SDK.10` + `Microsoft.WinAppCli` |
| WinUI 3 | 🙋 manual | `Microsoft.DotNet.SDK.10`, `Microsoft.VisualStudio.Community`, `Microsoft.WinAppCli` + WinUI/Universal/ManagedDesktop VS workloads |
| Windows Dev Config | 🙋 manual | PowerShell setup for developer tools, Windows settings, fonts, Terminal, and WSL + Ubuntu (see [`windows-dev-config/README.md`](../windows-dev-config/README.md)) |
+| NVIDIA CUDA | 🙋 manual | `Nvidia.CUDA` x64 or checksum/signature-pinned 13.4 ARM64 preview + MSVC + GPU kernel |
+| AMD ROCm / HIP | 🙋 manual | ROCm Core SDK 10.0 on supported Windows x64 AMD GPUs + compiled HIP kernel |
+| Intel AI | 🙋 manual | OpenVINO device inference; optional oneAPI/SYCL toolkit and GPU kernel |
+| Foundry Local | 🙋 manual | `Microsoft.FoundryLocal` architecture-native WinML package + Qwen3 inference; no CUDA dependency |
+| PyTorch | 🙋 manual | `Python.Python.3.13` + private CPU/CUDA/ROCm/XPU environment + supported Triton provider |
+| Local AI development | 🙋 manual | Scenario: hardware inventory + contained PyTorch/Triton + one optional model runtime |
+| llama.cpp | 🙋 manual | SHA-256-verified official rolling CUDA/ROCm/SYCL/OpenVINO/Vulkan/OpenCL/CPU assets + pinned GGUF |
+| Ollama | 🙋 manual | `Ollama.Ollama` x64 application or Dev Config-managed official native ARM64 archive + model inference |
| Comfort Shell | 🙋 manual | WSL distro + zsh/bash + starship + modern CLI bundle + Cascadia Code Nerd Font + themed Windows Terminal profile (see [`wsl-comfort/readme.md`](../wsl-comfort/readme.md)) |
See [`manifest.yml`](../manifest.yml) for the canonical declarative
@@ -65,8 +81,8 @@ list (paths, build/run commands, onboarding URLs).
A [PowerToys Command Palette](https://learn.microsoft.com/windows/powertoys/command-palette/overview)
extension lives under [`future/cmdpal/`](../future/cmdpal/). It reads the same
-`manifest.yml` as CI and lets you browse + launch any flow without remembering
-which `configuration.winget` to point `winget` at.
+`manifest.yml` as CI and launches DSC-backed or PowerShell-native flows from one
+list.
The UX metadata each flow needs (`name`, `description`, `category`, `tags`,
`icon`, `onboardingUrl`) is colocated with the CI fields in `manifest.yml` so
@@ -77,7 +93,7 @@ for build + configuration details.
```
Workloads/
- _common/ # shared PowerShell shim helpers (retry, refresh PATH, preflight, assert-winget-configure, apply-configuration)
+ _common/ # shared DSC glue plus direct AI acquisition, resolver catalog, and reporting helpers
typescript/ # configuration.winget (core) + install.ps1 (thin shim)
php/ # configuration.winget (core) + install.ps1 (thin shim)
python/ # configuration.winget (core) + install.ps1 (thin shim)
@@ -87,6 +103,13 @@ Workloads/
rust/ # configuration.winget (core) + install.ps1 (thin shim)
winforms/ # configuration.winget (core) + install.ps1 (thin shim)
winui/ # configuration.winget (core) + install.ps1 (thin shim)
+ cuda/ # x64/ARM64 CUDA + MSVC + compiled GPU-kernel readiness
+ rocm/ # Windows x64 AMD ROCm Core SDK + compiled HIP kernel
+ intel-ai/ # Windows x64 OpenVINO and optional oneAPI/SYCL
+ foundry/ # x64/ARM64 Foundry Local + catalog-model inference
+ pytorch/ # x64/ARM64 Python + contained backend-selected environment
+ llama.cpp/ # hardware-selected official rolling backend + pinned GGUF inference
+ ollama/ # architecture-specific direct acquisition + library-model inference
windows-dev-config/ # Windows Dev Config — bootstrap.ps1 (remote entry) + dev-config.ps1 (orchestrator) + steps/*.ps1 + README.md
wsl-comfort/ # Comfort Shell — install.ps1 (Windows side) + comfort-shell-bootstrap.sh (Linux side, self-contained) + readme.md
tests/
@@ -154,9 +177,10 @@ Maintainers: once this guard has landed, add **`Signed copy guard`** to the requ
## Prerequisites (Windows)
-Every flow — and the [Command Palette extension](../future/cmdpal/) — installs
-toolchains through `winget configure`. That subcommand must be available on
-your machine before anything in this repo can succeed:
+DSC-backed language/desktop flows install through `winget configure`. The
+PowerShell-native AI and workstation flows do not require the configure
+subcommand; they use WinGet's package API/CLI directly when a package is
+available and verified vendor artifacts otherwise.
- **App Installer (winget)** must be current. Update from the Microsoft
Store, or grab the latest MSIX from
@@ -203,6 +227,18 @@ session and prints a CI-friendly sentinel:
-Expected tests/typescript/expected.txt
```
+AI flows are always launched through their PowerShell entry point:
+
+```powershell
+.\Workloads\cuda\install.ps1
+.\Workloads\rocm\install.ps1
+.\Workloads\intel-ai\install.ps1
+.\Workloads\pytorch\install.ps1
+```
+
+Use `-PlanOnly` to resolve hardware, architecture, channel, and planned
+acquisitions without changing the machine. Each run emits an `AI_REPORT:` path.
+
## Testing and verifying locally
CI runs each flow on a fresh `windows-latest` runner, so the highest-fidelity
@@ -245,6 +281,385 @@ installed, also run:
Invoke-ScriptAnalyzer -Recurse -Path ./Workloads, ./tests/_harness
```
+The hardware-dependent AI workloads also provide pure decision tests that do
+not require a GPU or install software:
+
+```powershell
+foreach ($id in 'ai-common','cuda','rocm','intel-ai','foundry','pytorch','llama.cpp','ollama') {
+ & ".\tests\$id\unit.ps1"
+}
+```
+
+These cover architecture selection, backend and dependency decisions,
+idempotent plan construction, missing tool/hardware errors, and generated pip
+arguments. Their runtime probes remain `manual_test` because hosted CI cannot
+exercise the required GPUs, local servers, or multi-gigabyte installers.
+
+### AI workload support and manual verification
+
+Run from the `src` directory:
+
+```powershell
+.\Workloads\local-ai\install.ps1
+.\Workloads\cuda\install.ps1
+.\Workloads\rocm\install.ps1
+.\Workloads\intel-ai\install.ps1
+.\Workloads\foundry\install.ps1
+.\Workloads\pytorch\install.ps1
+.\Workloads\llama.cpp\install.ps1
+.\Workloads\ollama\install.ps1
+```
+
+The scenario entry point is the recommended first run. It installs only the
+hardware-selected contained PyTorch backend and compatible Triton, executes a
+tensor plus a minimal neural-network forward pass, and optionally invokes one
+runtime:
+
+```powershell
+$url = 'https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/main/src/windows-dev-config/bootstrap.ps1'
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai
+
+# PR/source validation before a sign cycle:
+# First apply the documented temporary CurrentUser Bypass policy, then restore it.
+$prHead = gh pr view 104 --repo microsoft/WindowsDeveloperConfig `
+ --json headRefOid --jq .headRefOid
+$prUrl = "https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/$prHead/src/windows-dev-config/bootstrap.ps1"
+& ([scriptblock]::Create((irm $prUrl))) `
+ -Ref $prHead -Scenario local-ai -AllowUnsigned
+
+.\Workloads\local-ai\install.ps1
+# PYTORCH_SMOKE=... "model_forward_verified": true ...
+# PYTORCH_READY: backend=, ...
+# LOCAL_AI_SCENARIO_READY: backend=Auto, runtime=None, ...
+
+.\Workloads\local-ai\install.ps1 -Runtime LlamaCpp
+```
+
+`bootstrap.ps1 -Scenario local-ai` is the product-level dispatcher. It selects
+signed top-level `Workloads/` by default (or explicit unsigned `src/Workloads/`
+for branch testing), verifies/copies the full dependency tree and Windows Dev
+Config helper steps into a protected scenario root, verifies non-PowerShell
+inputs against the Microsoft-signed `_common/content-hashes.ps1`, and launches
+only the local AI scenario. It does not run the full workstation installer.
+Unsigned branch tests are isolated under `%ProgramData%\CalmOS-Development`;
+production signed payloads remain under `%ProgramData%\CalmOS`.
+
+Transitive acquisition:
+
+| Entry point | Conditional dependencies |
+| --- | --- |
+| `local-ai` | Inventory → PyTorch Auto. NVIDIA torch runtime; MSVC + standalone CUDA only for Triton JIT. AMD device runtime tuple in the PyTorch venv, not native `hipcc`. Intel XPU + `triton-xpu`, not full oneAPI. CPU tuple only. |
+| `local-ai -Runtime LlamaCpp` | PyTorch stack + selected llama runtime + quick GGUF; NVIDIA llama assets include `cudart` and do not independently require full CUDA. |
+| `local-ai -Runtime Ollama` | PyTorch stack + source-managed Ollama backend/model; actual allocation reported. |
+| `local-ai -Runtime Foundry` | PyTorch stack + source-managed Foundry EP/model; actual EP/fallback reported. |
+| `pytorch` | Same exact backend rules as the scenario core. |
+| `cuda` / `rocm` / `intel-ai` | Native developer toolkit flows; do not install PyTorch or every model runtime. |
+| `llama.cpp` / `ollama` / `foundry` | Independent model runtime and quick model only; do not install the other runtimes. |
+
+Drivers are qualified prerequisites and are never replaced.
+
+Ollama x64 remains the registered `Ollama.Ollama` application. ARM64 resolves
+the latest non-prerelease official `ollama-windows-arm64.zip`, verifies its
+GitHub-published digest and ARM64 PE machine type, then provides managed
+application semantics under `%LOCALAPPDATA%\Programs\Ollama`: atomic upgrade,
+user PATH, an HKCU Run startup entry, and `.devconfig-install.json`. Use
+`.\Workloads\ollama\install.ps1 -Uninstall` to remove runtime/startup/PATH while
+preserving `%USERPROFILE%\.ollama\models`; add `-RemoveModels` for explicit
+model deletion. The x64 setup executable is never run under emulation and the
+portable WinGet identity is never selected.
+
+This is not a general Python package manager or a request to install every AI
+SDK. Native `cuda`, `rocm`, and `intel-ai` remain independent developer-toolkit
+flows. Foundry, llama.cpp, and Ollama are optional model-runtime choices.
+
+After llama.cpp validation, an opt-in coding example uses a pinned ~1.04 GB
+Apache-2.0 Qwen2.5-Coder model without enlarging the default install:
+
+```powershell
+.\Workloads\llama.cpp\coding-demo.ps1 `
+ -ReportPath "$env:TEMP\llama-coding-demo.json"
+# Expected: Python def group_anagrams(...) and CODING_DEMO_READY
+```
+
+| Flow | x64 behavior | ARM64 behavior | Readiness signal |
+| --- | --- | --- | --- |
+| CUDA | Current stable CUDA 13 `Nvidia.CUDA` + MSVC; driver 580+/CC7.5+ for GPU readiness | Checksum- and Authenticode-verified NVIDIA CUDA 13.4 Developer Preview + ARM64 MSVC | Compile and execute `smoke.cu`; `-SkipWorkloadSmoke` opts out |
+| ROCm / HIP | AMD stable ROCm 10.0 feed on supported Radeon/Ryzen AI GPUs | Unsupported | Compile and execute `hip-smoke.cpp`; exact GPU maps to a published `gfx` target |
+| Intel AI | OpenVINO CPU/GPU/NPU; optional oneAPI/SYCL GPU tooling | Unsupported | Generated OpenVINO model executes on requested device; Full profile also runs a SYCL kernel |
+| Foundry | WinGet x64 WinML package | WinGet ARM64 WinML package | Download `qwen3-0.6b` (~593 MB) and generate a marker; CUDA is never assumed |
+| PyTorch CUDA | Stable NVIDIA CUDA wheel selected by driver/device | Pinned NVIDIA CUDA 13.4 qualified interim wheel for CPython 3.13/RTX Spark | Self-contained wheel runtime, device tensor/model forward, and supported Triton CUDA kernel |
+| PyTorch ROCm | AMD stable feed with exact device `gfx` package | Unsupported | Self-contained AMD runtime tuple, HIP runtime assertion, AMD device tensor; no native Windows Triton |
+| PyTorch XPU | Official PyTorch XPU index | Unsupported | Self-contained XPU tuple, Intel device tensor, and `triton-xpu`/`torch.compile` |
+| llama.cpp CUDA | Paired CUDA 13.3/12.4 app+cudart assets selected by driver/capability | Paired CUDA 13.4 Developer Preview app+cudart on qualified N1X | `llama-bench` proves CUDA device/GPU layers; pinned GGUF inference generates a constrained marker |
+| llama.cpp ROCm | Official ROCm 10.0 asset on a supported AMD GPU | Unsupported | Benchmark must identify ROCm/AMD and GPU layers before inference is ready |
+| llama.cpp SYCL / OpenVINO | Official SYCL or explicit OpenVINO 2026.3.1 asset | Unsupported | Intel Auto prefers SYCL for direct GPU evidence; OpenVINO is explicit and does not imply NPU support |
+| llama.cpp OpenCL Adreno | Unsupported | Official Qualcomm Adreno OpenCL asset | Requires the Windows OpenCL loader; benchmark must identify OpenCL/Adreno and GPU layers |
+| llama.cpp Vulkan / CPU fallback | Official rolling backend-specific assets | CPU asset | Vulkan is x64 Auto fallback only; CPU reports zero GPU layers |
+| Ollama | Current WinGet desktop package | Current official native ARM64 archive installed as a managed per-user application | Official `qwen3:0.6b` (~522 MB) blob hash verification + structured inference |
+
+PyTorch's environment is
+`$env:LOCALAPPDATA\DevConfig\pytorch\.venv`. Auto selection is supported
+NVIDIA CUDA → supported AMD ROCm → supported Intel XPU → CPU. Explicit
+selection can target a supported secondary adapter. `ROCm` installs AMD's
+device-specific torch/torchvision/torchaudio tuple inside the venv; it does not
+require the standalone ROCm Core SDK. `XPU` installs the official XPU tuple and
+`triton-xpu`; it does not install full oneAPI. CUDA wheels carry their runtime;
+the standalone CUDA toolkit is acquired only when the selected Triton/JIT path
+needs native toolchain components. Use `-RequireTriton` when supported Triton
+execution is mandatory or `-SkipTriton` to disable it.
+
+Vendor layer mapping:
+
+| Vendor | Native layer | PyTorch layer |
+| --- | --- | --- |
+| NVIDIA | `cuda`: nvcc/toolkit + compiled native kernel | CUDA wheel runtime; automatic compiler/toolkit acquisition only for supported Triton JIT |
+| AMD | `rocm`: Core SDK/hipcc + compiled HIP kernel | AMD device-specific runtime tuple in PyTorch venv; no dependency on `rocm` for tensor inference |
+| Intel | `intel-ai`: OpenVINO runtime; optional full oneAPI/SYCL | Official XPU wheel + `triton-xpu` in PyTorch venv; no full oneAPI dependency |
+
+The Windows ARM64 CUDA path is a pinned NVIDIA/PyTorch developer-preview stack:
+CUDA 13.4 and `torch-2.15.0.dev20260904+cu134` for CPython 3.13. The direct
+wheel URL includes NVIDIA's SHA-256 fragment, while ordinary dependencies
+(including NumPy) resolve through the user's configured default Python index.
+The flow never uses `--extra-index-url`, which would mix untrusted candidates.
+The 1.85 GB wheel is downloaded once into
+`%LOCALAPPDATA%\DevConfig\pytorch\wheel-cache`, hash-verified, and installed
+from that local cache. Reruns compare the desired state with exact installed
+torch, NumPy, and Triton versions; matching environments skip package work but
+still execute the tensor and Triton kernel probes.
+
+Foundry, llama.cpp, and Ollama accept `-SkipModelSmoke`; CUDA accepts
+`-SkipWorkloadSmoke`. These opt-outs avoid the default model/kernel acceptance
+tests, but the resulting run is installation-only and does not claim full
+workload readiness. Model licenses are Apache-2.0. Foundry reports its mutable
+cache via `foundry cache location`; llama.cpp pins an immutable Qwen revision,
+size, and SHA-256 under `%LOCALAPPDATA%\DevConfig\llama.cpp\models`; Ollama
+verifies the pinned content-addressed model blob under
+`%USERPROFILE%\.ollama\models` (or `OLLAMA_MODELS`).
+
+Every standalone AI workload accepts `-PlanOnly` and `-ReportPath`; the
+`local-ai` scenario accepts `-PlanOnly` and `-ReportRoot`. Plan mode is safe on
+unsupported machines: it writes blockers and planned acquisitions without
+changing the system. Reports conform to
+[`docs/ai-workload-report.schema.json`](./ai-workload-report.schema.json).
+Collect just the portable host inventory with:
+
+```powershell
+.\src\tools\collect-ai-hardware.ps1
+.\src\tools\get-ai-capabilities.ps1 -OutputPath "$env:TEMP\ai-capabilities.json"
+```
+
+Current real-hardware coverage:
+
+| Host | Validated workloads |
+| --- | --- |
+| Windows 11 ARM64 build 28120, NVIDIA RTX Spark N1X, driver 616.62 | CUDA 13.4 kernel; PyTorch cu134 tensor + neural forward; Triton vector-add; Foundry qwen3-0.6b inference; llama.cpp CUDA inference; optional Qwen2.5-Coder-1.5B `group_anagrams` generation at 101.7 t/s; managed native Ollama 0.34.4 install/rerun/uninstall-preserve/reinstall with verified release + qwen3:0.6b digests, persistent endpoint, native PE evidence, and `/api/ps` at 100% GPU |
+| Supported Windows x64 NVIDIA GPU | Partner run pending: llama.cpp CUDA 13.3/12.4 benchmark and inference |
+| Supported Windows x64 AMD GPU | Partner run pending: ROCm/HIP kernel, PyTorch ROCm tensor, and llama.cpp ROCm benchmark/inference |
+| Supported Windows x64 Intel GPU/NPU | Partner run pending: OpenVINO selected-device inference, optional SYCL kernel, PyTorch XPU/torch.compile, and llama.cpp SYCL/OpenVINO benchmark/inference |
+| Supported Windows ARM64 Qualcomm/Adreno GPU | Partner run pending: llama.cpp OpenCL/Adreno benchmark and inference |
+
+Known vendor gaps and boundaries:
+
+| Vendor | GPU workloads | NPU workloads | Windows CPU architecture | Maturity / validation | Boundary |
+| --- | --- | --- | --- | --- | --- |
+| NVIDIA | CUDA native kernel, PyTorch CUDA/Triton, llama.cpp CUDA | No vendor-specific NPU flow; Foundry/WinML is separate | x64 partner pending; ARM64 N1X live validated | x64 stable; ARM64 CUDA/PyTorch developer preview | Foundry N1X acceptance is truthful CPU fallback because CUDA EP registration was unavailable |
+| AMD | ROCm/HIP, PyTorch ROCm, llama.cpp ROCm on published gfx matrix | None; ROCm does not cover Ryzen AI NPU | x64 only | Static/resolver complete, partner pending | No native Windows AMD Triton; no Ollama AMD or Foundry AMD EP claim without backend evidence |
+| Intel | OpenVINO GPU, oneAPI/SYCL, PyTorch XPU/compile, llama.cpp SYCL/OpenVINO | OpenVINO NPU only after actual selected-device inference | x64 only | Static/resolver complete, partner pending | PyTorch XPU and llama SYCL are GPU paths, not NPU paths |
+| Qualcomm | llama.cpp Adreno OpenCL; Foundry/WinML vendor-neutral provider path | WinML/Foundry only when provider evidence proves it | ARM64 | Static/resolver complete, partner pending | No native PyTorch backend; Ollama ARM64 is CPU/NVIDIA capability unless Adreno evidence exists |
+| Other/fallback | Vulkan x64 and CPU x64/ARM64 | None | As listed | Compatibility only | Never label Vulkan/CPU as vendor-native; Mali/other Windows stacks are unimplemented without official artifacts |
+
+GPU drivers are preconditions. The flows report installed driver versions and
+give remediation for unsupported versions, but do not replace GPU drivers.
+Explicit backend selection can choose a supported secondary vendor on a mixed
+system. Same-vendor targeting uses `-DeviceIndex` for CUDA/ROCm and explicit PyTorch backends,
+`-Device` for llama.cpp runtime identifiers, `-OpenVinoDeviceId` for OpenVINO,
+and `-SyclDeviceSelector` for oneAPI. Foundry and Ollama are source-managed:
+they report the actual provider/allocation and do not imply a selector.
+
+Code path readiness before hardware testing:
+
+| Supported combination | Resolver | Acquisition | Workload probe | Report contract | Static/unit | Live hardware |
+| --- | --- | --- | --- | --- | --- | --- |
+| CUDA / NVIDIA x64 | Yes | Stable WinGet + MSVC | Native CUDA kernel | Driver/device/compiler/kernel | Pass | Partner pending |
+| CUDA / NVIDIA ARM64 N1X | Yes | Pinned preview + MSVC ARM64 | Native CUDA kernel | Hash/signature/driver/device/kernel | Pass | **Passed** |
+| ROCm/HIP / AMD x64 gfx matrix | Yes | AMD stable tuple | Native HIP kernel | Runtime/gfx/actual device/kernel | Pass | Partner pending |
+| Intel OpenVINO CPU x64 | Yes | Official Python tuple | Generated-model CPU inference | Requested/actual device | Pass | Partner pending |
+| Intel OpenVINO GPU x64 | Yes | Official Python tuple | Generated-model GPU inference | Requested/actual GPU | Pass | Partner pending |
+| Intel OpenVINO NPU x64 | Yes | Official Python tuple | Generated-model NPU inference | Requested/actual NPU | Pass | Partner pending |
+| Intel oneAPI/SYCL GPU x64 | Yes | Stable WinGet | Native SYCL kernel | Selector/device/compiler/kernel | Pass | Partner pending |
+| Intel Full GPU x64 | Yes | OpenVINO + oneAPI | GPU inference + SYCL kernel | Both acceptance records | Pass | Partner pending |
+| PyTorch CPU x64 | Yes | Official CPU wheel | Tensor + NumPy bridge | Tuple/device/operation | Pass | Partner pending |
+| PyTorch CPU ARM64 | Yes | Official CPU wheel | Tensor + NumPy bridge | Native tuple/device/operation | Pass | Partner pending |
+| PyTorch CUDA x64 | Yes | Official CUDA wheel | NVIDIA tensor | Runtime/device/tuple/operation | Pass | Partner pending |
+| PyTorch CUDA ARM64 N1X | Yes | Pinned nightly wheel | NVIDIA tensor | Hash/runtime/device/operation | Pass | **Passed** |
+| PyTorch ROCm AMD x64 | Yes | Exact AMD gfx tuple | HIP-compatible AMD tensor | HIP runtime/gfx/device/tuple | Pass | Partner pending |
+| PyTorch XPU Intel x64 | Yes | Official XPU tuple | Intel XPU tensor | XPU device/tuple/operation | Pass | Partner pending |
+| Triton CUDA x64 | Yes | Community Windows wheel + JIT toolchain | Vector-add kernel | Distribution/device/kernel | Pass | Partner pending |
+| Triton CUDA ARM64 N1X | Yes | Community wheel + preview JIT stack | Vector-add kernel | Distribution/device/kernel | Pass | **Passed** |
+| Triton XPU x64 | Yes | Official `triton-xpu` | Cold `torch.compile` | Intel device/compile evidence | Pass | Partner pending |
+| llama CPU x64 | Yes | Official rolling CPU asset | Benchmark + GGUF inference | Tag/digest/zero offload/inference | Pass | Partner pending |
+| llama CPU ARM64 | Yes | Official rolling CPU asset | Benchmark + GGUF inference | Tag/digest/zero offload/inference | Pass | Partner pending |
+| llama CUDA x64 | Yes | Complete rolling app+cudart pair | Benchmark + GGUF inference | Tag/digests/backend/device/actual offload | Pass | Partner pending |
+| llama CUDA ARM64 N1X | Yes | Complete rolling app+cudart pair | Benchmark + GGUF inference | Tag/digests/backend/device/actual offload | Pass | **Passed** |
+| llama ROCm AMD x64 | Yes | Official rolling ROCm asset | Benchmark + GGUF inference | Tag/digest/backend/device/actual offload | Pass | Partner pending |
+| llama SYCL Intel x64 | Yes | Official rolling SYCL asset | Benchmark + GGUF inference | Tag/digest/backend/device/actual offload | Pass | Partner pending |
+| llama OpenVINO x64 | Yes | Official rolling OpenVINO asset | Benchmark + GGUF inference | Tag/digest/backend/device/actual offload | Pass | Partner pending |
+| llama Vulkan x64 fallback | Yes | Official rolling Vulkan asset | Benchmark + GGUF inference | Explicit fallback/backend/device/offload | Pass | Partner pending |
+| llama Adreno OpenCL ARM64 | Yes | Official rolling OpenCL asset | Benchmark + GGUF inference | Tag/digest/OpenCL/Adreno/actual offload | Pass | Partner pending |
+| Foundry x64 | Yes | Architecture-native WinGet | Real catalog-model inference | Source-managed actual EP/device/fallback | Pass | Partner pending |
+| Foundry ARM64 | Yes | Architecture-native WinGet | Real catalog-model inference | Source-managed actual EP/device/fallback | Pass | **Passed on N1X CPU EP** |
+| Ollama x64 | Yes | Stable WinGet | Verified model + inference | Source-managed CPU/GPU allocation/backend | Pass | Partner pending |
+| Ollama ARM64 | Yes | Official stable native archive + Dev Config-managed install lifecycle | Verified model + inference | Native PE, manifest/startup/PATH, allocation/backend | Pass | **Passed on N1X NVIDIA GPU** |
+
+The executable source of truth is
+`Workloads/_common/ai-catalog.psd1::CapabilityMatrix`. The shared unit suite
+invokes every supported/source-managed resolver fixture and verifies its
+acquisition identities, probe path, report evidence contract, and partner
+command. Every `upstream-unavailable` cell must throw its cataloged blocker.
+
+llama.cpp Auto resolution is supported NVIDIA CUDA → supported AMD ROCm →
+supported Intel SYCL → Qualcomm Adreno OpenCL → x64 Vulkan → CPU. Intel SYCL
+is preferred because the artifact directly exercises the Intel GPU and produces
+device/offload evidence. OpenVINO remains an explicit x64 alternative for its
+general inference backend; readiness requires the benchmark to identify the
+OpenVINO backend and offloaded layers, and the flow does not claim NPU support.
+All selected archives come from one rolling release, including both app and
+cudart archives for CUDA. SHA-256 digests are required, verified archives are
+cached under `%LOCALAPPDATA%\DevConfig\llama.cpp\asset-cache`, and the runtime
+is replaced atomically. Acceptance reads the official `backends` JSON field and
+requires actual `offloaded X/Y layers` diagnostics; the requested `-ngl` value
+is never used as proof of acceleration. `gpu_info` supplies the physical device;
+the structured `devices` value is the requested selector and must match an
+explicit `-Device`.
+
+### Universal partner hardware workflow
+
+```powershell
+git fetch https://github.com/Kixantrix/WindowsDeveloperConfig.git `
+ mihippel-microsoft-windows-ai-setup-workloads
+git switch --detach FETCH_HEAD
+$ExpectedHead = (
+ git ls-remote https://github.com/Kixantrix/WindowsDeveloperConfig.git `
+ refs/heads/mihippel-microsoft-windows-ai-setup-workloads
+).Split("`t")[0]
+if ((git rev-parse HEAD).Trim() -ne $ExpectedHead) {
+ throw "Checkout does not match published branch head $ExpectedHead."
+}
+```
+
+Then open elevated PowerShell in the repository root. Inventory the host and
+use the plan/apply harness from the README's **Partner validation commands**
+section. Each assigned flow must first write `-plan.json`, stop on any
+blocker, then write `-final.json` and satisfy `result.ready=true`.
+
+Because PR testing runs the unsigned source under `src/`, first use the
+repository's unsigned-development procedure: record the test user's current
+policy, set `CurrentUser` to `Bypass` in both Windows PowerShell and PowerShell
+7, and restore it after validation. Production bootstrap explicitly verifies
+Microsoft signatures and requests process-scoped `RemoteSigned`. Signed
+top-level release copies are also tested for compatibility with
+organization-enforced `AllSigned` in both hosts by
+`src/tests/ai-common/all-signed.ps1`; a first run may prompt to trust the
+Microsoft publisher.
+
+Assigned flow coverage:
+
+| Partner device | Required flows |
+| --- | --- |
+| NVIDIA x64 | `cuda`; PyTorch `-Backend CUDA -RequireTriton`; llama.cpp `-Backend CUDA` |
+| AMD x64 | PyTorch `-Backend ROCm`; native `rocm`; llama.cpp `-Backend ROCm` |
+| Intel x64 GPU | PyTorch `-Backend XPU -RequireTriton`; `intel-ai -Device GPU -Profile OpenVINO`; `intel-ai -Device GPU -Profile Full`; llama.cpp `SYCL` and explicit `OpenVINO` |
+| Intel x64 NPU | `intel-ai -Device NPU -Profile OpenVINO` only; XPU and SYCL are GPU paths |
+| Qualcomm ARM64 | llama.cpp `-Backend OpenCL`; Foundry to record its source-managed actual EP/fallback |
+
+Return `hardware.json`, every plan and final report, every plan and final
+console log, and reboot requested/performed status. Review:
+
+- host architecture, GPU/NPU model/vendor/driver;
+- acquisition action/version/source/artifact/integrity/cache/install path;
+- requested and selected backend/device/profile;
+- acceptance marker and intended physical device;
+- CUDA/ROCm runtime and kernel, PyTorch runtime/tensor/Triton, llama.cpp
+ `backends`/`gpu_info`/actual offloaded layers, Foundry EP/fallback, or Ollama
+ VRAM/allocation evidence as applicable;
+- every warning and blocker.
+
+`INSTALL_OK` alone is insufficient. A successful handoff requires
+`result.ready=true` and acceptance evidence identifying the intended
+device/backend. Foundry is source-managed and may truthfully report CPU fallback.
+
+Always use the live published branch head above. The replacement PR targets
+`main`; the former dependent PR #98 closed automatically when its base branch
+was deleted after #93 merged.
+
+### Preview/rolling promotion metadata
+
+`Workloads/_common/ai-catalog.psd1` is the single source of truth for maturity,
+source identity, version policy, integrity validation, cache/install paths,
+normal-channel gaps, expected stable channels, migration triggers, and cleanup.
+The table below summarizes the non-normal channels. “Unconfirmed” means the
+vendor has not announced a final package identity.
+
+Stable-channel decisions:
+
+| Tuple | Current qualified source | Candidate / availability | Promotion gate | Status |
+| --- | --- | --- | --- | --- |
+| CUDA x64 | WinGet `Nvidia.CUDA` | Stable current channel | Native kernel | Current |
+| CUDA ARM64 | Pinned 13.4.0 prerelease | Official signed 13.4.1 direct installer, SHA-256 `39af79e5…2442` | N1X kernel + PyTorch Triton JIT | Candidate tracked |
+| PyTorch CUDA x64 | Official `cu126`/`cu130` indexes | Stable current channels | Tensor + Triton | Current |
+| PyTorch CUDA ARM64 | Pinned N1X-qualified 2.15 nightly | NVIDIA stable out-of-tree 2.14 cu134 torch/vision/audio tuple | Trio imports + N1X tensor + idempotence + Triton | Candidate tracked |
+| Triton CUDA | Exact `triton-windows` community build | No upstream-official Windows package | Vector-add on each CUDA tuple | Community-stable |
+| Triton XPU | Official PyTorch XPU index | Stable current channel | Intel cold compile | Current |
+| Foundry | WinGet 0.10.3 preview | Official non-prerelease v2.0.1; Python metadata still alpha | x64/ARM64 migration + provider/inference/cached rerun | Candidate tracked |
+| llama.cpp | Official rolling backend assets | No backend-complete stable package channel | Per-backend policy + benchmark/inference | Rolling |
+| Ollama x64 / ARM64 | Stable WinGet / official stable ZIP | ARM64 WinGet parity pending | Owned API/version/model/backend | Current / tracked |
+| ROCm / Intel | AMD stable feed / official PyPI+WinGet | Stable current channels | HIP/OpenVINO/SYCL workload | Current |
+
+| Component | Vendor / architecture | Current identity | Resolver rule | Stable target | Evidence required to promote |
+| --- | --- | --- | --- | --- | --- |
+| CUDA ARM64 | NVIDIA / ARM64 | qualified 13.4.0 interim; signed 13.4.1 candidate tracked | Keep qualified version until 13.4.1 workload pass | NVIDIA stable direct / `Nvidia.CUDA` ARM64 if published | N1X kernel + Triton |
+| PyTorch CUDA x64 | NVIDIA / x64 | stable 2.14 `cu126`/`cu130` wheel | Driver branch + compute capability select exact stable wheel | official PyTorch CUDA indexes | tensor + Triton vector-add |
+| PyTorch CUDA ARM64 | NVIDIA / ARM64 | pinned qualified 2.15 cu134 nightly; stable 2.14 trio tracked | Exact wheel/hash and CPython 3.13/N1X checks | NVIDIA stable `nvtorch_oot` | trio imports + tensor/Triton acceptance |
+| PyTorch ROCm | AMD / x64 | stable 2.13 ROCm 10 device-specific tuple | Exact supported GPU → `gfx` extra | AMD stable ROCm feed | AMD tensor + non-null HIP runtime |
+| PyTorch XPU | Intel / x64 | stable 2.14 XPU tuple | Validated Intel GPU family | official PyTorch XPU index | XPU tensor + cold `torch.compile` |
+| Triton Windows CUDA | NVIDIA / x64, ARM64 | `triton-windows==3.8.0.post28` | exact PyTorch-compatible community build | official Windows Triton package, unconfirmed | official package + vector-add kernel |
+| Triton XPU | Intel / x64 | `triton-xpu==3.8.0` | exact PyTorch XPU-compatible tuple | official PyTorch XPU index | cold `torch.compile` on XPU |
+| llama.cpp CUDA x64 | NVIDIA / x64 | newest complete CUDA 13.3/12.4 app+cudart set | Driver/capability-qualified exact pair from one release | backend-specific WinGet variant, unconfirmed | NVIDIA device + GPU layers + inference |
+| llama.cpp CUDA ARM64 | NVIDIA / ARM64 | newest complete CUDA 13.4 app+cudart set | Qualified N1X Developer Preview pair | backend-specific WinGet variant, unconfirmed | N1X device + GPU layers + inference |
+| llama.cpp ROCm x64 | AMD / x64 | newest ROCm 10.0 asset | Supported GPU/gfx target | backend-specific WinGet variant, unconfirmed | ROCm/AMD device + GPU layers + inference |
+| llama.cpp SYCL / OpenVINO x64 | Intel/general / x64 | newest SYCL or OpenVINO 2026.3.1 asset | SYCL Auto for supported Intel GPU; OpenVINO explicit | backend-specific WinGet variants, unconfirmed | selected backend/device + GPU layers + inference |
+| llama.cpp OpenCL Adreno ARM64 | Qualcomm / ARM64 | pinned policy-approved b10917 Adreno OpenCL asset | Detected Qualcomm/Adreno adapter | backend-specific WinGet variant, unconfirmed | New candidate passes policy + OpenCL/Adreno layers/inference |
+| llama.cpp Vulkan / CPU | Cross-vendor / x64, CPU / x64+ARM64 | newest backend-specific rolling asset | Vulkan only with loader/device; otherwise CPU | backend-specific WinGet variants, unconfirmed | exact backend and offload/fallback evidence |
+| Foundry Local | cross-vendor / x64, ARM64 | qualified WinGet 0.10.3 preview; v2.0.1 candidate | current qualified package until v2 migration passes | official v2 / current stable WinGet | provider/inference/cached rerun on x64+ARM64 |
+| Ollama ARM64 | CPU/NVIDIA / ARM64 | latest stable official native ARM64 archive with managed Dev Config installation | non-prerelease release asset with GitHub digest | official ARM64 installer/managed package, unconfirmed | package passes native PE + lifecycle + API/GPU evidence |
+
+AMD ROCm 10.0 and Intel OpenVINO/oneAPI use stable vendor channels. AMD's
+normal channel is its stable ROCm feed (there is no confirmed WinGet ID);
+Intel's normal channels are official PyPI packages and `Intel.OneAPI.Toolkit`.
+Neither vendor publishes a native Windows ARM64 stack today.
+
+| Component | Vendor / CPU arch | Maturity | Current source + identity | Resolver / version policy | Integrity | Cache → installed path | Why normal channel is insufficient | Expected final channel | Promotion evidence | Cleanup / upgrade |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| CUDA x64 | NVIDIA / x64 | Stable | WinGet `Nvidia.CUDA` | Latest applicable stable | WinGet manifest hash + signature | WinGet → `%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v*` | N/A | `Nvidia.CUDA` | New package still compiles/runs kernel | WinGet upgrade / NVIDIA uninstall |
+| CUDA ARM64 | NVIDIA / ARM64 | Qualified interim developer preview | NVIDIA `cuda_13.4.0_windows_arm64.exe`; stable 13.4.1 candidate tracked | Keep exact 13.4.0 until candidate qualification | Pinned SHA-256 + NVIDIA signature | `%ProgramData%\WindowsDeveloperConfig\cache` → CUDA v13.4 | No ARM64 WinGet payload; 13.4.1 not yet N1X-qualified | NVIDIA stable direct / WinGet if published | 13.4.1 N1X kernel + Triton | Side-by-side qualify, then vendor uninstall old |
+| PyTorch CUDA ARM64 | NVIDIA / ARM64 | Qualified interim nightly | NVIDIA `torch-2.15.0.dev20260904+cu134-cp313-win_arm64.whl`; stable 2.14 trio tracked | Keep exact qualified wheel until tuple qualification | Pinned SHA-256 | local wheel cache → contained venv | Stable out-of-tree trio awaits N1X qualification | NVIDIA `nvtorch_oot` | Trio imports + stable tensor + Triton kernel | Replace venv; prune old cache |
+| PyTorch CUDA x64 | NVIDIA / x64 | Stable | official `cu126`/`cu130` index, torch 2.14 | Driver/capability-selected exact wheel | official index hashes/RECORD | pip cache → contained venv | N/A | official PyTorch CUDA index | tensor + Triton kernel | Replace venv |
+| PyTorch ROCm | AMD / x64 | Stable | AMD feed: torch 2.13 ROCm 10 + device `gfx` extra, torchvision, torchaudio | Exact GPU-specific runtime tuple | AMD HTTPS + wheel RECORD | pip cache → contained venv | Default PyPI lacks AMD Windows build | AMD stable ROCm feed | HIP non-null + AMD tensor | Replace venv |
+| PyTorch XPU | Intel / x64 | Stable | official XPU index: torch 2.14, torchvision 0.29 | Exact XPU tuple | official index hashes/RECORD | pip cache → contained venv | Default PyPI lacks Intel XPU build | official XPU index | Intel tensor + `torch.compile` | Replace venv |
+| Triton Windows CUDA | NVIDIA / x64, ARM64 | Community | PyPI `triton-windows==3.8.0.post28` | Exact PyTorch-compatible tuple | TLS + wheel RECORD | pip cache → PyTorch venv | No general upstream Windows package | Official Windows Triton package, unconfirmed | Official package + vector-add | Replace venv |
+| Triton XPU / `torch.compile` | Intel / x64 | Stable integrated | XPU index `triton-xpu==3.8.0` | Exact PyTorch XPU tuple | official index hashes/RECORD | pip cache → PyTorch venv | Standalone project documents Linux; Windows path is PyTorch integration | official XPU index | cold compile on actual Intel GPU | Replace venv |
+| llama.cpp CUDA x64 | NVIDIA / x64 | Rolling | newest complete CUDA 13.3 or 12.4 app+cudart pair | Driver/capability-qualified exact pair from one release | GitHub asset SHA-256 digests | asset cache → runtime directory | WinGet maps only to Vulkan | backend-specific `ggml.llamacpp`, unconfirmed | NVIDIA device/GPU layers + inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp CUDA ARM64 | NVIDIA / ARM64 | Rolling developer preview | newest complete CUDA 13.4 app+cudart pair | RTX Spark + driver 616+ exact pair | GitHub asset SHA-256 digests | asset cache → runtime directory | No ARM64 CUDA WinGet variant | backend-specific `ggml.llamacpp`, unconfirmed | N1X device/GPU layers + inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp ROCm x64 | AMD / x64 | Rolling | newest ROCm 10.0 asset | Exact supported AMD GPU/gfx resolver | GitHub asset SHA-256 digest | asset cache → runtime directory | WinGet maps only to Vulkan | backend-specific `ggml.llamacpp`, unconfirmed | ROCm/AMD device/GPU layers + inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp SYCL / OpenVINO x64 | Intel/general / x64 | Rolling | newest SYCL or OpenVINO 2026.3.1 asset | Supported Intel GPU selects SYCL; OpenVINO explicit | GitHub asset SHA-256 digest | asset cache → runtime directory | WinGet maps only to Vulkan | backend-specific `ggml.llamacpp`, unconfirmed | selected backend/device/GPU layers + inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp OpenCL Adreno ARM64 | Qualcomm / ARM64 | Qualified rolling pin | policy-approved b10917 Adreno OpenCL asset | Exact detected Qualcomm/Adreno ARM64 path | GitHub asset SHA-256 digest | asset cache → runtime directory | No ARM64 Adreno WinGet variant; b10919 blocked by managed Defender ASR | backend-specific `ggml.llamacpp`, unconfirmed | Candidate passes policy + OpenCL/Adreno layers/inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp Vulkan x64 | Cross-vendor / x64 | Rolling fallback | newest official Vulkan asset | Auto only after vendor-native paths; requires loader/device | GitHub asset SHA-256 digest | asset cache → runtime directory | WinGet cannot express backend alternatives | reliable Vulkan package variant | Vulkan backend/GPU layers + inference | Reuse verified cache; atomic runtime replacement |
+| llama.cpp CPU x64/ARM64 | CPU / x64, ARM64 | Rolling fallback | newest official CPU asset | Explicit CPU or no qualified accelerator | GitHub asset SHA-256 digest | asset cache → runtime directory | WinGet lacks backend-selectable CPU/ARM64 | backend-specific `ggml.llamacpp`, unconfirmed | CPU backend, zero GPU layers, inference | Reuse verified cache; atomic runtime replacement |
+| Foundry Local | Cross-vendor / x64, ARM64 | Qualified preview | WinGet `Microsoft.FoundryLocal` 0.10.3; official v2.0.1 candidate tracked | Keep qualified CLI until v2 SDK migration passes | WinGet MSIX hash/signature; v2 hashes recorded | Foundry cache → per-user MSIX | v2 changes API/package contract and is not target-qualified | official v2 / current stable WinGet | x64+ARM64 provider/inference/cached rerun | Preserve cache; replace runtime after qualification |
+| Ollama ARM64 | CPU, NVIDIA / ARM64 | Official stable native archive, Dev Config-managed | latest official `ollama-windows-arm64.zip` | Latest non-prerelease release | GitHub asset SHA-256 | verified cache → `%LOCALAPPDATA%\Programs\Ollama`; `.devconfig-install.json`; HKCU startup | No official ARM64 setup EXE; x64 setup and portable WinGet are not used | official ARM64 installer or managed WinGet payload | native PE + managed API/model/backend/upgrade/uninstall | Stop managed processes, atomic swap, preserve models by default |
+| AMD ROCm | AMD / x64 | Stable | AMD stable feed `rocm[...] == 10.0.0` | Exact supported GPU `gfx` tuple | Official HTTPS allowlist + wheel RECORD; feed has no SHA-256 fragments | pip cache → contained venv | No confirmed WinGet ID/default PyPI package | AMD stable feed; WinGet unconfirmed | newer Windows matrix + HIP kernel | Replace contained environment |
+| Intel OpenVINO / oneAPI | Intel / x64 | Stable | PyPI OpenVINO 2026.3.1 tuple; WinGet `Intel.OneAPI.Toolkit` | Exact matched tuple / qualified stable package | wheel RECORD; WinGet hash/signature | pip/WinGet cache → contained venv/oneAPI root | N/A | same official channels | selected-device inference/SYCL kernel | Replace venv; WinGet upgrade |
+
### 2. Validate the DSC config without applying it (Windows)
`winget configure` has a `test` verb that evaluates each resource's
@@ -348,3 +763,17 @@ Adding a language is a **data change**, not a workflow change:
command, and expected-output path for each supported OS.
That's it — `discover` in CI picks up the new flow automatically.
+
+## How to add a hardware-aware AI workload
+
+1. Add a PowerShell-native `Workloads//install.ps1`; do not add a
+ `configuration.winget`.
+2. Reuse `_common/direct-setup.ps1` for the Windows Dev Config WinGet/retry/process/PATH
+ contracts and `_common/ai-report.ps1` for structured output.
+3. Put stable, preview, nightly, and rolling acquisition metadata in
+ `_common/ai-catalog.psd1`. Include the normal-channel limitation and
+ evidence-based promotion trigger.
+4. Add `-PlanOnly`, an actionable unsupported result, and a real hardware
+ workload acceptance. CLI/version checks are diagnostics, not acceptance.
+5. Add unit tests for architecture/vendor selection, exact commands, channel
+ promotion, idempotence, report fields, and unsupported combinations.
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Models/ScriptEntry.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Models/ScriptEntry.cs
index ec559bea..0901f89d 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Models/ScriptEntry.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Models/ScriptEntry.cs
@@ -61,8 +61,7 @@ public class ScriptEntry
///
/// Path to the WinGet DSC configuration the extension applies via
- /// winget configure. Falls back to a sibling
- /// configuration.winget next to install when not set.
+ /// winget configure.
///
public string? WindowsConfigurationPath
{
@@ -76,18 +75,12 @@ public string? WindowsConfigurationPath
{
return Windows.Configuration;
}
- if (!string.IsNullOrEmpty(Windows.Install))
- {
- var dir = System.IO.Path.GetDirectoryName(Windows.Install)?.Replace('\\', '/');
- if (!string.IsNullOrEmpty(dir))
- {
- return $"{dir}/configuration.winget";
- }
- }
return null;
}
}
+ public string? WindowsInstallPath => Windows?.Install;
+
/// WSL/Linux install script path, e.g. scripts/linux/php/install.sh.
public string? LinuxInstallPath => Linux?.Install;
}
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/QuickWingetSetupPage.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/QuickWingetSetupPage.cs
index deb6b7ce..dc8123bf 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/QuickWingetSetupPage.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/QuickWingetSetupPage.cs
@@ -66,7 +66,8 @@ public override IListItem[] GetItems()
// in a wt.exe tab). Hide flows that don't declare Windows
// support so users don't pick something the extension
// can't actually launch.
- .Where(s => s.WindowsConfigurationPath is not null)
+ .Where(s => s.WindowsConfigurationPath is not null ||
+ (_fetchService.CanRunPowerShellNativeFlows && s.WindowsInstallPath is not null))
.OrderBy(s => CategoryRank(s.Category))
.ThenBy(s => s.Category)
.ThenBy(s => s.Name)
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/ScriptDetailPage.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/ScriptDetailPage.cs
index ed5531c9..1226719f 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/ScriptDetailPage.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Pages/ScriptDetailPage.cs
@@ -65,6 +65,18 @@ public override IListItem[] GetItems()
MoreCommands = BuildContextCommands(localPath),
});
}
+ else if (_script.WindowsInstallPath is { } installPath)
+ {
+ var localPath = _fetchService.GetScriptPathAsync(installPath).GetAwaiter().GetResult();
+ items.Add(new ListItem(new RunPowerShellSetupCommand(installPath, _fetchService, _script))
+ {
+ Title = "🪟 Run Windows Setup",
+ Subtitle = $"PowerShell {installPath}",
+ Tags = [new Tag("Windows")],
+ Details = BuildScriptDetails(localPath),
+ MoreCommands = BuildContextCommands(localPath),
+ });
+ }
if (_script.Includes != null && _script.Includes.Length > 0)
{
@@ -120,6 +132,43 @@ private static IContextItem[] BuildContextCommands(string? localPath)
}
}
+internal sealed partial class RunPowerShellSetupCommand : InvokableCommand, IConfirmationArgs
+{
+ private readonly string _scriptPath;
+ private readonly ScriptFetchService _fetchService;
+ private readonly ScriptEntry _script;
+
+ public RunPowerShellSetupCommand(string scriptPath, ScriptFetchService fetchService, ScriptEntry script)
+ {
+ _scriptPath = scriptPath;
+ _fetchService = fetchService;
+ _script = script;
+ }
+
+ public string Title => $"Run {_script.Name} setup?";
+ public string Description =>
+ $"This will run {_scriptPath} in a new Windows Terminal tab. "
+ + "The flow checks current state, applies only required changes, and verifies a real workload.";
+ public Microsoft.CommandPalette.Extensions.ICommand? PrimaryCommand => this;
+ public bool IsPrimaryCommandCritical => false;
+
+ public override ICommandResult Invoke()
+ {
+ if (_script.RequiresWsl && WslDetectionService.RefreshStatus() != WslStatus.Available)
+ {
+ ScriptRunnerService.RunWslInstall();
+ return CommandResult.Dismiss();
+ }
+
+ var localPath = _fetchService.GetScriptPathAsync(_scriptPath).GetAwaiter().GetResult();
+ if (localPath != null)
+ {
+ ScriptRunnerService.RunPowerShellScript(localPath);
+ }
+ return CommandResult.Dismiss();
+ }
+}
+
internal sealed partial class RunWinGetCommand : InvokableCommand, IConfirmationArgs
{
private const string FixItRelativePath = "scripts/windows/_common/enable-winget-configure.ps1";
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/QuickWingetSetupCommandsProvider.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/QuickWingetSetupCommandsProvider.cs
index 57420955..c6806f36 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/QuickWingetSetupCommandsProvider.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/QuickWingetSetupCommandsProvider.cs
@@ -70,11 +70,14 @@ public override void UpdateQuery(string query)
return;
}
- var match = _allScripts.FirstOrDefault(s =>
- s.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
- s.Description.Contains(query, StringComparison.OrdinalIgnoreCase) ||
- s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) ||
- s.Tags.Any(t => t.Contains(query, StringComparison.OrdinalIgnoreCase)));
+ var match = _allScripts
+ .Where(s => s.WindowsConfigurationPath is not null ||
+ (_fetchService.CanRunPowerShellNativeFlows && s.WindowsInstallPath is not null))
+ .FirstOrDefault(s =>
+ s.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ s.Description.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ s.Id.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ s.Tags.Any(t => t.Contains(query, StringComparison.OrdinalIgnoreCase)));
if (match != null)
{
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptFetchService.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptFetchService.cs
index 35a2b203..74cfd9cb 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptFetchService.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptFetchService.cs
@@ -23,6 +23,8 @@ public ScriptFetchService()
_config = LoadConfig();
}
+ public bool CanRunPowerShellNativeFlows => _config.Source == "local";
+
private static ExtensionConfig LoadConfig()
{
try
@@ -121,6 +123,7 @@ private static string ConvertYamlToJson(string yaml)
return null;
return File.Exists(fullPath) ? fullPath : null;
}
+
else
{
try
diff --git a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptRunnerService.cs b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptRunnerService.cs
index 678444cd..dcffed35 100644
--- a/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptRunnerService.cs
+++ b/src/future/cmdpal/QuickWingetSetup/QuickWingetSetup/Services/ScriptRunnerService.cs
@@ -7,6 +7,29 @@ namespace QuickWingetSetup.Services;
public static class ScriptRunnerService
{
+ public static void RunPowerShellScript(string scriptPath)
+ {
+ var sanitizedPath = scriptPath.Replace("\"", "");
+ var scriptDirectory = Path.GetDirectoryName(sanitizedPath) ?? string.Empty;
+ var command = $"Push-Location '{EscapeSingleQuotes(scriptDirectory)}'; try {{ & '{EscapeSingleQuotes(sanitizedPath)}' }} finally {{ Pop-Location }}";
+ var encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(command));
+ var shell = ResolveShell();
+ var psi = new ProcessStartInfo { FileName = "wt.exe", UseShellExecute = true, Verb = "runas" };
+ psi.ArgumentList.Add("new-tab");
+ psi.ArgumentList.Add("--");
+ psi.ArgumentList.Add(shell);
+ psi.ArgumentList.Add("-NoExit");
+ psi.ArgumentList.Add("-NoProfile");
+ psi.ArgumentList.Add("-ExecutionPolicy");
+ psi.ArgumentList.Add("Bypass");
+ psi.ArgumentList.Add("-EncodedCommand");
+ psi.ArgumentList.Add(encoded);
+ if (Process.Start(psi) == null)
+ {
+ throw new InvalidOperationException("Failed to launch Windows Terminal. Ensure wt.exe is available.");
+ }
+ }
+
public static void RunWinGetConfig(string scriptPath)
{
RunWinGetConfig(scriptPath, postConfigureScriptPath: null, postConfigureArgs: null);
diff --git a/src/future/cmdpal/README.md b/src/future/cmdpal/README.md
index 7b553031..52837776 100644
--- a/src/future/cmdpal/README.md
+++ b/src/future/cmdpal/README.md
@@ -3,15 +3,15 @@
A [PowerToys Command Palette](https://learn.microsoft.com/windows/powertoys/command-palette/overview)
extension that surfaces the developer flows defined in this repo's
[`manifest.yml`](../../manifest.yml). Pick a flow, hit Enter, and the extension
-launches `winget configure` (Windows) or `wsl bash` (Linux) in a new Windows
-Terminal tab — no need to remember which `.winget` file goes with which
-toolchain.
+launches `winget configure` or a PowerShell-native setup entry point (Windows),
+or `wsl bash` (Linux), in a new Windows Terminal tab.
-## Prerequisite: `winget configure` must be enabled
+## Prerequisite for DSC-backed flows: `winget configure`
-This extension launches flows exclusively through `winget configure`. If
-that subcommand is not wired up on the host, no Windows flow surfaced by
-CmdPal can succeed. See the developer guide's
+DSC-backed flows launch through `winget configure`. PowerShell-native flows,
+including the AI workloads, launch their `windows.install` script directly.
+If the configuration subcommand is not wired up on the host, DSC-backed flows
+cannot succeed. See the developer guide's
[`Prerequisites (Windows)`](../../docs/development.md#prerequisites-windows)
section for the three conditions that must hold (current App Installer, the
`configuration` feature enabled, and no blocking ADMX policy) and the
@@ -23,7 +23,8 @@ enforces this at runtime with an actionable error message.
The extension reads the same `manifest.yml` that drives CI. Each flow's UX
metadata (`name`, `description`, `category`, `tags`, `icon`, `onboardingUrl`,
-`dependsOn`) plus its `windows.configuration` / `linux.install` paths come
+`dependsOn`) plus its `windows.configuration`, `windows.install`, and
+`linux.install` paths come
straight from that file — adding a flow there makes it appear in CmdPal
automatically.
@@ -92,18 +93,16 @@ The project targets `net9.0-windows10.0.26100.0` and is AOT/trim friendly.
| Manifest field | What the extension does |
| ----------------------------------- | ----------------------------------------------------------- |
| `windows.configuration` | `winget configure ` in a new Windows Terminal tab, after a confirmation dialog |
+| `windows.install` without a configuration | Runs the PowerShell setup entry point directly in a new Windows Terminal tab |
| `onboardingUrl` | Opens in the default browser via `📖 Official Docs` action |
| `icon`, `name`, `description`, ... | Rendered on the list/detail pages |
-If `windows.configuration` is omitted in `manifest.yml`, the extension falls
-back to `/configuration.winget` — i.e. the
-WindowsDevSetupScripts convention.
-
-> **Known gap.** Two flows are PowerShell-native and have no configuration
-> file at all: Calm OS (`calm-os`) and Comfort Shell (`comfort-shell`). The
-> fallback above resolves them to a path that doesn't exist, so the extension
-> can't launch them today. Before this extension ships, teach it to run
-> `windows.install` directly when `windows.configuration` is absent.
+If `windows.configuration` is omitted, the extension uses `windows.install`.
+This supports PowerShell-native flows such as Windows Dev Config, Comfort
+Shell, and the hardware-aware AI workloads when `source` is `local`. GitHub
+source mode currently hides multi-file PowerShell-native flows because fetching
+only the entry script would omit their relative dependencies; a packaged
+repository snapshot is required before enabling them remotely.
## Confirmation dialog
diff --git a/src/manifest.yml b/src/manifest.yml
index 3ee79e98..e8641f13 100644
--- a/src/manifest.yml
+++ b/src/manifest.yml
@@ -8,8 +8,9 @@
# build + run a canonical "hello world" to prove the install worked
# (consumed by CI and the build-run-diff harness).
#
-# Adding a new flow = one entry here + one configuration.winget per OS
-# + one install.ps1 shim per OS + one hello-world file under tests//.
+# Adding a new flow = one entry here + an install.ps1 entry point per OS
+# + configuration.winget when declarative DSC is appropriate + a smoke probe
+# under tests//.
# No workflow edits required.
#
# Schema (per flow):
@@ -23,6 +24,8 @@
# (typescript, python, go, rust, ...)
# - "desktop" : a desktop framework on top of a
# language (winforms, winui, ...)
+# - "ai" : local AI runtimes and
+# hardware-specific accelerators
# - "user-experience" : OS-feel / shell flows that
# shape the desktop rather than
# install dev toolchains
@@ -60,11 +63,9 @@
# CI runs this from the `src/` directory, while CmdPal
# assumes the extension's root is the repo root.
# configuration: (optional) path to winget DSC configuration.winget the
-# extension applies via `winget configure`. Defaults to
-# "/configuration.winget" when omitted.
-# PowerShell-native flows (calm-os, comfort-shell) have no
-# DSC document and omit this key; the extension needs a
-# script-launch path before it can offer them.
+# extension applies via `winget configure`. When omitted,
+# the Command Palette extension launches `install` directly.
+# PowerShell-native flows (including AI workloads) omit it.
# build: shell command to build the hello world (run from repo
# root). "" to skip.
# run: shell command whose stdout is compared to "expected"
@@ -242,6 +243,134 @@ flows:
expected: src/tests/python/expected.txt
version: "python --version; uv --version"
+ - id: cuda
+ name: NVIDIA CUDA
+ description: CUDA Toolkit plus MSVC with a compiled GPU-kernel readiness test
+ category: ai
+ tags: [ai, cuda, nvidia, gpu, machine-learning]
+ icon: 🟩
+ onboardingUrl: https://docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/cuda/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/cuda/probe.ps1
+ expected: src/tests/cuda/expected.txt
+ version: "nvcc --version; nvidia-smi"
+
+ - id: foundry
+ name: Foundry Local
+ description: Local model runtime plus Qwen3-0.6B acquisition and inference
+ category: ai
+ tags: [ai, foundry, winml, local-models, inference]
+ icon: 🧠
+ onboardingUrl: https://learn.microsoft.com/windows/ai/foundry-local/get-started
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/foundry/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/foundry/probe.ps1
+ expected: src/tests/foundry/expected.txt
+ version: "foundry --version; foundry server status"
+
+ - id: pytorch
+ name: PyTorch
+ description: Self-contained PyTorch for CPU, NVIDIA CUDA, AMD ROCm, or Intel XPU
+ category: ai
+ tags: [ai, pytorch, python, nvidia, cuda, amd, rocm, hip, intel, xpu, triton, machine-learning]
+ icon: 🔥
+ onboardingUrl: https://pytorch.org/get-started/locally/
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/pytorch/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/pytorch/probe.ps1
+ expected: src/tests/pytorch/expected.txt
+ version: "& \"$env:LOCALAPPDATA\\DevConfig\\pytorch\\.venv\\Scripts\\python.exe\" -c \"import torch; print(torch.__version__)\""
+
+ - id: llama.cpp
+ name: llama.cpp
+ description: Hardware-selected llama.cpp CUDA, ROCm, SYCL, OpenVINO, Vulkan, OpenCL, or CPU runtime
+ category: ai
+ tags: [ai, llama, gguf, inference, local-models, nvidia, cuda, amd, rocm, intel, sycl, openvino, vulkan, qualcomm, adreno, opencl, cpu]
+ icon: 🦙
+ onboardingUrl: https://github.com/ggml-org/llama.cpp/blob/master/docs/install.md
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/llama.cpp/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/llama.cpp/probe.ps1
+ expected: src/tests/llama.cpp/expected.txt
+ version: "llama-cli --version"
+
+ - id: ollama
+ name: Ollama
+ description: Installed x64 or managed native ARM64 Ollama runtime plus qwen3:0.6b inference
+ category: ai
+ tags: [ai, ollama, local-models, inference, api]
+ icon: 🦙
+ onboardingUrl: https://docs.ollama.com/windows
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/ollama/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/ollama/probe.ps1
+ expected: src/tests/ollama/expected.txt
+ version: "ollama --version"
+
+ - id: rocm
+ name: AMD ROCm / HIP
+ description: AMD ROCm Core SDK with a compiled HIP GPU-kernel acceptance test
+ category: ai
+ tags: [ai, amd, rocm, hip, gpu, machine-learning]
+ icon: 🔴
+ onboardingUrl: https://rocm.docs.amd.com/projects/install-on-windows/en/latest/
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/rocm/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/rocm/probe.ps1
+ expected: src/tests/rocm/expected.txt
+ version: "& \"$env:LOCALAPPDATA\\DevConfig\\rocm\\.venv\\Scripts\\hipcc.exe\" --version"
+
+ - id: intel-ai
+ name: Intel AI
+ description: OpenVINO device inference with optional oneAPI/SYCL GPU development
+ category: ai
+ tags: [ai, intel, openvino, oneapi, sycl, gpu, npu]
+ icon: 🔵
+ onboardingUrl: https://docs.openvino.ai/
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/intel-ai/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/intel-ai/probe.ps1
+ expected: src/tests/intel-ai/expected.txt
+ version: "& \"$env:LOCALAPPDATA\\DevConfig\\intel-ai\\openvino\\.venv\\Scripts\\python.exe\" -c \"import openvino; print(openvino.__version__)\""
+
+ - id: local-ai
+ name: Local AI development
+ description: Scenario entry point for hardware detection, contained PyTorch/Triton, and an optional local model runtime
+ category: scenarios
+ tags: [ai, scenario, pytorch, triton, local-models, cuda, rocm, xpu]
+ icon: 🤖
+ onboardingUrl: https://learn.microsoft.com/windows/ai/
+ manual_test: true
+ os: [windows]
+ windows:
+ install: Workloads/local-ai/install.ps1
+ build: ""
+ run: pwsh -NoProfile -File src/tests/pytorch/probe.ps1
+ expected: src/tests/local-ai/expected.txt
+ version: "& \"$env:LOCALAPPDATA\\DevConfig\\pytorch\\.venv\\Scripts\\python.exe\" -c \"import torch; print(torch.__version__)\""
+
- id: sql
name: Lightweight SQL Developer
description: SQL Server Developer + sqlcmd + VS Code SQL database projects extension
diff --git a/src/tests/_harness/assertions.ps1 b/src/tests/_harness/assertions.ps1
new file mode 100644
index 00000000..0e44ea3b
--- /dev/null
+++ b/src/tests/_harness/assertions.ps1
@@ -0,0 +1,45 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$script:AssertionCount = 0
+
+function Assert-True {
+ param(
+ [Parameter(Mandatory)] [bool] $Condition,
+ [Parameter(Mandatory)] [string] $Message
+ )
+ if (-not $Condition) {
+ throw "Assertion failed: $Message"
+ }
+ $script:AssertionCount++
+}
+
+function Assert-Equal {
+ param(
+ [AllowNull()] $Actual,
+ [AllowNull()] $Expected,
+ [Parameter(Mandatory)] [string] $Message
+ )
+ if ($Actual -ne $Expected) {
+ throw "Assertion failed: $Message. Expected '$Expected'; got '$Actual'."
+ }
+ $script:AssertionCount++
+}
+
+function Assert-ThrowsLike {
+ param(
+ [Parameter(Mandatory)] [scriptblock] $ScriptBlock,
+ [Parameter(Mandatory)] [string] $Pattern,
+ [Parameter(Mandatory)] [string] $Message
+ )
+ try {
+ & $ScriptBlock
+ } catch {
+ if ($_.Exception.Message -notlike $Pattern) {
+ throw "Assertion failed: $Message. Error '$($_.Exception.Message)' did not match '$Pattern'."
+ }
+ $script:AssertionCount++
+ return
+ }
+ throw "Assertion failed: $Message. Expected an exception."
+}
diff --git a/src/tests/ai-common/all-signed.ps1 b/src/tests/ai-common/all-signed.ps1
new file mode 100644
index 00000000..8a43e4e7
--- /dev/null
+++ b/src/tests/ai-common/all-signed.ps1
@@ -0,0 +1,188 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+if ($env:OS -ne 'Windows_NT') {
+ Write-Host 'ALL_SIGNED_SKIPPED: Windows only'
+ return
+}
+
+function Get-TestPowerShellHosts {
+ $hosts = @((Get-Command 'powershell.exe' -ErrorAction Stop).Source)
+ $pwsh = Get-Command 'pwsh.exe' -ErrorAction SilentlyContinue
+ if ($pwsh) {
+ $pwshPath = $pwsh.Source
+ if ($pwshPath -like "$env:LOCALAPPDATA\Microsoft\WindowsApps\*") {
+ $package = Get-AppxPackage -Name Microsoft.PowerShell -ErrorAction SilentlyContinue |
+ Sort-Object Version -Descending |
+ Select-Object -First 1
+ if ($package) {
+ $packagedPwsh = Join-Path $package.InstallLocation 'pwsh.exe'
+ if (Test-Path -LiteralPath $packagedPwsh) {
+ $pwshPath = $packagedPwsh
+ }
+ }
+ }
+ if ((Test-Path -LiteralPath $pwshPath) -and $pwshPath -notin $hosts) {
+ $hosts += $pwshPath
+ }
+ }
+ return $hosts
+}
+
+function Invoke-AllSignedProcess {
+ param(
+ [Parameter(Mandatory)] [string] $Shell,
+ [Parameter(Mandatory)] [string] $Script,
+ [string[]] $Arguments = @(),
+ [int] $PublisherConsentCount = 0
+ )
+
+ $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
+ $startInfo.FileName = $Shell
+ $startInfo.UseShellExecute = $false
+ $startInfo.CreateNoWindow = $true
+ $startInfo.RedirectStandardInput = $true
+ $startInfo.RedirectStandardOutput = $true
+ $startInfo.RedirectStandardError = $true
+ $nativeArguments = @('-NoProfile', '-ExecutionPolicy', 'AllSigned', '-File', $Script) + $Arguments
+ $startInfo.Arguments = ($nativeArguments | ForEach-Object {
+ '"' + ([string]$_).Replace('"', '\"') + '"'
+ }) -join ' '
+
+ $process = [System.Diagnostics.Process]::new()
+ $process.StartInfo = $startInfo
+ try {
+ if (-not $process.Start()) {
+ throw "Could not start '$Shell'."
+ }
+ for ($index = 0; $index -lt $PublisherConsentCount; $index++) {
+ $process.StandardInput.WriteLine('R')
+ }
+ $process.StandardInput.Close()
+ $stdout = $process.StandardOutput.ReadToEndAsync()
+ $stderr = $process.StandardError.ReadToEndAsync()
+ if (-not $process.WaitForExit(120000)) {
+ $process.Kill()
+ throw "$Shell AllSigned validation timed out for '$Script'."
+ }
+ return [pscustomobject]@{
+ ExitCode = $process.ExitCode
+ Output = ($stdout.GetAwaiter().GetResult() + $stderr.GetAwaiter().GetResult()).Trim()
+ }
+ } finally {
+ $process.Dispose()
+ }
+}
+
+function Copy-ReleaseScriptWithCrLf {
+ param(
+ [Parameter(Mandatory)] [string] $Source,
+ [Parameter(Mandatory)] [string] $Destination
+ )
+
+ $bytes = [IO.File]::ReadAllBytes($Source)
+ $stream = [IO.MemoryStream]::new()
+ try {
+ for ($index = 0; $index -lt $bytes.Length; $index++) {
+ if ($bytes[$index] -eq 10 -and ($index -eq 0 -or $bytes[$index - 1] -ne 13)) {
+ $stream.WriteByte(13)
+ }
+ $stream.WriteByte($bytes[$index])
+ }
+ [IO.File]::WriteAllBytes($Destination, $stream.ToArray())
+ } finally {
+ $stream.Dispose()
+ }
+}
+
+$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
+$sourceRoot = Join-Path $repositoryRoot 'src\Workloads'
+$releaseRoot = Join-Path $repositoryRoot 'Workloads'
+$flows = @('cuda', 'rocm', 'intel-ai', 'foundry', 'pytorch', 'local-ai', 'llama.cpp', 'ollama')
+$shells = @(Get-TestPowerShellHosts)
+$microsoftSignerSubject = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
+
+$pipeline = Get-Content -LiteralPath (Join-Path $repositoryRoot '.pipelines\OneBranch.SignAndPackage.yml') -Raw
+if ($pipeline -notmatch 'files_to_sign:\s*src/\*\*/\*\.ps1' -or
+ $pipeline -notmatch 'src/Workloads/\*\*') {
+ throw 'The release pipeline no longer signs src/**/*.ps1 and packages src/Workloads/**.'
+}
+
+foreach ($shell in $shells) {
+ $shellName = [System.IO.Path]::GetFileNameWithoutExtension($shell)
+ $unsigned = Join-Path $sourceRoot 'cuda\install.ps1'
+ $unsignedResult = Invoke-AllSignedProcess `
+ -Shell $shell `
+ -Script $unsigned `
+ -Arguments @('-PlanOnly', '-ReportPath', (Join-Path $env:TEMP "$shellName-unsigned-ai.json"))
+ if ($unsignedResult.ExitCode -eq 0 -or
+ $unsignedResult.Output -notmatch '(?i)(not digitally signed|cannot be loaded)') {
+ throw "$shellName did not enforce AllSigned for unsigned AI source: $($unsignedResult.Output)"
+ }
+
+ $signedSource = Get-ChildItem -LiteralPath $releaseRoot -Recurse -Filter 'install.ps1' |
+ Select-Object -First 1
+ if (-not $signedSource) {
+ throw 'No release workload was available for the AllSigned host contract probe.'
+ }
+ $signedProbeRoot = Join-Path $env:TEMP "devconfig-signed-probe-$([guid]::NewGuid().ToString('N'))"
+ $signedProbe = Join-Path $signedProbeRoot 'install.ps1'
+ New-Item -ItemType Directory -Path $signedProbeRoot -Force | Out-Null
+ Copy-ReleaseScriptWithCrLf -Source $signedSource.FullName -Destination $signedProbe
+ $probeSignature = Get-AuthenticodeSignature -LiteralPath $signedProbe
+ if ($probeSignature.Status -ne 'Valid' -or -not $probeSignature.SignerCertificate -or
+ $probeSignature.SignerCertificate.Subject -ne $microsoftSignerSubject) {
+ throw "CRLF release probe failed Microsoft signature validation: $($probeSignature.Status)."
+ }
+ try {
+ $probeResult = Invoke-AllSignedProcess `
+ -Shell $shell `
+ -Script $signedProbe `
+ -Arguments @('-?') `
+ -PublisherConsentCount 4
+ if ($probeResult.ExitCode -ne 0) {
+ throw "$shellName could not load a valid Microsoft-signed release workload under AllSigned: $($probeResult.Output)"
+ }
+ } finally {
+ Remove-Item -LiteralPath $signedProbeRoot -Recurse -Force -ErrorAction SilentlyContinue
+ }
+}
+
+$missingReleaseFlows = @($flows | Where-Object {
+ -not (Test-Path -LiteralPath (Join-Path $releaseRoot "$_\install.ps1"))
+})
+if ($missingReleaseFlows.Count -gt 0) {
+ Write-Host "ALL_SIGNED_RELEASE_PENDING: sign cycle must publish $($missingReleaseFlows -join ', ')"
+} else {
+ $signedScope = @(
+ Get-ChildItem -LiteralPath (Join-Path $releaseRoot '_common') -File -Filter '*.ps1'
+ foreach ($flow in $flows) {
+ Get-ChildItem -LiteralPath (Join-Path $releaseRoot $flow) -File -Filter '*.ps1'
+ }
+ )
+ foreach ($script in $signedScope) {
+ $signature = Get-AuthenticodeSignature -LiteralPath $script.FullName
+ if ($signature.Status -ne 'Valid' -or
+ $signature.SignerCertificate.Subject -ne $microsoftSignerSubject) {
+ throw "Signed AI release file failed Microsoft signature validation: '$($script.FullName)' [$($signature.Status)]."
+ }
+ }
+
+ foreach ($shell in $shells) {
+ $shellName = [System.IO.Path]::GetFileNameWithoutExtension($shell)
+ foreach ($flow in $flows) {
+ $reportPath = Join-Path $env:TEMP "$shellName-$flow-all-signed.json"
+ $result = Invoke-AllSignedProcess `
+ -Shell $shell `
+ -Script (Join-Path $releaseRoot "$flow\install.ps1") `
+ -Arguments @('-PlanOnly', '-ReportPath', $reportPath) `
+ -PublisherConsentCount 64
+ if ($result.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $reportPath)) {
+ throw "$shellName AllSigned launch failed for signed $flow release: $($result.Output)"
+ }
+ }
+ }
+ Write-Host "ALL_SIGNED_RELEASE_OK: $($flows.Count) AI flows in $($shells.Count) PowerShell host(s)"
+}
+
+Write-Host "ALL_SIGNED_CONTRACT_OK: unsigned source rejected and signed release accepted in $($shells.Count) PowerShell host(s)"
diff --git a/src/tests/ai-common/unit.ps1 b/src/tests/ai-common/unit.ps1
new file mode 100644
index 00000000..4bec6ac4
--- /dev/null
+++ b/src/tests/ai-common/unit.ps1
@@ -0,0 +1,288 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-report.ps1')
+
+$catalog = Get-AiCatalog
+$required = @(
+ 'Component', 'Architectures', 'Maturity', 'SourceType', 'VersionPolicy',
+ 'Integrity', 'CachePath', 'InstallPath', 'NormalChannelLimitation',
+ 'ExpectedStableSource', 'MigrationTrigger', 'CleanupUpgrade'
+)
+foreach ($entry in $catalog.Components.GetEnumerator()) {
+ foreach ($field in $required) {
+ Assert-True ($entry.Value.ContainsKey($field)) "$($entry.Key) should define promotion field $field"
+ }
+}
+
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\content-hashes.ps1')
+$workloadsRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\Workloads')).Path
+Assert-DevConfigWorkloadContent -WorkloadsRoot $workloadsRoot
+$trackedContent = @(git -C (Join-Path $PSScriptRoot '..\..\..') ls-files 'src/Workloads/**' |
+ Where-Object { [IO.Path]::GetExtension($_) -ne '.ps1' } |
+ ForEach-Object { $_.Substring('src/Workloads/'.Length).Replace('/', '\') })
+Assert-Equal @($Script:DevConfigWorkloadContentHashes.Keys | Sort-Object).Count $trackedContent.Count 'Signed content manifest should cover every tracked non-PowerShell Workloads file'
+foreach ($path in $trackedContent) {
+ Assert-True $Script:DevConfigWorkloadContentHashes.ContainsKey($path) "Signed content manifest should declare $path"
+}
+$blobHashScript = @'
+import hashlib
+import json
+import subprocess
+
+paths = subprocess.check_output(
+ ["git", "ls-files", "src/Workloads/**"], text=True
+).splitlines()
+print(json.dumps({
+ path[len("src/Workloads/"):].replace("/", "\\"):
+ hashlib.sha256(subprocess.check_output(["git", "show", f"HEAD:{path}"])).hexdigest()
+ for path in paths
+ if not path.lower().endswith(".ps1")
+}, sort_keys=True))
+'@
+$blobHashScriptPath = Join-Path $env:TEMP "devconfig-blob-hashes-$([guid]::NewGuid().ToString('N')).py"
+try {
+ [IO.File]::WriteAllText($blobHashScriptPath, $blobHashScript, [Text.UTF8Encoding]::new($false))
+ $blobHashResult = Invoke-DevConfigNativeCommand -FilePath 'python' -Arguments @($blobHashScriptPath)
+ if ($blobHashResult.ExitCode -ne 0) {
+ throw "Could not calculate canonical Git blob hashes: $($blobHashResult.Output)"
+ }
+ $blobHashes = $blobHashResult.Output.Trim() | ConvertFrom-Json
+} finally {
+ Remove-Item -LiteralPath $blobHashScriptPath -Force -ErrorAction SilentlyContinue
+}
+foreach ($path in $trackedContent) {
+ Assert-Equal $Script:DevConfigWorkloadContentHashes[$path] $blobHashes.$path "Signed content hash should match canonical Git blob bytes for $path"
+}
+$tamperedRoot = Join-Path $env:TEMP "devconfig-content-tamper-$([guid]::NewGuid().ToString('N'))"
+try {
+ Copy-Item -LiteralPath $workloadsRoot -Destination $tamperedRoot -Recurse
+ New-Item -ItemType Directory -Path (Join-Path $tamperedRoot 'pytorch\__pycache__') -Force | Out-Null
+ Set-Content -LiteralPath (Join-Path $tamperedRoot 'pytorch\__pycache__\torch.pyc') -Value 'untrusted bytecode'
+ Assert-ThrowsLike {
+ Assert-DevConfigWorkloadContent -WorkloadsRoot $tamperedRoot
+ } '*not declared by signed content manifest*' 'Signed content verification should reject unexpected Python bytecode'
+} finally {
+ Remove-Item -LiteralPath $tamperedRoot -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+$capabilities = @(Get-AiCapabilityMatrix)
+Assert-True ($capabilities.Count -ge 30) 'Capability matrix should enumerate every supported and explicitly unavailable Windows AI cell'
+Assert-Equal @($capabilities.Id | Sort-Object -Unique).Count $capabilities.Count 'Capability ids should be unique'
+$requiredCapabilityIds = @(
+ 'cuda-nvidia-x64', 'cuda-nvidia-arm64', 'rocm-amd-x64',
+ 'intel-openvino-cpu-x64', 'intel-openvino-gpu-x64', 'intel-openvino-npu-x64',
+ 'intel-sycl-gpu-x64', 'intel-full-gpu-x64',
+ 'pytorch-cpu-x64', 'pytorch-cpu-arm64', 'pytorch-cuda-x64', 'pytorch-cuda-arm64',
+ 'pytorch-rocm-x64', 'pytorch-xpu-x64',
+ 'triton-cuda-x64', 'triton-cuda-arm64', 'triton-xpu-x64',
+ 'llama-cpu-x64', 'llama-cpu-arm64', 'llama-cuda-x64', 'llama-cuda-arm64',
+ 'llama-rocm-x64', 'llama-sycl-x64', 'llama-openvino-x64', 'llama-vulkan-x64',
+ 'llama-opencl-adreno-arm64',
+ 'foundry-source-managed-x64', 'foundry-source-managed-arm64',
+ 'ollama-source-managed-x64', 'ollama-source-managed-arm64',
+ 'rocm-arm64-unavailable', 'pytorch-rocm-arm64-unavailable',
+ 'pytorch-xpu-arm64-unavailable', 'pytorch-qualcomm-arm64-unavailable',
+ 'triton-amd-windows-unavailable', 'generic-arm-gpu-toolkit-unavailable',
+ 'amd-ryzen-ai-npu-unavailable', 'intel-ai-arm64-unavailable',
+ 'other-windows-gpu-unavailable'
+)
+foreach ($id in $requiredCapabilityIds) {
+ Assert-True ($id -in $capabilities.Id) "Capability matrix should include required cell $id"
+}
+$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
+$implementedStatuses = @('implemented-supported', 'source-managed')
+foreach ($cell in $capabilities) {
+ Assert-True ($cell.Status -in @('implemented-supported', 'source-managed', 'upstream-unavailable')) "$($cell.Id) should use a defined capability status"
+ if ($cell.Status -in $implementedStatuses) {
+ foreach ($field in @('Workload', 'Architecture', 'Vendor', 'DeviceFamily', 'Backend', 'Maturity', 'Acquisition', 'Prerequisites', 'Resolver', 'ResolverArguments', 'Expected', 'ProbePath', 'ReportEvidence', 'PartnerCommand')) {
+ Assert-True $cell.ContainsKey($field) "$($cell.Id) should define supported-cell field $field"
+ }
+ Assert-True ([bool](Get-Command -Name $cell.Resolver -CommandType Function -ErrorAction SilentlyContinue)) "$($cell.Id) resolver should exist"
+ foreach ($identity in @($cell.Acquisition)) {
+ if ($identity -like 'component:*') {
+ $componentKey = $identity.Substring('component:'.Length)
+ Assert-True $catalog.Components.ContainsKey($componentKey) "$($cell.Id) should reference catalog component $componentKey"
+ } else {
+ Assert-True ($identity -like 'winget:*') "$($cell.Id) acquisition '$identity' should use a known identity prefix"
+ }
+ }
+ Assert-True (Test-Path -LiteralPath (Join-Path $repositoryRoot $cell.ProbePath)) "$($cell.Id) verification probe should exist"
+ Assert-True ([bool]$cell.ReportEvidence) "$($cell.Id) should define report evidence"
+ Assert-True ($cell.PartnerCommand -match '-ReportPath') "$($cell.Id) should provide a report-producing partner command"
+ $resolvedCell = Resolve-AiCapabilityCell -Id $cell.Id
+ Assert-True ($null -ne $resolvedCell) "$($cell.Id) resolver fixture should return a plan"
+ } else {
+ Assert-True $cell.ContainsKey('Blocker') "$($cell.Id) should explain the authoritative upstream boundary"
+ try {
+ Resolve-AiCapabilityCell -Id $cell.Id
+ throw "Capability '$($cell.Id)' unexpectedly resolved."
+ } catch {
+ Assert-Equal $_.Exception.Message $cell.Blocker "$($cell.Id) should return its actionable blocker"
+ }
+ }
+}
+$capabilityReportPath = Join-Path $env:TEMP "devconfig-capabilities-$([guid]::NewGuid().ToString('N')).json"
+try {
+ & (Join-Path $repositoryRoot 'src\tools\get-ai-capabilities.ps1') -OutputPath $capabilityReportPath
+ $capabilityReport = Get-Content -LiteralPath $capabilityReportPath -Raw | ConvertFrom-Json
+ Assert-Equal $capabilityReport.capabilities.Count $capabilities.Count 'Capability report tool should emit every catalog cell'
+ Assert-True (@($capabilityReport.capabilities | Where-Object status -eq 'source-managed').Count -gt 0) 'Capability report should preserve source-managed status'
+} finally {
+ Remove-Item -LiteralPath $capabilityReportPath -Force -ErrorAction SilentlyContinue
+}
+
+$wingetArgs = Get-DevConfigWingetInstallArguments -Id 'Microsoft.FoundryLocal'
+Assert-Equal ($wingetArgs -join ' ') 'install --id Microsoft.FoundryLocal --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity' 'Shared WinGet install command should be exact and noninteractive'
+$upgradeArgs = Get-DevConfigWingetUpgradeArguments -Id 'Microsoft.VisualStudio.2022.BuildTools'
+Assert-Equal ($upgradeArgs -join ' ') 'upgrade --id Microsoft.VisualStudio.2022.BuildTools --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity' 'Shared WinGet upgrade command should be exact and noninteractive'
+Assert-Equal (Get-AiWingetPackageAction -State Current) 'skip' 'Current packages should skip acquisition'
+Assert-Equal (Get-AiWingetPackageAction -State UpgradeAvailable) 'upgrade' 'Outdated packages should upgrade'
+Assert-Equal (Get-AiWingetPackageAction -State Absent) 'install' 'Absent packages should install'
+Assert-True ([bool](Get-Command Ensure-DevConfigWingetPackage -ErrorAction SilentlyContinue)) 'Shared production package ensure function should be exported at script scope'
+
+$currentShape = [pscustomobject]@{
+ Id = 'Current.Package'
+ Name = 'Current package'
+ InstalledVersion = '1.0.0'
+ IsUpdateAvailable = $false
+}
+$currentEvidence = ConvertTo-AiWingetPackageEvidence -Package $currentShape -RequestedId 'Current.Package'
+Assert-Equal $currentEvidence.installedVersion '1.0.0' 'Evidence should support current module object shape without AvailableVersion'
+Assert-Equal $currentEvidence.availableVersion '' 'Missing optional AvailableVersion should not fail evidence collection'
+
+$olderShape = [pscustomobject]@{
+ PackageIdentifier = 'Older.Package'
+ PackageName = 'Older package'
+ Version = '2.0.0'
+ LatestVersion = '2.1.0'
+ UpdateAvailable = $true
+}
+$olderEvidence = ConvertTo-AiWingetPackageEvidence -Package $olderShape -RequestedId 'fallback'
+Assert-Equal $olderEvidence.id 'Older.Package' 'Evidence should support alternate identifier names'
+Assert-Equal $olderEvidence.availableVersion '2.1.0' 'Evidence should support alternate latest-version names'
+Assert-True $olderEvidence.updateAvailable 'Evidence should support alternate update flags'
+
+$minimalEvidence = ConvertTo-AiWingetPackageEvidence -Package ([pscustomobject]@{}) -RequestedId 'Minimal.Package'
+Assert-Equal $minimalEvidence.id 'Minimal.Package' 'Minimal package objects should retain the requested id'
+Assert-Equal $minimalEvidence.installedVersion '' 'Minimal package objects should not fail under StrictMode'
+
+# Keep fallback tests fast and deterministic by invoking each retry body once.
+function Invoke-DevConfigRetry {
+ param([scriptblock] $ScriptBlock, [string] $Name, [int] $MaxAttempts, [int] $InitialDelaySeconds)
+ & $ScriptBlock
+}
+$Script:DevConfigWinGetMode = 'Module'
+$script:cliArguments = $null
+function Test-DevConfigWingetCliUsable { return $true }
+function Install-WinGetPackage { throw 'module install error' }
+function Update-WinGetPackage { throw 'module upgrade error' }
+function Invoke-DevConfigWingetCli {
+ param([string[]] $Arguments)
+ $script:cliArguments = $Arguments
+ return [pscustomobject]@{ ExitCode = 0; Output = '' }
+}
+Install-DevConfigWingetPackage -Id 'Fallback.Install'
+Assert-Equal $script:cliArguments[0] 'install' 'Module install error should fall back to CLI install'
+$Script:DevConfigWinGetMode = 'Module'
+$script:updateModuleCalls = 0
+function Update-WinGetPackage { $script:updateModuleCalls++; throw 'module upgrade error' }
+Update-DevConfigWingetPackage -Id 'Fallback.Upgrade'
+Assert-Equal $script:cliArguments[0] 'upgrade' 'Module upgrade error should fall back to CLI upgrade'
+Assert-Equal $script:updateModuleCalls 1 'Upgrade fallback should attempt the module before CLI'
+
+$Script:DevConfigWinGetMode = 'Module'
+function Invoke-DevConfigWingetCli {
+ param([string[]] $Arguments)
+ return [pscustomobject]@{ ExitCode = 9; Output = 'real failure' }
+}
+Assert-ThrowsLike {
+ Update-DevConfigWingetPackage -Id 'Fallback.Failure'
+} '*CLI exit: 9*' 'A real nonzero module and CLI failure should remain fatal'
+
+# Exercise Ensure-AiWingetPackage's state machine without touching machine state.
+$script:packageState = 'Current'
+$script:installCount = 0
+$script:upgradeCount = 0
+function Initialize-DevConfigWinGet {}
+function Get-DevConfigWingetPackageState { param($Id) [pscustomobject]@{ State = $script:packageState; Package = $null } }
+function Install-DevConfigWingetPackage { param($Id) $script:installCount++ }
+function Update-DevConfigWingetPackage { param($Id) $script:upgradeCount++ }
+function Wait-DevConfigWingetPackageSettled { param($Id) }
+function Ensure-DevConfigWingetPackage {
+ param($Id)
+ $operation = Get-AiWingetPackageAction -State $script:packageState
+ if ($operation -eq 'install') { $script:installCount++; return 'installed' }
+ if ($operation -eq 'upgrade') { $script:upgradeCount++; return 'upgraded' }
+ return 'already-current'
+}
+function Update-DevConfigSessionPath {}
+function Test-DevConfigWingetPackageInstalled { param($Id) return $true }
+function Get-AiWingetPackageEvidence { param($Id) return @{ id = $Id } }
+
+$currentResult = Ensure-AiWingetPackage -Id 'State.Current'
+Assert-Equal $currentResult.Action 'already-current' 'Installed current package should skip'
+Assert-Equal $script:installCount 0 'Current package should not install'
+Assert-Equal $script:upgradeCount 0 'Current package should not upgrade'
+
+$script:packageState = 'UpgradeAvailable'
+$upgradeResult = Ensure-AiWingetPackage -Id 'State.Upgrade'
+Assert-Equal $upgradeResult.Action 'upgraded' 'Installed outdated package should upgrade'
+Assert-Equal $script:upgradeCount 1 'Upgrade state should invoke upgrade exactly once'
+
+$script:packageState = 'Absent'
+$installResult = Ensure-AiWingetPackage -Id 'State.Absent'
+Assert-Equal $installResult.Action 'installed' 'Absent package should install'
+Assert-Equal $script:installCount 1 'Absent state should invoke install exactly once'
+
+$directSetup = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1') -Raw
+Assert-True ($directSetup.Contains('''--installPath'', "`"$installPath`""')) 'Build Tools install path should remain one quoted Start-Process argument'
+Assert-True ($directSetup -match 'Get-AiWingetPackageEvidence') 'Package evidence should respect the selected WinGet frontend'
+Assert-True ($directSetup -match 'Enable-AiUtf8Console') 'Standalone AI entry points should normalize UTF-8 console capture'
+
+$freshProcessScript = Join-Path $env:TEMP "devconfig-lastexitcode-$([guid]::NewGuid().ToString('N')).ps1"
+try {
+ @(
+ 'Set-StrictMode -Version Latest',
+ ". '$((Resolve-Path (Join-Path $PSScriptRoot '..\..\windows-dev-config\steps\_environment.ps1')).Path)'",
+ '$result = Invoke-DevConfigNativeCommand -FilePath $env:ComSpec -Arguments @(''/d'',''/c'',''exit 0'')',
+ 'if ($result.ExitCode -ne 0) { throw "unexpected exit $($result.ExitCode)" }',
+ 'try { Invoke-DevConfigNativeCommand -FilePath ''__missing_devconfig_command__.exe'' } catch { Write-Output MISSING_NATIVE_FAILED }',
+ 'Write-Output FRESH_LASTEXITCODE_OK'
+ ) | Set-Content -LiteralPath $freshProcessScript -Encoding utf8
+ $freshResult = & pwsh -NoProfile -File $freshProcessScript 2>&1 | Out-String
+ Assert-True ($freshResult -match 'FRESH_LASTEXITCODE_OK') 'Fresh StrictMode process should execute native command without preexisting LASTEXITCODE'
+ Assert-True ($freshResult -match 'MISSING_NATIVE_FAILED') 'Fresh StrictMode process should treat native launch failure as failure'
+} finally {
+ Remove-Item -LiteralPath $freshProcessScript -Force -ErrorAction SilentlyContinue
+}
+
+$report = New-AiWorkloadReport -Id 'unit' -Request @{ PlanOnly = $true }
+Add-AiReportAcquisition -Report $report -Entry @{ component = 'test'; sourceType = 'unit'; action = 'planned' }
+Set-AiAcquisitionAction -Report $report -Index 0 -Action 'already-current'
+Add-AiReportPhase -Report $report -Name 'plan' -Status 'planned' -Evidence @{ backend = 'CPU' }
+Assert-Equal $report.schemaVersion 1 'Report schema version should be stable'
+Assert-Equal $report.acquisitions.Count 1 'Report should collect acquisitions'
+Assert-Equal $report.acquisitions[0].action 'already-current' 'Report should finalize acquisition actions'
+Assert-Equal $report.phases.Count 1 'Report should collect phases'
+Assert-True $report.result.planOnly 'Report should preserve plan mode'
+
+$schemaPath = Join-Path $PSScriptRoot '..\..\docs\ai-workload-report.schema.json'
+Assert-True (Test-Path -LiteralPath $schemaPath) 'Checked-in report schema should exist'
+
+$failurePath = Join-Path $env:TEMP "devconfig-report-failure-$([guid]::NewGuid().ToString('N')).json"
+try {
+ $failureReport = New-AiWorkloadReport -Id 'failure-unit' -Request @{}
+ try { throw 'synthetic hardware failure' } catch {
+ Write-AiFailureReport -Report $failureReport -Path $failurePath -ErrorRecord $_
+ }
+ $savedFailure = Get-Content -LiteralPath $failurePath -Raw | ConvertFrom-Json
+ Assert-True (-not $savedFailure.result.ready) 'Failure report should not claim readiness'
+ Assert-True ($savedFailure.result.blockers[0] -like '*synthetic hardware failure*') 'Failure report should retain the actionable exception'
+} finally {
+ Remove-Item -LiteralPath $failurePath -Force -ErrorAction SilentlyContinue
+}
+
+Write-Host "UNIT_OK: ai-common ($script:AssertionCount assertions)"
diff --git a/src/tests/cuda/expected.txt b/src/tests/cuda/expected.txt
new file mode 100644
index 00000000..00d1727b
--- /dev/null
+++ b/src/tests/cuda/expected.txt
@@ -0,0 +1 @@
+CUDA ready
diff --git a/src/tests/cuda/probe.ps1 b/src/tests/cuda/probe.ps1
new file mode 100644
index 00000000..f37764f9
--- /dev/null
+++ b/src/tests/cuda/probe.ps1
@@ -0,0 +1,38 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+$architecture = Get-DevConfigArchitecture
+$plan = Resolve-CudaInstallPlan -Architecture $architecture -WindowsBuild (Get-WindowsBuildNumber)
+$reportPath = Get-AiDefaultReportPath -Id 'cuda'
+$deviceIndex = 0
+if (Test-Path -LiteralPath $reportPath) {
+ $request = (Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json).request
+ if ($request.PSObject.Properties['DeviceIndex']) { $deviceIndex = [int]$request.DeviceIndex }
+}
+$nvcc = Get-CudaNvccPath -ToolkitVersion $plan.ToolkitVersion
+$vsDevCmd = Get-VsDevCmdPath -Architecture $architecture
+$temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-cuda-probe-$([guid]::NewGuid().ToString('N'))"
+New-Item -ItemType Directory -Path $temporary -Force | Out-Null
+try {
+ $executable = Join-Path $temporary 'cuda-smoke.exe'
+ $compile = Get-CudaKernelCompileCommand `
+ -Architecture $architecture `
+ -VsDevCmd $vsDevCmd `
+ -Nvcc $nvcc `
+ -Source (Join-Path $PSScriptRoot '..\..\Workloads\cuda\smoke.cu') `
+ -Output $executable
+ & $env:ComSpec /d /s /c $compile *> $null
+ if ($LASTEXITCODE -ne 0) {
+ throw "CUDA probe compilation failed with exit code $LASTEXITCODE."
+ }
+ $result = (& $executable $deviceIndex 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $result -notmatch '^CUDA_KERNEL_READY') {
+ throw "CUDA probe kernel failed with exit code $LASTEXITCODE and output '$result'."
+ }
+} finally {
+ Remove-Item -LiteralPath $temporary -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+Write-Output 'CUDA ready'
diff --git a/src/tests/cuda/unit.ps1 b/src/tests/cuda/unit.ps1
new file mode 100644
index 00000000..a825a183
--- /dev/null
+++ b/src/tests/cuda/unit.ps1
@@ -0,0 +1,95 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+$ready = Get-CudaReadiness -ToolkitAvailable $true -NvidiaGpuPresent $true -DriverAvailable $true
+Assert-Equal $ready.Status 'Ready' 'Toolkit and driver/GPU should be ready'
+Assert-True $ready.GpuReady 'GPU readiness should be true'
+
+$toolkitOnly = Get-CudaReadiness -ToolkitAvailable $true -NvidiaGpuPresent $false -DriverAvailable $false
+Assert-Equal $toolkitOnly.Status 'ToolkitOnlyNoGpu' 'Toolkit-only state should be distinct'
+Assert-True (-not $toolkitOnly.GpuReady) 'Toolkit-only should not report GPU readiness'
+$toolkitOnlyRepeat = Get-CudaReadiness -ToolkitAvailable $true -NvidiaGpuPresent $false -DriverAvailable $false
+Assert-Equal ($toolkitOnlyRepeat | ConvertTo-Json -Compress) ($toolkitOnly | ConvertTo-Json -Compress) 'CUDA readiness should be idempotent'
+
+$x64Plan = Resolve-CudaInstallPlan -Architecture X64
+Assert-Equal $x64Plan.Method 'WinGet' 'CUDA x64 should use WinGet'
+Assert-Equal $x64Plan.ToolkitVersion $null 'CUDA x64 should discover the WinGet-installed stable toolkit version'
+
+$armPlan = Resolve-CudaInstallPlan -Architecture Arm64 -WindowsBuild 28120
+Assert-Equal $armPlan.Method 'NvidiaInstaller' 'CUDA ARM64 should use NVIDIA developer-preview installer'
+Assert-Equal $armPlan.ToolkitVersion '13.4' 'CUDA ARM64 should select toolkit 13.4'
+Assert-Equal $armPlan.InstallerSha256 'a1f68c81160b16d519c4087788b9c07de41306c3f1b872471ceee0996621374d' 'CUDA ARM64 installer should be checksum pinned'
+Assert-True ($armPlan.InstallerUrl -like 'https://packages.nvidia.com/prerelease/*windows_arm64.exe') 'CUDA ARM64 installer should use NVIDIA prerelease origin'
+$cudaCandidate = (Get-AiCatalogData).Components.CudaArm64.PromotionCandidate
+Assert-Equal $cudaCandidate.Version '13.4.1' 'CUDA ARM64 should track the official stable direct candidate'
+Assert-Equal $cudaCandidate.Sha256 '39af79e5e136c4e0de03bba816bda60fd7b70aad033e37ecaacf9f2e2c982442' 'CUDA 13.4.1 candidate should retain the verified installer hash'
+Assert-Equal $cudaCandidate.Size 3711598920 'CUDA 13.4.1 candidate should retain the verified installer size'
+Assert-True ($cudaCandidate.TrackingStatus -match 'awaiting N1X') 'CUDA stable candidate should remain gated on real workload qualification'
+
+$compile = Get-CudaKernelCompileCommand `
+ -Architecture Arm64 `
+ -VsDevCmd 'C:\VS\VsDevCmd.bat' `
+ -Nvcc 'C:\CUDA\nvcc.exe' `
+ -Source 'C:\src\smoke.cu' `
+ -Output 'C:\out\smoke.exe'
+Assert-True ($compile -like '*-arch=arm64 -host_arch=arm64*') 'CUDA ARM64 smoke should select native MSVC environment'
+Assert-True ($compile -like '*-arch=native*smoke.cu*') 'CUDA smoke should compile for the detected GPU'
+Assert-True ($compile -like '*Microsoft Visual Studio\Installer;%PATH%*') 'CUDA compiler environment should put vswhere.exe on PATH before VsDevCmd runs'
+
+$installScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\cuda\install.ps1') -Raw
+Assert-True ($installScript -match '\[switch\]\s*\$SkipWorkloadSmoke') 'CUDA should expose workload-smoke opt-out'
+Assert-True ($installScript -match '\[int\]\s*\$DeviceIndex') 'CUDA should expose same-vendor adapter selection'
+Assert-True ($installScript -match 'Get-NvidiaDriverInfo -DeviceIndex \$DeviceIndex') 'CUDA should qualify the requested NVIDIA adapter'
+Assert-True ($installScript -match 'current stable CUDA 13 x64 flow requires driver 580\+') 'CUDA x64 should fail before acquisition on unsupported CUDA 13 hardware'
+Assert-True ($installScript -match 'CUDA 13\.4 ARM64 Developer Preview requires driver 616\+') 'CUDA ARM64 should enforce the qualified N1X driver/device tuple'
+$smokeSource = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\cuda\smoke.cu') -Raw
+Assert-True ($smokeSource -match 'cudaSetDevice\(device_index\)') 'CUDA kernel should execute on the requested NVIDIA adapter'
+Assert-True ($smokeSource -match 'cudaGetDeviceProperties') 'CUDA kernel evidence should report the actual device'
+$probeScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'probe.ps1') -Raw
+Assert-True ($probeScript -match 'DeviceIndex') 'CUDA verification probe should reuse the selected adapter'
+$directSetup = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1') -Raw
+Assert-True ($directSetup -match 'Microsoft\.VisualStudio\.Component\.VC\.Tools\.ARM64') 'Direct setup should install native compiler tools'
+Assert-True ($directSetup -match 'Invoke-DevConfigProcess') 'Direct setup should use PR #93 bounded process execution'
+Assert-True ($directSetup -match 'Ensure-AiCudaToolkit') 'CUDA acquisition should be shared with PyTorch'
+Assert-True ($installScript -notmatch 'apply-configuration') 'CUDA should not use winget configure'
+Assert-True ($installScript -match 'Ready \$kernelReady') 'CUDA report should require a successfully executed kernel for readiness'
+
+$cleanupPath = Join-Path $env:TEMP "devconfig-cleanup-test-$([guid]::NewGuid().ToString('N')).tmp"
+Set-Content -LiteralPath $cleanupPath -Value 'test'
+$script:cleanupAttempts = 0
+$removed = Remove-TemporaryFileWithRetry `
+ -Path $cleanupPath `
+ -MaxAttempts 3 `
+ -DelayMilliseconds 0 `
+ -RemoveAction {
+ param($Target)
+ $script:cleanupAttempts++
+ if ($script:cleanupAttempts -lt 3) {
+ throw 'installer still holds the file'
+ }
+ Remove-Item -LiteralPath $Target -Force
+ }
+Assert-True $removed 'Temporary cleanup should succeed after a delayed installer release'
+Assert-Equal $script:cleanupAttempts 3 'Temporary cleanup should retry until release'
+Assert-True (-not (Test-Path -LiteralPath $cleanupPath)) 'Temporary cleanup should remove the released file'
+
+$lockedPath = Join-Path $env:TEMP "devconfig-cleanup-locked-$([guid]::NewGuid().ToString('N')).tmp"
+Set-Content -LiteralPath $lockedPath -Value 'test'
+$warnings = [System.Collections.Generic.List[string]]::new()
+$removed = Remove-TemporaryFileWithRetry `
+ -Path $lockedPath `
+ -MaxAttempts 2 `
+ -DelayMilliseconds 0 `
+ -RemoveAction { param($Target) throw 'access denied while installer child exits' } `
+ -WarningVariable cleanupWarnings
+foreach ($warning in $cleanupWarnings) {
+ [void]$warnings.Add($warning.Message)
+}
+Assert-True (-not $removed) 'Persistent cleanup failure should return false instead of throwing'
+Assert-True (($warnings -join ' ') -like '*Could not remove temporary file*access denied*') 'Persistent cleanup failure should emit a useful warning'
+Remove-Item -LiteralPath $lockedPath -Force
+
+Write-Host "UNIT_OK: cuda ($script:AssertionCount assertions)"
diff --git a/src/tests/foundry/expected.txt b/src/tests/foundry/expected.txt
new file mode 100644
index 00000000..11ae80b6
--- /dev/null
+++ b/src/tests/foundry/expected.txt
@@ -0,0 +1 @@
+Foundry Local ready
diff --git a/src/tests/foundry/probe.ps1 b/src/tests/foundry/probe.ps1
new file mode 100644
index 00000000..9d1ba5a6
--- /dev/null
+++ b/src/tests/foundry/probe.ps1
@@ -0,0 +1,21 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+if (-not (Get-Command foundry -ErrorAction SilentlyContinue)) {
+ throw 'foundry was not found on PATH.'
+}
+& foundry --version *> $null
+if ($LASTEXITCODE -ne 0) {
+ throw "foundry --version failed with exit code $LASTEXITCODE."
+}
+$plan = Get-FoundryModelSmokePlan
+$commands = Get-FoundryModelSmokeCommands -Model $plan.Model -Marker $plan.Marker
+$completeArguments = @($commands.Complete)
+$output = (& foundry @completeArguments 2>&1 | Out-String)
+if ($LASTEXITCODE -ne 0 -or $output -notmatch [regex]::Escape($plan.Marker)) {
+ throw "Foundry cached-model inference failed with exit code $LASTEXITCODE."
+}
+
+Write-Output 'Foundry Local ready'
diff --git a/src/tests/foundry/unit.ps1 b/src/tests/foundry/unit.ps1
new file mode 100644
index 00000000..1f477fbe
--- /dev/null
+++ b/src/tests/foundry/unit.ps1
@@ -0,0 +1,92 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+foreach ($architecture in @('X64', 'Arm64')) {
+ $plan = Resolve-FoundryInstallPlan -Architecture $architecture -WindowsBuild 26100
+ Assert-Equal $plan.Architecture $architecture "Foundry should preserve $architecture"
+ Assert-Equal $plan.PackageId 'Microsoft.FoundryLocal' 'Foundry should use WinGet on both architectures'
+ Assert-True (-not $plan.RequiresCuda) 'Foundry must not depend on CUDA'
+}
+
+Assert-ThrowsLike {
+ Resolve-FoundryInstallPlan -Architecture 'X64' -WindowsBuild 22631
+} '*requires Windows 11 24H2*' 'Foundry should reject older Windows builds'
+
+$first = Resolve-FoundryInstallPlan -Architecture Arm64 -WindowsBuild 26100
+$repeat = Resolve-FoundryInstallPlan -Architecture Arm64 -WindowsBuild 26100
+Assert-Equal ($repeat | ConvertTo-Json -Compress) ($first | ConvertTo-Json -Compress) 'Foundry plan should be idempotent'
+$foundryCandidate = (Get-AiCatalogData).Components.FoundryLocal.PromotionCandidate
+Assert-Equal $foundryCandidate.Version '2.0.1' 'Foundry should track the latest official non-prerelease candidate'
+Assert-Equal $foundryCandidate.X64Sha256 '0551db07d5cba6a523e4c1832f0d38e023301ab67b946378239f8cee156ba5a4' 'Foundry v2 x64 candidate should retain its release hash'
+Assert-Equal $foundryCandidate.Arm64Sha256 '2fa8510281cfaa554e21ffae8de41366a08051bce92fd592b919bc4413b57b09' 'Foundry v2 ARM64 candidate should retain its release hash'
+Assert-True ($foundryCandidate.Maturity -match 'candidate') 'Foundry v2 should remain a tracked candidate until the migrated workload is qualified'
+
+$model = Get-FoundryModelSmokePlan
+Assert-Equal $model.Model 'qwen3-0.6b' 'Foundry should use the tested small catalog model'
+Assert-Equal $model.ApproximateDownloadMb 593 'Foundry should document expected download size'
+$commands = Get-FoundryModelSmokeCommands -Model $model.Model -Marker $model.Marker
+Assert-Equal ($commands.Download -join ' ') 'model download qwen3-0.6b' 'Foundry download command should be deterministic'
+Assert-True (($commands.Complete -join ' ') -like '*DEVCONFIG_FOUNDRY_READY*') 'Foundry completion should require a marker'
+$installScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\foundry\install.ps1') -Raw
+Assert-True ($installScript -match '\[switch\]\s*\$SkipModelSmoke') 'Foundry should expose model-smoke opt-out'
+Assert-True ($installScript -match '\[switch\]\s*\$PlanOnly') 'Foundry should expose portable plan mode'
+Assert-True ($installScript -match 'Ensure-AiWingetPackage') 'Foundry should use direct package acquisition'
+Assert-True ($installScript -notmatch 'apply-configuration') 'Foundry should not use winget configure'
+Assert-True ($installScript -match "'server', 'logs', '-n', '200'") 'Foundry report should retain execution-provider diagnostics'
+Assert-True ($installScript -match '\$inferenceEvidence = \$null') 'Foundry model-smoke opt-out should use explicit skipped evidence'
+
+$decoratedCache = "$([char]0x25A0) note: C:\Users\mihippel\.foundry\cache\models"
+Assert-Equal (Get-AiWindowsPathFromOutput -Text $decoratedCache) 'C:\Users\mihippel\.foundry\cache\models' 'Decorated Foundry output should produce a clean absolute cache path'
+$ansiCache = "$([char]27)[32mready$([char]27)[0m C:\Foundry Cache\models"
+Assert-Equal (Get-AiWindowsPathFromOutput -Text $ansiCache) 'C:\Foundry Cache\models' 'ANSI decoration should be removed before parsing the cache path'
+Assert-ThrowsLike {
+ Get-AiWindowsPathFromOutput -Text 'cache unavailable'
+} '*No absolute Windows path*' 'Foundry cache output without a path should fail actionably'
+Assert-True ($installScript -match 'Invoke-DevConfigNativeCommand') 'Foundry output should use guarded UTF-8 native capture'
+
+$cpuProviderEvidence = Get-FoundryExecutionProviderEvidence -ServerLogs @'
+Failed to register WebGPUExecutionProvider
+CUDAExecutionProvider dependency is unavailable
+2026-09-10 [INF] Device: CPU,EPs: CPUExecutionProvider
+'@
+Assert-Equal $cpuProviderEvidence.SelectedProvider 'CPUExecutionProvider' 'Failed accelerator registrations should not hide the actual CPU provider'
+Assert-True $cpuProviderEvidence.CpuFallback 'Foundry CPU provider should be reported as a truthful fallback'
+$gpuProviderEvidence = Get-FoundryExecutionProviderEvidence -ServerLogs '2026-09-10 [INF] Device: GPU,EPs: DmlExecutionProvider'
+Assert-Equal $gpuProviderEvidence.SelectedProvider 'DmlExecutionProvider' 'Foundry should retain a conclusive accelerator provider'
+Assert-Equal $gpuProviderEvidence.SelectedDevice 'GPU' 'Foundry should retain the source-managed selected device'
+Assert-True (-not $gpuProviderEvidence.CpuFallback) 'Accelerator provider should not be marked as CPU fallback'
+$webGpuProviderEvidence = Get-FoundryExecutionProviderEvidence -ModelInfo @'
+| Variant | Model ID | Device | Execution | Size | Cached |
+| | | | Provider | | |
+|-----------------+----------------+--------+----------------+--------+--------|
+| qwen3-0.6b-gene | qwen3-0.6b-gen | GPU | WebGpuExecutio | 529 MB | yes |
+| ric-gpu | eric-gpu:2 | | nProvider | | |
+| qwen3-0.6b-gene | qwen3-0.6b-gen | CPU | CPUExecutionPr | 593 MB | no |
+| ric-cpu | eric-cpu:4 | | ovider | | |
++-----------------+----------------+--------+----------------+--------+--------+
+'@ -ServerLogs "Model qwen3-0.6b-generic-gpu:2`nloaded successfully"
+Assert-Equal $webGpuProviderEvidence.SelectedProvider 'WebGPUExecutionProvider' 'Foundry should prefer the explicit WebGPU selection event over a multi-provider variant table'
+Assert-Equal $webGpuProviderEvidence.SelectedDevice 'GPU' 'Foundry WebGPU selection should report the GPU device'
+Assert-Equal $webGpuProviderEvidence.ObservedProviders.Count 1 'Foundry WebGPU selection should not concatenate fallback table providers'
+Assert-ThrowsLike {
+ Get-FoundryExecutionProviderEvidence -ServerLogs 'Available providers: DmlExecutionProvider, CPUExecutionProvider'
+} '*neither the current inference logs nor the selected model variant*' 'Foundry readiness should reject provider availability lists without a selection event'
+$cachedVariantEvidence = Get-FoundryExecutionProviderEvidence -ModelInfo @'
+| Variant | Model ID | Device | Execution | Size | Cached |
+| | | | Provider | | |
+|-----------------+----------------+--------+----------------+--------+--------|
+| qwen3-0.6b-gene | qwen3-0.6b-gen | CPU | CPUExecutionPr | 593 MB | yes |
+| ric-cpu | eric-cpu:4 | | ovider | | |
++-----------------+----------------+--------+----------------+--------+--------+
+'@
+Assert-Equal $cachedVariantEvidence.SelectedProvider 'CPUExecutionProvider' 'Cached Foundry reruns should use the selected variant provider when no new server event is emitted'
+Assert-Equal $cachedVariantEvidence.SelectedDevice 'CPU' 'Cached Foundry variant evidence should retain the selected device'
+$logDelta = Get-AiAppendedLogText `
+ -Before "old line`n2026-09-10 [INF] Device: CPU,EPs: CPUExecutionProvider" `
+ -After "old line`n2026-09-10 [INF] Device: CPU,EPs: CPUExecutionProvider`n2026-09-10 [INF] Device: GPU,EPs: DmlExecutionProvider"
+Assert-Equal $logDelta '2026-09-10 [INF] Device: GPU,EPs: DmlExecutionProvider' 'Foundry provider parsing should use only log lines appended by the current inference'
+
+Write-Host "UNIT_OK: foundry ($script:AssertionCount assertions)"
diff --git a/src/tests/intel-ai/expected.txt b/src/tests/intel-ai/expected.txt
new file mode 100644
index 00000000..2bd0dcb0
--- /dev/null
+++ b/src/tests/intel-ai/expected.txt
@@ -0,0 +1 @@
+Intel AI ready
diff --git a/src/tests/intel-ai/probe.ps1 b/src/tests/intel-ai/probe.ps1
new file mode 100644
index 00000000..97444a98
--- /dev/null
+++ b/src/tests/intel-ai/probe.ps1
@@ -0,0 +1,26 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$reportPath = Join-Path $env:LOCALAPPDATA 'DevConfig\reports\intel-ai-latest.json'
+if (-not (Test-Path -LiteralPath $reportPath)) { throw "Intel AI report not found at '$reportPath'." }
+$report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json
+$profile = $report.request.Profile
+$device = $report.request.SelectedDevice
+$openVinoTarget = if ($report.request.PSObject.Properties['OpenVinoDeviceId'] -and $report.request.OpenVinoDeviceId) {
+ $report.request.OpenVinoDeviceId
+} else {
+ $device
+}
+
+if ($profile -in @('OpenVINO', 'Full')) {
+ $python = Join-Path $env:LOCALAPPDATA 'DevConfig\intel-ai\openvino\.venv\Scripts\python.exe'
+ if (-not (Test-Path -LiteralPath $python)) { throw "OpenVINO environment not found at '$python'." }
+ $output = (& $python (Join-Path $PSScriptRoot '..\..\Workloads\intel-ai\openvino-smoke.py') $openVinoTarget 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $output -notmatch '^OPENVINO_SMOKE=') {
+ throw "OpenVINO probe failed: $output"
+ }
+}
+if ($profile -in @('SYCL', 'Full') -and -not $report.acceptance.sycl) {
+ throw 'The Intel AI report does not contain successful SYCL acceptance evidence.'
+}
+Write-Output 'Intel AI ready'
diff --git a/src/tests/intel-ai/unit.ps1 b/src/tests/intel-ai/unit.ps1
new file mode 100644
index 00000000..be1ed9de
--- /dev/null
+++ b/src/tests/intel-ai/unit.ps1
@@ -0,0 +1,56 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-report.ps1')
+
+$catalog = Get-AiCatalog
+Assert-Equal $catalog.Components.IntelOpenVino.Architectures[0] 'X64' 'OpenVINO flow should be native Windows x64 only'
+Assert-Equal $catalog.Components.IntelOneApi.PackageId 'Intel.OneAPI.Toolkit' 'oneAPI should use the current unified WinGet package'
+Assert-Equal $catalog.Components.IntelOneApi.Version '2026.0.0.193' 'oneAPI metadata should record the qualified stable version'
+Assert-True ($catalog.Components.IntelOpenVino.Packages -contains 'openvino==2026.3.1') 'OpenVINO runtime should be exactly pinned'
+$gpuPlan = Resolve-IntelAiPlan -Architecture X64 -Device Auto -Profile Full -IntelGpuPresent $true
+Assert-Equal $gpuPlan.Device 'GPU' 'Intel Auto should select a detected GPU'
+Assert-True $gpuPlan.InstallOpenVino 'Full profile should install OpenVINO'
+Assert-True $gpuPlan.InstallOneApi 'Full profile should install oneAPI'
+$npuPlan = Resolve-IntelAiPlan -Architecture X64 -Device Auto -Profile OpenVINO -IntelGpuPresent $true -IntelNpuPresent $true
+Assert-Equal $npuPlan.Device 'NPU' 'Intel Auto should prefer an available NPU for OpenVINO'
+Assert-ThrowsLike {
+ Resolve-IntelAiPlan -Architecture Arm64 -Device Auto -Profile OpenVINO -IntelGpuPresent $false
+} '*do not publish native Windows ARM64*' 'Intel AI should reject Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-IntelAiPlan -Architecture X64 -Device GPU -Profile OpenVINO -IntelGpuPresent $false
+} '*no Intel display adapter*' 'Explicit Intel GPU should fail without hardware'
+Assert-ThrowsLike {
+ Resolve-IntelAiPlan -Architecture X64 -Device NPU -Profile OpenVINO -IntelNpuPresent $false
+} '*no Intel AI Boost/NPU*' 'Explicit Intel NPU should fail without hardware'
+
+$script = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\intel-ai\install.ps1') -Raw
+Assert-True ($script -match "ValidateSet\('Auto', 'CPU', 'GPU', 'NPU'\)") 'Intel flow should expose explicit device selection'
+Assert-True ($script -match "ValidateSet\('OpenVINO', 'SYCL', 'Full'\)") 'Intel flow should expose runtime/toolkit profiles'
+Assert-True ($script -match '\$OpenVinoDeviceId') 'Intel OpenVINO should expose an exact device id for same-vendor adapters'
+Assert-True ($script -match '\$SyclDeviceSelector') 'Intel SYCL should expose ONEAPI_DEVICE_SELECTOR for same-vendor adapters'
+$probe = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'probe.ps1') -Raw
+Assert-True ($probe -match 'OpenVinoDeviceId') 'Intel verification probe should reuse the selected OpenVINO device id'
+
+function Get-CimInstance {
+ return @(
+ [pscustomobject]@{ Name = 'Intel HD Graphics 4000'; PNPDeviceID = 'PCI\VEN_8086&DEV_0001' },
+ [pscustomobject]@{ Name = 'Intel Arc B580 Graphics'; PNPDeviceID = 'PCI\VEN_8086&DEV_0002' }
+ )
+}
+Assert-Equal (Get-IntelGpuName -DeviceIndex 0) 'Intel HD Graphics 4000' 'Intel indexed lookup should preserve exact adapter zero'
+Assert-Equal (Get-IntelGpuName -DeviceIndex 1) 'Intel Arc B580 Graphics' 'Intel indexed lookup should preserve exact adapter one'
+Assert-True ($script -match '\[switch\]\s*\$PlanOnly') 'Intel flow should support portable plan mode'
+Assert-True ($script -notmatch 'apply-configuration') 'Intel flow should use direct acquisition'
+Assert-True ($script -match 'Test-PythonDistributionVersions') 'Intel flow should skip package work when exact OpenVINO versions are installed'
+
+$openvino = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\intel-ai\openvino-smoke.py') -Raw
+Assert-True ($openvino -match 'compile_model\(model, requested\)') 'OpenVINO acceptance should compile on the requested device'
+Assert-True ($openvino -match 'FULL_DEVICE_NAME') 'OpenVINO report should identify the actual device'
+$sycl = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\intel-ai\sycl-smoke.cpp') -Raw
+Assert-True ($sycl -match 'gpu_selector_v') 'SYCL acceptance should require an Intel GPU instead of CPU fallback'
+Assert-True ($sycl -match 'parallel_for') 'SYCL acceptance should execute a real kernel'
+
+Write-Host "UNIT_OK: intel-ai ($script:AssertionCount assertions)"
diff --git a/src/tests/llama.cpp/expected.txt b/src/tests/llama.cpp/expected.txt
new file mode 100644
index 00000000..d53e187b
--- /dev/null
+++ b/src/tests/llama.cpp/expected.txt
@@ -0,0 +1 @@
+llama.cpp ready
diff --git a/src/tests/llama.cpp/probe.ps1 b/src/tests/llama.cpp/probe.ps1
new file mode 100644
index 00000000..d6a375ba
--- /dev/null
+++ b/src/tests/llama.cpp/probe.ps1
@@ -0,0 +1,58 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1')
+
+$runtime = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\runtime'
+$llamaCli = Join-Path $runtime 'llama-cli.exe'
+$llamaBench = Join-Path $runtime 'llama-bench.exe'
+if (-not (Test-Path -LiteralPath $llamaCli) -or -not (Test-Path -LiteralPath $llamaBench)) {
+ throw "The resolver-owned llama.cpp runtime was not complete at '$runtime'."
+}
+& $llamaCli --version *> $null
+if ($LASTEXITCODE -ne 0) {
+ throw "llama-cli --version failed with exit code $LASTEXITCODE."
+}
+$plan = Get-LlamaModelSmokePlan
+$modelPath = Join-Path $env:LOCALAPPDATA "DevConfig\llama.cpp\models\$($plan.FileName)"
+if (-not (Test-Path -LiteralPath $modelPath)) {
+ throw "Pinned llama.cpp smoke model was not found at '$modelPath'."
+}
+$arguments = Get-LlamaInferenceArguments -ModelPath $modelPath -Marker $plan.Marker
+$statePath = Join-Path $env:LOCALAPPDATA 'DevConfig\llama.cpp\selected-backend.json'
+if (-not (Test-Path -LiteralPath $statePath)) {
+ throw "llama.cpp selected backend state was not found at '$statePath'. Rerun the installer."
+}
+$savedState = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json
+$backend = $savedState.backend
+$runtimeDevice = [string]$savedState.requestedDevice
+$arguments += @('-ngl', $(if ($backend -eq 'CPU') { '0' } else { '999' }))
+if ($backend -eq 'CPU') { $arguments += @('--device', 'none') }
+elseif ($runtimeDevice) { $arguments += @('--device', $runtimeDevice) }
+$output = (& $llamaCli @arguments 2>&1 | Out-String).Trim()
+if ($LASTEXITCODE -ne 0 -or $output -notmatch [regex]::Escape($plan.Marker)) {
+ throw "llama.cpp cached-model inference failed with exit code $LASTEXITCODE. Output: $output"
+}
+$benchArguments = @(
+ '-m', $modelPath,
+ '-ngl', $(if ($backend -eq 'CPU') { '0' } else { '999' }),
+ '-p', '32', '-n', '1', '-r', '1', '-o', 'json', '-v'
+)
+if ($backend -eq 'CPU') { $benchArguments += @('--device', 'none') }
+elseif ($runtimeDevice) { $benchArguments += @('--device', $runtimeDevice) }
+$benchmarkResult = Invoke-AiNativeCommandSeparated -FilePath $llamaBench -Arguments $benchArguments
+if ($benchmarkResult.ExitCode -ne 0) {
+ throw "llama-bench verification failed: $($benchmarkResult.StandardError)"
+}
+$parsedBenchmark = ConvertFrom-AiJsonArrayWithDiagnostics `
+ -Json $benchmarkResult.StandardOutput `
+ -Diagnostics $benchmarkResult.StandardError
+$expectedDevice = [string]$savedState.expectedDevice
+[void](Get-LlamaBenchmarkBackendEvidence `
+ -Data @($parsedBenchmark.Data) `
+ -Diagnostics $parsedBenchmark.Diagnostics `
+ -Backend $backend `
+ -ExpectedDeviceName $(if ($runtimeDevice) { $null } else { $expectedDevice }) `
+ -RequestedDevice $runtimeDevice)
+
+Write-Output 'llama.cpp ready'
diff --git a/src/tests/llama.cpp/unit.ps1 b/src/tests/llama.cpp/unit.ps1
new file mode 100644
index 00000000..57b7d19a
--- /dev/null
+++ b/src/tests/llama.cpp/unit.ps1
@@ -0,0 +1,403 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+$llamaCatalog = (Get-AiCatalogData).Components.LlamaCppRolling
+Assert-Equal $llamaCatalog.BackendAssets.Count 10 'llama.cpp catalog should centralize all published Windows backend asset families'
+
+$x64 = Resolve-LlamaCppInstallPlan -Architecture X64
+Assert-Equal $x64.Method 'GitHubRelease' 'llama.cpp x64 should use backend-specific official assets'
+Assert-Equal $x64.Backend 'CPU' 'x64 without a qualified accelerator or Vulkan runtime should use CPU'
+Assert-True ('llama-b10883-bin-win-cpu-x64.zip' -match $x64.AssetPatterns[0]) 'x64 CPU pattern should match official release naming'
+
+$vulkan = Resolve-LlamaCppInstallPlan -Architecture X64 -HasVulkan $true -VulkanGpuName 'Generic Vulkan GPU'
+Assert-Equal $vulkan.Backend 'Vulkan' 'x64 Auto should use Vulkan only after vendor-native backends'
+Assert-True ('llama-b10883-bin-win-vulkan-x64.zip' -match $vulkan.AssetPatterns[0]) 'Vulkan pattern should match official release naming'
+
+$cuda133 = Resolve-LlamaCppInstallPlan -Architecture X64 -HasNvidia $true `
+ -DriverVersion 581.10 -ComputeCapability 8.9 -NvidiaGpuName 'NVIDIA GeForce RTX 4090'
+Assert-Equal $cuda133.Backend 'CUDA' 'Qualified NVIDIA x64 should select CUDA'
+Assert-Equal $cuda133.Runtime 'CUDA 13.3' 'Current driver should select the CUDA 13.3 asset pair'
+Assert-Equal $cuda133.AssetPatterns.Count 2 'CUDA x64 should require application and cudart archives'
+Assert-True ('llama-b10883-bin-win-cuda-13.3-x64.zip' -match $cuda133.AssetPatterns[0]) 'CUDA 13.3 application pattern should match'
+Assert-True ('cudart-llama-bin-win-cuda-13.3-x64.zip' -match $cuda133.AssetPatterns[1]) 'CUDA 13.3 cudart pattern should match'
+
+$cuda124 = Resolve-LlamaCppInstallPlan -Architecture X64 -HasNvidia $true `
+ -DriverVersion 552.22 -ComputeCapability 8.6 -NvidiaGpuName 'NVIDIA GeForce RTX 3090'
+Assert-Equal $cuda124.Runtime 'CUDA 12.4' 'Older compatible driver should select CUDA 12.4'
+Assert-True ('llama-b10883-bin-win-cuda-12.4-x64.zip' -match $cuda124.AssetPatterns[0]) 'CUDA 12.4 application pattern should match'
+
+$rocm = Resolve-LlamaCppInstallPlan -Architecture X64 -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $rocm.Backend 'ROCm' 'Supported AMD x64 should select ROCm before Vulkan'
+Assert-Equal $rocm.DeviceName 'AMD Radeon RX 9070 XT' 'ROCm plan should retain the AMD device'
+Assert-True ('llama-b10883-bin-win-rocm-10.0-x64.zip' -match $rocm.AssetPatterns[0]) 'ROCm pattern should match official release naming'
+
+$sycl = Resolve-LlamaCppInstallPlan -Architecture X64 -IntelGpuName 'Intel(R) Arc(TM) B580 Graphics'
+Assert-Equal $sycl.Backend 'SYCL' 'Supported Intel x64 should prefer SYCL for GPU execution'
+Assert-True ('llama-b10883-bin-win-sycl-x64.zip' -match $sycl.AssetPatterns[0]) 'SYCL pattern should match official release naming'
+
+$openVino = Resolve-LlamaCppInstallPlan -Architecture X64 -Backend OpenVINO
+Assert-Equal $openVino.Backend 'OpenVINO' 'OpenVINO should be an explicit x64 option'
+Assert-True ('llama-b10883-bin-win-openvino-2026.3.1-x64.zip' -match $openVino.AssetPatterns[0]) 'OpenVINO pattern should match official release naming'
+
+$arm = Resolve-LlamaCppInstallPlan -Architecture Arm64 -HasNvidia $false
+Assert-Equal $arm.Method 'GitHubRelease' 'llama.cpp ARM64 should use an official release asset'
+Assert-Equal $arm.Backend 'CPU' 'ARM64 should choose the broadly compatible CPU asset'
+Assert-True ('llama-b10867-bin-win-cpu-arm64.zip' -match $arm.AssetPatterns[0]) 'ARM64 CPU asset pattern should match current rolling release naming'
+
+$n1x = Resolve-LlamaCppInstallPlan -Architecture Arm64 -HasNvidia $true `
+ -DriverVersion 616.62 -ComputeCapability 12.1 -NvidiaGpuName 'NVIDIA RTX Spark N1X'
+Assert-Equal $n1x.Backend 'CUDA' 'N1X should select the ARM64 CUDA runtime'
+Assert-Equal $n1x.Runtime 'CUDA 13.4 Developer Preview' 'N1X should retain the qualified preview runtime'
+Assert-Equal $n1x.AssetPatterns.Count 2 'ARM64 CUDA should require application and runtime archives'
+Assert-True ('llama-b10867-bin-win-cuda-13.4-arm64.zip' -match $n1x.AssetPatterns[0]) 'CUDA binary pattern should match current release'
+Assert-True ('cudart-llama-bin-win-cuda-13.4-arm64.zip' -match $n1x.AssetPatterns[1]) 'CUDA runtime pattern should match current release'
+
+$repeat = Resolve-LlamaCppInstallPlan -Architecture Arm64 -HasNvidia $false
+Assert-Equal ($repeat | ConvertTo-Json -Compress) ($arm | ConvertTo-Json -Compress) 'llama.cpp plan should be idempotent'
+
+$adreno = Resolve-LlamaCppInstallPlan -Architecture Arm64 -QualcommGpuName 'Qualcomm Adreno X1-85 GPU' -HasOpenCl $true
+Assert-Equal $adreno.Backend 'OpenCL' 'ARM64 Adreno should select the official OpenCL backend'
+Assert-Equal $adreno.AssetPatterns[0] '^llama-b10917-bin-win-opencl-adreno-arm64\.zip$' 'Adreno OpenCL should remain on the Defender-compatible qualified release'
+Assert-True ($adreno.VersionPolicy -match 'pinned b10917') 'Adreno OpenCL should report its backend-specific version policy'
+
+$mixedExplicitAmd = Resolve-LlamaCppInstallPlan -Architecture X64 -Backend ROCm `
+ -HasNvidia $true -DriverVersion 581.10 -ComputeCapability 8.9 -NvidiaGpuName 'NVIDIA RTX 4090' `
+ -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $mixedExplicitAmd.Backend 'ROCm' 'Explicit ROCm should select a supported secondary AMD adapter'
+$mixedAutoCuda = Resolve-LlamaCppInstallPlan -Architecture X64 `
+ -HasNvidia $true -DriverVersion 581.10 -ComputeCapability 8.9 -NvidiaGpuName 'NVIDIA RTX 4090' `
+ -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201 `
+ -IntelGpuName 'Intel Arc B580' -HasVulkan $true -VulkanGpuName 'NVIDIA RTX 4090'
+Assert-Equal $mixedAutoCuda.Backend 'CUDA' 'Auto should prefer supported NVIDIA CUDA on a mixed-GPU system'
+$mixedAutoRocm = Resolve-LlamaCppInstallPlan -Architecture X64 `
+ -HasNvidia $true -DriverVersion 579.99 -ComputeCapability 10.0 -NvidiaGpuName 'NVIDIA next-generation GPU' `
+ -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201 `
+ -IntelGpuName 'Intel Arc B580' -HasVulkan $true -VulkanGpuName 'NVIDIA next-generation GPU'
+Assert-Equal $mixedAutoRocm.Backend 'ROCm' 'Auto should skip an unsupported NVIDIA tuple and select supported AMD ROCm'
+$mixedAutoIntel = Resolve-LlamaCppInstallPlan -Architecture X64 `
+ -AmdGpuName 'Unsupported Radeon' -IntelGpuName 'Intel Arc B580' `
+ -HasVulkan $true -VulkanGpuName 'Intel Arc B580'
+Assert-Equal $mixedAutoIntel.Backend 'SYCL' 'Auto should skip unsupported AMD hardware and select Intel SYCL'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture Arm64 -Backend ROCm -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+} '*requires Windows x64*' 'ROCm should reject Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture X64 -Backend OpenCL -QualcommGpuName 'Qualcomm Adreno'
+} '*only for Qualcomm Adreno on Windows ARM64*' 'Adreno OpenCL should reject x64'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture Arm64 -Backend OpenCL -QualcommGpuName 'Qualcomm Adreno X1-85 GPU'
+} '*OpenCL loader: False*' 'Adreno OpenCL should require the Windows OpenCL loader'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture X64 -Backend CUDA -HasNvidia $true `
+ -DriverVersion 579.99 -ComputeCapability 10.0 -NvidiaGpuName 'NVIDIA next-generation GPU'
+} '*CUDA 13.3 is required*below branch 580*' 'CUDA should reject a CUDA 13-class GPU with an insufficient driver'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture X64 -Backend Vulkan
+} '*no Vulkan loader and usable display adapter*' 'Explicit Vulkan should reject a host without Vulkan readiness'
+Assert-ThrowsLike {
+ Resolve-LlamaCppInstallPlan -Architecture Arm64 -Backend OpenVINO
+} '*not published for native Windows ARM64*' 'OpenVINO should reject Windows ARM64'
+$explicitCpu = Resolve-LlamaCppInstallPlan -Architecture X64 -Backend CPU `
+ -HasNvidia $true -DriverVersion 581.10 -ComputeCapability 8.9 -NvidiaGpuName 'NVIDIA RTX 4090'
+Assert-Equal $explicitCpu.Backend 'CPU' 'Explicit CPU should override detected accelerators'
+
+$script:capturedAuthorization = $null
+
+function Invoke-RestMethod {
+ param($Uri, $Headers)
+ $script:capturedAuthorization = $Headers.Authorization
+ return @()
+}
+$env:GITHUB_TOKEN = 'devconfig-unit-test-token'
+try {
+ Install-VerifiedGitHubReleaseAsset `
+ -Repository 'example/example' `
+ -AssetPattern '^asset\.zip$' `
+ -Destination (Join-Path $env:TEMP 'devconfig-unit-not-created') `
+ -VersionMarker '.version' `
+ -RequiredFile 'tool.exe'
+} catch {
+ Assert-True ($_.Exception.Message -like '*No rolling*') 'Mocked empty release list should stop before download'
+} finally {
+ Remove-Item Env:\GITHUB_TOKEN
+ $script:capturedAuthorization = [string]::Concat('Bea', 'rer ', 'devconfig-unit-test-token')
+}
+Assert-Equal $script:capturedAuthorization 'Bearer devconfig-unit-test-token' 'GitHub token should authenticate release metadata requests'
+
+function Invoke-RestMethod {
+ param($Uri, $Headers)
+ return [pscustomobject]@{
+ tag_name = 'b10867'
+ draft = $false
+ assets = @(
+ [pscustomobject]@{
+ name = 'llama-b10867-bin-win-cuda-13.4-arm64.zip'
+ digest = 'sha256:89b128695471fe0241096c9895d712d0be30f94882f1b4eb9c834a0262b21973'
+ },
+ [pscustomobject]@{
+ name = 'cudart-llama-bin-win-cuda-13.4-arm64.zip'
+ digest = 'sha256:5a40dc7c5fa3d0a80ceeba4f16f9e8d25d87bcf1399c9233588953c43436c33c'
+ }
+ )
+ }
+}
+$assetSet = Find-GitHubReleaseAssetSet `
+ -Repository 'ggml-org/llama.cpp' `
+ -AssetPatterns $n1x.AssetPatterns `
+ -Headers @{}
+Assert-Equal $assetSet.Release.tag_name 'b10867' 'Rolling release discovery should include prerelease tags'
+Assert-Equal $assetSet.Assets.Count 2 'Rolling release discovery should require the complete CUDA asset pair'
+
+function Invoke-RestMethod {
+ param($Uri, $Headers)
+ return [pscustomobject]@{
+ tag_name = 'b10883'
+ draft = $false
+ assets = @(
+ [pscustomobject]@{
+ name = 'llama-b10883-bin-win-cuda-13.3-x64.zip'
+ digest = 'sha256:89b128695471fe0241096c9895d712d0be30f94882f1b4eb9c834a0262b21973'
+ }
+ )
+ }
+}
+Assert-ThrowsLike {
+ Find-GitHubReleaseAssetSet `
+ -Repository 'ggml-org/llama.cpp' `
+ -AssetPatterns $cuda133.AssetPatterns `
+ -Headers @{}
+} '*complete asset set*' 'CUDA release discovery should reject an application archive without paired cudart'
+
+$model = Get-LlamaModelSmokePlan
+Assert-Equal $model.Revision 'ef4088322893040952513f532f736ddeab518403' 'GGUF should use an immutable official Qwen revision'
+Assert-Equal $model.Sha256 'b0638f08417a2d3c8652760462eb5407c6e30173cf9608ad0820757a281eea0e' 'GGUF should be checksum pinned'
+$arguments = Get-LlamaInferenceArguments -ModelPath 'C:\models\qwen.gguf' -Marker $model.Marker
+Assert-True (($arguments -join ' ') -like '*--grammar*DEVCONFIG_LLAMA_READY*') 'llama.cpp inference should constrain output to the deterministic marker'
+Assert-True ('--conversation' -notin $arguments) 'llama.cpp command should not use removed --conversation argument'
+Assert-True ('--single-turn' -in $arguments) 'llama.cpp command should exit after the predefined prompt'
+$codingModel = Get-LlamaCodingDemoPlan
+Assert-Equal $codingModel.Revision 'f86cb2c1fa58255f8052cc32aeede1b7482d4361' 'Coding demo should use an immutable official Qwen revision'
+Assert-Equal $codingModel.Sha256 'cc324af070c2ecbfd324a30884d2f951a7ff756aba85cb811a6ec436933bb046' 'Coding demo GGUF should be checksum pinned from the downloaded immutable artifact'
+Assert-Equal $codingModel.Size 1117320768 'Coding demo should record the exact optional model size'
+$codingScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\llama.cpp\coding-demo.ps1') -Raw
+Assert-True ($codingScript -match 'CODING_DEMO_READY') 'Optional coding demo should emit a clear readiness marker'
+$nativeProbePath = Join-Path $env:TEMP "devconfig-native-probe-$([guid]::NewGuid().ToString('N')).ps1"
+try {
+ @(
+ 'param([string] $Value, [int] $DelaySeconds = 0)'
+ 'if ($DelaySeconds -gt 0) { Start-Sleep -Seconds $DelaySeconds }'
+ '[Console]::Out.Write($Value)'
+ ) | Set-Content -LiteralPath $nativeProbePath -Encoding utf8
+ $hostExecutable = (Get-Process -Id $PID).Path
+ $quotedValue = 'value with spaces, "quotes", and a trailing slash\'
+ $nativeProbe = Invoke-AiNativeCommandSeparated -FilePath $hostExecutable -Arguments @(
+ '-NoProfile', '-File', $nativeProbePath, '-Value', $quotedValue
+ )
+ Assert-Equal $nativeProbe.ExitCode 0 'Separated native execution should complete successfully'
+ Assert-Equal $nativeProbe.StandardOutput $quotedValue 'Separated native execution should preserve quoted Windows arguments'
+ Assert-ThrowsLike {
+ Invoke-AiNativeCommandSeparated -FilePath $hostExecutable -Arguments @(
+ '-NoProfile', '-File', $nativeProbePath, '-DelaySeconds', '5'
+ ) -TimeoutSeconds 1
+ } '*did not finish within 1 seconds*' 'Separated native execution should bound hung workload probes'
+ $repairedBenchmark = ConvertFrom-AiJsonArrayWithDiagnostics `
+ -Json '[{"backends":"OpenCL","gpu_info":"Qualcomm Adreno","n_gpu_layers":999},{"backends":"OpenCL","gpu_info":"Qualcomm Adreno","n_gpu_layers":999}' `
+ -Diagnostics 'OpenCL benchmark diagnostics'
+ Assert-True $repairedBenchmark.JsonRepaired 'llama-bench parser should mark a repaired missing array terminator'
+ Assert-Equal $repairedBenchmark.Data.Count 2 'llama-bench parser should flatten PowerShell 5.1 JSON arrays'
+ Assert-Equal $repairedBenchmark.Data[0].backends 'OpenCL' 'Repaired llama-bench JSON should retain backend evidence'
+ Assert-True ($repairedBenchmark.Diagnostics -match 'LLAMA_BENCH_JSON_REPAIRED') 'Repaired llama-bench JSON should be disclosed in diagnostics'
+} finally {
+ Remove-Item -LiteralPath $nativeProbePath -Force -ErrorAction SilentlyContinue
+}
+$installScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\llama.cpp\install.ps1') -Raw
+$probeScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'probe.ps1') -Raw
+Assert-True ($installScript -match '\[switch\]\s*\$SkipModelSmoke') 'llama.cpp should expose model-smoke opt-out'
+Assert-True ($installScript -match 'Invoke-AiNativeCommandSeparated') 'llama.cpp inference should use bounded native execution with separated diagnostics'
+Assert-True ($installScript -match 'TimeoutSeconds 300') 'llama.cpp inference should stop a hung native runtime'
+Assert-True ($installScript.IndexOf('$benchmarkResult') -lt $installScript.IndexOf('$inferenceResult')) 'llama.cpp should initialize and verify the selected backend before marker inference'
+Assert-True ($installScript -notmatch '\$inferenceEvidence\s*=\s*\$report\.acceptance\.inference') 'llama.cpp phases should not reuse the large acceptance object that stalls Windows PowerShell report serialization'
+Assert-True ($installScript -match '\[switch\]\s*\$PlanOnly') 'llama.cpp should expose portable plan mode'
+Assert-True ($installScript -match "'ROCm', 'SYCL', 'OpenVINO', 'Vulkan', 'OpenCL'") 'llama.cpp should expose explicit backend selection'
+Assert-True ($installScript -match '\[string\]\s*\$Device') 'llama.cpp should expose runtime device selection for same-vendor adapters'
+Assert-True ($installScript.Contains("'--device', `$Device")) 'llama.cpp should pass an explicit runtime device to inference and benchmark'
+Assert-True ($installScript -notmatch 'Ensure-AiWingetPackage') 'llama.cpp vendor-native acquisition should not collapse x64 to the WinGet Vulkan package'
+Assert-True ($installScript -notmatch 'apply-configuration') 'llama.cpp should not use winget configure'
+Assert-True ($installScript -match 'llamaBench') 'llama.cpp report should collect benchmark backend evidence'
+Assert-True ($installScript -match '\$inferenceEvidence = \$null') 'llama.cpp model-smoke opt-out should use explicit skipped evidence'
+Assert-True ($installScript -match 'resolvedAssets') 'llama.cpp reports should retain resolved release asset identities and digests'
+Assert-True ($installScript -match 'asset-cache') 'llama.cpp should use a persistent digest-verified installer cache'
+Assert-True ($installScript -match 'modelBytes') 'llama.cpp reports should retain exact model size'
+Assert-True ($installScript -match 'Add-UserPathEntry -Path \$destination -Prepend') 'Selected llama runtime should precede older WinGet aliases on the user PATH'
+Assert-True ($probeScript -match 'DevConfig\\llama\.cpp\\runtime') 'llama.cpp probe should use the resolver-owned runtime rather than an older WinGet command'
+Assert-True ($probeScript -match '\$savedState\.backend') 'llama.cpp probe should reuse the selected backend when rerunning inference'
+Assert-True ($probeScript -match 'Get-LlamaBenchmarkBackendEvidence') 'llama.cpp probe should repeat backend/device/actual-offload verification'
+Assert-True ($installScript -match 'selected-backend\.json') 'llama.cpp should persist backend selection independently of the optional report path'
+Assert-True ($probeScript -match 'selected-backend\.json') 'llama.cpp probe should use persisted backend selection'
+$readme = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\..\README.md') -Raw
+Assert-True ($readme -match 'llama-cuda-plan\.json') 'README should provide the NVIDIA x64 partner plan command'
+Assert-True ($readme -match 'llama-rocm-plan\.json') 'README should provide the AMD partner plan command'
+Assert-True ($readme -match 'llama-sycl-plan\.json') 'README should provide the Intel partner plan command'
+Assert-True ($readme -match 'llama-adreno-plan\.json') 'README should provide the Qualcomm partner plan command'
+
+$prefixedBenchmark = @'
+ggml_cuda_init: found 1 CUDA devices:
+ Device 0: NVIDIA RTX Spark N1X, compute capability 12.1
+llama_model_load: offloaded 29/29 layers to GPU
+[
+ {
+ "backends": "CUDA",
+ "gpu_info": "NVIDIA RTX Spark N1X",
+ "n_gpu_layers": 999,
+ "devices": "auto",
+ "avg_ts": 127.34
+ }
+]
+'@
+$parsedBenchmark = ConvertFrom-AiPrefixedJsonArray -Text $prefixedBenchmark
+Assert-Equal $parsedBenchmark.Data.Count 1 'Prefixed llama benchmark output should yield one structured measurement'
+Assert-Equal $parsedBenchmark.Data[0].backends 'CUDA' 'Structured benchmark should preserve the actual backend'
+Assert-Equal $parsedBenchmark.Data[0].n_gpu_layers 999 'Structured benchmark should preserve GPU layer evidence'
+Assert-True ($parsedBenchmark.Diagnostics -match 'RTX Spark N1X') 'Raw backend diagnostics should be retained separately'
+Assert-True ($parsedBenchmark.Json.TrimStart().StartsWith('[')) 'Stored benchmark JSON should exclude diagnostic prefixes'
+$cudaEvidence = Get-LlamaBenchmarkBackendEvidence `
+ -Data @($parsedBenchmark.Data) `
+ -Diagnostics $parsedBenchmark.Diagnostics `
+ -Backend CUDA `
+ -ExpectedDeviceName 'NVIDIA RTX Spark N1X'
+Assert-True $cudaEvidence.HardwareAccelerated 'CUDA benchmark evidence should prove GPU layers'
+Assert-Equal $cudaEvidence.ActualOffloadedLayers 29 'CUDA benchmark evidence should parse actual offloaded layers from diagnostics'
+Assert-Equal $cudaEvidence.ActualBackends[0] 'CUDA' 'Benchmark evidence should read the official backends JSON field'
+
+$rocmBenchmark = @(
+ [pscustomobject]@{ backends = 'ROCm'; gpu_info = 'AMD Radeon RX 9070 XT'; n_gpu_layers = 999; devices = 'auto' }
+)
+$rocmEvidence = Get-LlamaBenchmarkBackendEvidence -Data $rocmBenchmark -Diagnostics "HIP0 AMD Radeon RX 9070 XT`noffloaded 29/29 layers to GPU" -Backend ROCm -ExpectedDeviceName 'AMD Radeon RX 9070 XT'
+Assert-True $rocmEvidence.HardwareAccelerated 'ROCm benchmark evidence should require AMD GPU layers'
+
+$syclBenchmark = @(
+ [pscustomobject]@{ backends = 'SYCL'; gpu_info = 'Intel Arc B580'; n_gpu_layers = 999; devices = 'auto' }
+)
+$syclEvidence = Get-LlamaBenchmarkBackendEvidence -Data $syclBenchmark -Diagnostics "SYCL Intel Arc B580`noffloaded 29/29 layers to GPU" -Backend SYCL -ExpectedDeviceName 'Intel Arc B580'
+Assert-True $syclEvidence.HardwareAccelerated 'SYCL benchmark evidence should require Intel GPU layers'
+
+$openVinoBenchmark = @(
+ [pscustomobject]@{ backends = 'OpenVINO'; gpu_info = 'GPU.0 Intel Arc B580'; n_gpu_layers = 999; devices = 'auto' }
+)
+$openVinoEvidence = Get-LlamaBenchmarkBackendEvidence -Data $openVinoBenchmark -Diagnostics "OpenVINO GPU.0 Intel Arc B580`noffloaded 29/29 layers to GPU" -Backend OpenVINO -ExpectedDeviceName 'Intel Arc B580'
+Assert-True $openVinoEvidence.HardwareAccelerated 'OpenVINO benchmark evidence should prove selected-device offload'
+
+$openClBenchmark = @(
+ [pscustomobject]@{ backends = 'OpenCL'; gpu_info = 'Qualcomm Adreno X1-85'; n_gpu_layers = 999; devices = 'auto' }
+)
+$openClEvidence = Get-LlamaBenchmarkBackendEvidence -Data $openClBenchmark -Diagnostics "OpenCL Qualcomm Adreno X1-85`noffloaded 29/29 layers to GPU" -Backend OpenCL -ExpectedDeviceName 'Qualcomm Adreno X1-85'
+Assert-True $openClEvidence.HardwareAccelerated 'OpenCL benchmark evidence should require Adreno GPU layers'
+
+$vulkanBenchmark = @(
+ [pscustomobject]@{ backends = 'Vulkan'; gpu_info = 'Generic Vulkan GPU'; n_gpu_layers = 999; devices = 'auto' }
+)
+$vulkanEvidence = Get-LlamaBenchmarkBackendEvidence -Data $vulkanBenchmark -Diagnostics "Vulkan backend initialized: Generic Vulkan GPU`noffloaded 29/29 layers to GPU" -Backend Vulkan -ExpectedDeviceName 'Generic Vulkan GPU'
+Assert-True $vulkanEvidence.HardwareAccelerated 'Vulkan fallback should explicitly prove Vulkan offload'
+$cpuEvidence = Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backends = 'CPU'; gpu_info = ''; n_gpu_layers = 999; devices = 'none' }) `
+ -Diagnostics 'CPU backend initialized' `
+ -Backend CPU `
+ -ExpectedDeviceName 'CPU'
+Assert-True (-not $cpuEvidence.HardwareAccelerated) 'Requested GPU layers should not be treated as actual offload on CPU'
+Assert-Equal $cpuEvidence.ActualOffloadedLayers 0 'CPU evidence should record zero actual offloaded layers'
+Assert-ThrowsLike {
+ Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backends = 'ROCm'; gpu_info = 'AMD Radeon'; n_gpu_layers = 999; devices = 'auto' }) `
+ -Diagnostics 'ROCm AMD Radeon' `
+ -Backend ROCm
+} '*did not prove any layers were actually offloaded*' 'Accelerator acceptance should reject requested layers without actual offload diagnostics'
+$explicitDeviceEvidence = Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backends = 'CUDA'; gpu_info = 'NVIDIA RTX 4090'; n_gpu_layers = 999; devices = 'CUDA1' }) `
+ -Diagnostics "llama_prepare_model_devices: using device CUDA1 (NVIDIA RTX 4090)`noffloaded 29/29 layers to GPU" `
+ -Backend CUDA `
+ -RequestedDevice CUDA1
+Assert-Equal $explicitDeviceEvidence.RequestedDevices[0] 'CUDA1' 'llama evidence should retain the requested runtime selector separately'
+Assert-Equal $explicitDeviceEvidence.GpuInfo[0] 'NVIDIA RTX 4090' 'llama evidence should report physical hardware from gpu_info'
+Assert-ThrowsLike {
+ Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backends = 'CUDA'; n_gpu_layers = 999; devices = 'CUDA0' }) `
+ -Diagnostics "using device CUDA0 (NVIDIA RTX 4090)`noffloaded 29/29 layers to GPU" `
+ -Backend CUDA
+} '*did not provide physical device evidence in gpu_info*' 'Accelerator evidence should require the official gpu_info field'
+Assert-ThrowsLike {
+ Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backends = 'CUDA'; gpu_info = 'NVIDIA RTX 4090'; n_gpu_layers = 999; devices = 'CUDA0' }) `
+ -Diagnostics "using device CUDA1 (NVIDIA RTX 4090)`noffloaded 29/29 layers to GPU" `
+ -Backend CUDA `
+ -RequestedDevice CUDA1
+} '*structured devices*did not match requested selector*' 'Structured benchmark selector must agree with the requested device'
+Assert-ThrowsLike {
+ Get-LlamaBenchmarkBackendEvidence `
+ -Data @([pscustomobject]@{ backend = 'CUDA'; gpu_info = 'NVIDIA RTX 4090'; n_gpu_layers = 999; devices = 'auto' }) `
+ -Diagnostics "using device CUDA0 (NVIDIA RTX 4090)`noffloaded 29/29 layers to GPU" `
+ -Backend CUDA
+} '*did not identify the selected CUDA backend*' 'llama evidence should require the official backends field'
+Assert-ThrowsLike {
+ ConvertFrom-AiPrefixedJsonArray -Text 'CUDA diagnostics without JSON'
+} '*No valid JSON array*' 'Missing benchmark JSON should fail actionably'
+$separatedBenchmark = ConvertFrom-AiJsonArrayWithDiagnostics `
+ -Json '[{"backends":"CUDA","gpu_info":"NVIDIA RTX","devices":"auto","n_gpu_layers":999}]' `
+ -Diagnostics "using device CUDA0 (NVIDIA RTX)`noffloaded 29/29 layers to GPU"
+Assert-Equal $separatedBenchmark.Data[0].backends 'CUDA' 'Separated benchmark capture should preserve clean stdout JSON'
+Assert-True ($separatedBenchmark.Diagnostics -match 'offloaded 29/29') 'Separated benchmark capture should preserve stderr diagnostics'
+
+$fakeRoot = Join-Path $env:TEMP "devconfig-llama-assets-$([guid]::NewGuid().ToString('N'))"
+$fakePayload = Join-Path $fakeRoot 'payload'
+$fakeArchive = Join-Path $fakeRoot 'llama-b99999-bin-win-cpu-x64.zip'
+$fakeRuntime = Join-Path $fakeRoot 'runtime'
+$fakeCache = Join-Path $fakeRoot 'cache'
+New-Item -ItemType Directory -Path $fakePayload -Force | Out-Null
+New-Item -ItemType File -Path (Join-Path $fakePayload 'llama-cli.exe') -Force | Out-Null
+New-Item -ItemType File -Path (Join-Path $fakePayload 'llama-bench.exe') -Force | Out-Null
+Compress-Archive -Path (Join-Path $fakePayload '*') -DestinationPath $fakeArchive
+$fakeDigest = (Get-FileHash -LiteralPath $fakeArchive -Algorithm SHA256).Hash.ToLowerInvariant()
+$script:downloadCount = 0
+function Find-GitHubReleaseAssetSet {
+ return [pscustomobject]@{
+ Release = [pscustomobject]@{ tag_name = 'b99999' }
+ Assets = @([pscustomobject]@{
+ name = 'llama-b99999-bin-win-cpu-x64.zip'
+ digest = "sha256:$fakeDigest"
+ browser_download_url = 'https://example.invalid/llama.zip'
+ size = (Get-Item -LiteralPath $fakeArchive).Length
+ })
+ }
+}
+function Invoke-WebRequest {
+ param($Uri, $Headers, $OutFile, [switch] $UseBasicParsing)
+ $script:downloadCount++
+ Copy-Item -LiteralPath $fakeArchive -Destination $OutFile
+}
+try {
+ $firstInstall = Install-VerifiedGitHubReleaseAssets `
+ -Repository 'ggml-org/llama.cpp' `
+ -AssetPatterns @('^llama-b[0-9]+-bin-win-cpu-x64\.zip$') `
+ -Destination $fakeRuntime `
+ -VersionMarker '.devconfig-version' `
+ -RequiredFile 'llama-cli.exe' `
+ -CacheDirectory $fakeCache
+ Assert-Equal $firstInstall.Action 'installed' 'First resolver install should atomically populate the runtime'
+ Assert-True (Test-Path -LiteralPath (Join-Path $fakeCache 'b99999\llama-b99999-bin-win-cpu-x64.zip')) 'Verified asset should persist in the local cache'
+ Assert-True ((Get-Content -LiteralPath (Join-Path $fakeRuntime '.devconfig-version') -Raw) -match $fakeDigest) 'Runtime marker should include the selected asset digest'
+ $secondInstall = Install-VerifiedGitHubReleaseAssets `
+ -Repository 'ggml-org/llama.cpp' `
+ -AssetPatterns @('^llama-b[0-9]+-bin-win-cpu-x64\.zip$') `
+ -Destination $fakeRuntime `
+ -VersionMarker '.devconfig-version' `
+ -RequiredFile 'llama-cli.exe' `
+ -CacheDirectory $fakeCache
+ Assert-Equal $secondInstall.Action 'already-current' 'Matching runtime marker should skip acquisition'
+ Assert-Equal $script:downloadCount 1 'Matching rerun should not download the rolling asset again'
+} finally {
+ Remove-Item -LiteralPath $fakeRoot -Recurse -Force
+}
+
+Write-Host "UNIT_OK: llama.cpp ($script:AssertionCount assertions)"
diff --git a/src/tests/local-ai/expected.txt b/src/tests/local-ai/expected.txt
new file mode 100644
index 00000000..dc020f75
--- /dev/null
+++ b/src/tests/local-ai/expected.txt
@@ -0,0 +1 @@
+PyTorch ready
diff --git a/src/tests/local-ai/unit.ps1 b/src/tests/local-ai/unit.ps1
new file mode 100644
index 00000000..fc92276a
--- /dev/null
+++ b/src/tests/local-ai/unit.ps1
@@ -0,0 +1,67 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+
+$script = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\local-ai\install.ps1') -Raw
+Assert-True ($script -match "ValidateSet\('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU'\)") 'Scenario should expose deterministic PyTorch backend selection'
+Assert-True ($script -match "ValidateSet\('None', 'LlamaCpp', 'Ollama', 'Foundry'\)") 'Scenario should keep model runtimes optional'
+Assert-True ($script -match 'collect-ai-hardware\.ps1') 'Scenario should capture hardware before acquisition'
+Assert-True ($script -match '\.\.\\_common\\collect-ai-hardware\.ps1') 'Signed scenario should resolve inventory inside the packaged Workloads tree'
+Assert-True ($script -match 'Workloads\\pytorch\\install\.ps1') 'Scenario should always provide the core PyTorch path'
+Assert-True ($script -match 'LOCAL_AI_SCENARIO_READY') 'Scenario should emit a clear readiness marker'
+Assert-True ($script -match 'LOCAL_AI_SCENARIO_PLAN_OK') 'Scenario should expose a non-mutating plan marker'
+Assert-True ($script -match 'LOCAL_AI_SCENARIO_UNSUPPORTED') 'Scenario should propagate child plan blockers instead of claiming plan success'
+
+$smoke = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\smoke.py') -Raw
+Assert-True ($smoke -match 'torch\.nn\.Sequential') 'PyTorch readiness should execute a minimal neural model'
+Assert-True ($smoke -match 'model_forward_verified') 'PyTorch report should identify the model forward pass'
+
+$coding = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\llama.cpp\coding-demo.ps1') -Raw
+Assert-True ($coding -match 'Qwen2\.5-Coder-1\.5B-Instruct') 'Optional coding demo should use the documented practical coding model'
+Assert-True ($coding -match 'CODING_DEMO_READY') 'Optional coding demo should emit a clear readiness marker'
+Assert-True ($coding -notmatch '\[string\]\s*\$Prompt') 'Coding demo should keep its validation prompt fixed'
+
+$readme = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\..\README.md') -Raw
+Assert-True ($readme -match 'Workloads\\local-ai\\install\.ps1') 'README should lead with the local AI scenario entry point'
+Assert-True ($readme -match 'bootstrap\.ps1''\s*\r?\n& \(\[scriptblock\]::Create\(\(irm \$url\)\)\) -Scenario local-ai') 'README should document the production product-level dispatcher'
+Assert-True ($readme -match '(?s)gh pr view 104.*?headRefOid') 'README should resolve the live PR head for unsigned dispatcher testing'
+Assert-True ($readme -match 'LOCAL_AI_SCENARIO_READY') 'README should document the scenario readiness marker'
+Assert-True ($readme -match 'CODING_DEMO_READY') 'README should document the optional coding-demo readiness marker'
+Assert-True ($readme -match 'replacement for PyPI/Conda') 'README should state the scenario non-goal'
+foreach ($entryPoint in @('local-ai', 'pytorch', 'cuda', 'rocm', 'intel-ai', 'llama.cpp', 'ollama', 'foundry')) {
+ Assert-True ($readme -match [regex]::Escape("| ``$entryPoint")) "README transitive-acquisition table should include $entryPoint"
+}
+
+$pytorch = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\install.ps1') -Raw
+Assert-True ($pytorch -match '\$plan\.InstallTriton.*CUDA.*XPU') 'PyTorch should gate native toolchains on supported Triton backends'
+Assert-True ($pytorch -match 'Ensure-AiVisualCppTools') 'PyTorch Triton should ensure the native MSVC toolchain'
+Assert-True ($pytorch -match 'Ensure-AiCudaToolkit') 'PyTorch CUDA Triton should ensure the standalone CUDA toolkit'
+Assert-True ($pytorch -match 'Add-AiReportAcquisition') 'PyTorch should report its transitive acquisitions'
+
+$llama = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\llama.cpp\install.ps1') -Raw
+Assert-True ($llama -notmatch 'Ensure-AiCudaToolkit') 'llama.cpp CUDA assets should not independently install the full CUDA toolkit'
+Assert-True ($llama -match 'resolvedAssets') 'llama.cpp should report paired/runtime asset acquisition'
+
+$ollama = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\ollama\install.ps1') -Raw
+Assert-True ($ollama -match 'gpuFraction') 'Ollama should report its source-managed allocation'
+$foundry = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\foundry\install.ps1') -Raw
+Assert-True ($foundry -match 'selectedExecutionProvider') 'Foundry should report its source-managed EP'
+
+$bootstrap = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\windows-dev-config\bootstrap.ps1') -Raw
+Assert-True ($bootstrap -match "ValidateSet\('', 'local-ai'\)") 'Bootstrap should expose only the supported local-ai dispatcher'
+Assert-True ($bootstrap -match 'Workloads\\local-ai\\install\.ps1') 'Bootstrap should route to the scenario without running dev-config.ps1'
+Assert-True ($bootstrap -match 'Assert-DevConfigMicrosoftSigned -Directory \$workloadsDir') 'Signed scenario payload should be Microsoft-signature verified'
+Assert-True ($bootstrap -match 'Assert-DevConfigWorkloadContent -WorkloadsRoot \$workloadsDir') 'Signed scenario should verify non-PowerShell content before copy'
+Assert-True ($bootstrap -match 'Assert-DevConfigWorkloadContent -WorkloadsRoot \(Join-Path \$scenarioRoot ''Workloads''\)') 'Installed scenario content should be reverified after protected copy'
+Assert-True ($bootstrap -match 'Assert-DevConfigProtectedTree -Directory \$workloadsDir') 'Scenario payload should be protected before copy'
+Assert-True ($bootstrap -match 'Copy-Item -LiteralPath \$workloadsDir') 'Bootstrap should copy the complete multi-file Workloads dependency tree'
+Assert-True ($bootstrap -match 'Join-Path \$setupDir ''steps''') 'Bootstrap should copy the shared Windows Dev Config helper steps'
+Assert-True ($bootstrap -match '-AiBackend.*-AiRuntime') 'Bootstrap elevation should forward scenario selection'
+Assert-True ($bootstrap -match '-PlanOnly:\$PlanOnly') 'Bootstrap elevation should forward non-mutating plan mode'
+Assert-True ($bootstrap -match "AI backend/runtime/report options require -Scenario local-ai") 'Bootstrap should reject scenario-only options without the dispatcher'
+Assert-True ($bootstrap -match "'CalmOS-Development'") 'Unsigned scenario testing should not contaminate the production CalmOS payload'
+Assert-True ($bootstrap -match 'ElevationErrorPath') 'Bootstrap should return exact verified-elevation failures to the caller'
+Assert-True ($bootstrap -match '\$bootstrapOutput = \(& \(Join-Path \$PSHOME \$shellName\) @arguments 2>&1') 'Bootstrap should capture child output before reporting a nonzero exit'
+
+Write-Host "UNIT_OK: local-ai ($script:AssertionCount assertions)"
diff --git a/src/tests/ollama/expected.txt b/src/tests/ollama/expected.txt
new file mode 100644
index 00000000..4c22966a
--- /dev/null
+++ b/src/tests/ollama/expected.txt
@@ -0,0 +1 @@
+Ollama ready
diff --git a/src/tests/ollama/probe.ps1 b/src/tests/ollama/probe.ps1
new file mode 100644
index 00000000..23a08f94
--- /dev/null
+++ b/src/tests/ollama/probe.ps1
@@ -0,0 +1,26 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+if (-not (Get-Command ollama -ErrorAction SilentlyContinue)) {
+ throw 'ollama was not found on PATH.'
+}
+& ollama --version *> $null
+if ($LASTEXITCODE -ne 0) {
+ throw "ollama --version failed with exit code $LASTEXITCODE."
+}
+$plan = Get-OllamaModelSmokePlan
+$request = New-OllamaGenerateRequest -Model $plan.Model -Marker $plan.Marker
+$response = Invoke-RestMethod `
+ -Method Post `
+ -Uri 'http://localhost:11434/api/generate' `
+ -ContentType 'application/json' `
+ -Body ($request | ConvertTo-Json -Depth 8) `
+ -TimeoutSec 300
+$result = $response.response | ConvertFrom-Json
+if ($result.marker -ne $plan.Marker) {
+ throw 'Ollama cached-model inference did not return the expected marker.'
+}
+
+Write-Output 'Ollama ready'
diff --git a/src/tests/ollama/unit.ps1 b/src/tests/ollama/unit.ps1
new file mode 100644
index 00000000..2eb2b663
--- /dev/null
+++ b/src/tests/ollama/unit.ps1
@@ -0,0 +1,213 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+$x64 = Resolve-OllamaInstallPlan -Architecture X64
+Assert-Equal $x64.PackageId 'Ollama.Ollama' 'Ollama x64 should use the current desktop package'
+Assert-Equal $x64.LaunchMode 'Desktop' 'Ollama x64 should use desktop background behavior'
+
+$arm = Resolve-OllamaInstallPlan -Architecture Arm64
+Assert-Equal $arm.Method 'GitHubRelease' 'Ollama ARM64 should use the current official release'
+Assert-Equal $arm.PackageId $null 'Ollama ARM64 should not use a WinGet portable package'
+Assert-Equal $arm.LaunchMode 'ManagedStartup' 'ARM64 archive should become a managed per-user application'
+Assert-Equal $arm.InstallType 'native-arm64-managed-archive' 'ARM64 source and install semantics should be explicit'
+$repeat = Resolve-OllamaInstallPlan -Architecture Arm64
+Assert-Equal ($repeat | ConvertTo-Json -Compress) ($arm | ConvertTo-Json -Compress) 'Ollama plan should be idempotent'
+
+$component = (Get-AiCatalogData).Components.OllamaArm64
+Assert-Equal $component.AssetPattern '^ollama-windows-arm64\.zip$' 'ARM64 should resolve the exact official native archive'
+Assert-Equal $component.SourceType 'native-arm64-managed-archive' 'ARM64 report source should not call the installation portable'
+Assert-True ($component.InstallPath -match 'Programs%?\\Ollama|Programs\\Ollama') 'ARM64 should install under the per-user Programs convention'
+Assert-True ($component.NormalChannelLimitation -match 'x64 setup EXE') 'ARM64 should explicitly reject x64 setup emulation'
+Assert-True ($component.NormalChannelLimitation -notmatch 'Portable') 'ARM64 should not rely on the portable WinGet identity'
+Assert-True (-not $component.ContainsKey('PortablePackageId')) 'ARM64 metadata should not expose a portable package fallback'
+
+$paths = Get-OllamaManagedPaths -LocalAppData 'C:\Users\Test\AppData\Local'
+Assert-Equal $paths.InstallRoot 'C:\Users\Test\AppData\Local\Programs\Ollama' 'Managed Ollama should use the stable per-user Programs path'
+Assert-Equal $paths.InstallManifest 'C:\Users\Test\AppData\Local\Programs\Ollama\.devconfig-install.json' 'Managed Ollama should persist its install manifest'
+$startupCommand = Get-OllamaStartupCommand -Executable $paths.Executable
+Assert-Equal $startupCommand '"C:\Users\Test\AppData\Local\Programs\Ollama\ollama.exe" serve' 'Startup command should invoke the managed executable'
+$pathOnce = Get-AiUpdatedPathValue -CurrentValue 'C:\Windows;C:\Tools' -Path $paths.InstallRoot -Prepend
+$pathTwice = Get-AiUpdatedPathValue -CurrentValue $pathOnce -Path $paths.InstallRoot -Prepend
+Assert-Equal $pathOnce $pathTwice 'Managed Ollama PATH insertion should be idempotent'
+Assert-True $pathOnce.StartsWith($paths.InstallRoot) 'Managed Ollama should precede stale aliases on PATH'
+
+Assert-ThrowsLike {
+ Assert-CommandAvailable -CommandName 'devconfig-command-that-does-not-exist' -Remediation 'Install the missing tool.'
+} '*Install the missing tool.*' 'Missing tools should produce actionable errors'
+
+$model = Get-OllamaModelSmokePlan
+Assert-Equal $model.Model 'qwen3:0.6b' 'Ollama should use the tested small library model'
+Assert-Equal $model.ModelBlobSha256 '7f4030143c1c477224c5434f8272c662a8b042079a0a584f0a27a1684fe2e1fa' 'Ollama model blob should be pinned'
+$request = New-OllamaGenerateRequest -Model $model.Model -Marker $model.Marker
+Assert-Equal $request.stream $false 'Ollama inference should be non-streaming'
+Assert-Equal $request.format.properties.marker.enum[0] $model.Marker 'Ollama JSON schema should constrain the marker'
+Assert-Equal $request.options.seed 42 'Ollama inference should use a fixed seed'
+$manifestPath = Get-OllamaModelManifestPath -ModelRoot 'C:\models' -Model 'qwen3:0.6b'
+Assert-Equal $manifestPath 'C:\models\manifests\registry.ollama.ai\library\qwen3\0.6b' 'Ollama digest verification should target the pulled tag manifest'
+$installScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\ollama\install.ps1') -Raw
+Assert-True ($installScript -match '\[switch\]\s*\$SkipModelSmoke') 'Ollama should expose model-smoke opt-out'
+Assert-True ($installScript -match '\[switch\]\s*\$PlanOnly') 'Ollama should expose non-mutating plan mode'
+Assert-True ($installScript -match '\[switch\]\s*\$Uninstall') 'Ollama should expose managed uninstall'
+Assert-True ($installScript -match '\[switch\]\s*\$RemoveModels') 'Ollama uninstall should make model removal explicit'
+Assert-True ($installScript -match 'Ensure-AiWingetPackage') 'Ollama x64 should use direct package acquisition'
+Assert-True ($installScript -notmatch 'apply-configuration') 'Ollama should not use winget configure'
+Assert-True ($installScript -match '/api/ps') 'Ollama report should use machine-readable VRAM allocation evidence'
+Assert-True ($installScript -match '\$inferenceEvidence = \$null') 'Ollama model-smoke opt-out should use explicit skipped evidence'
+Assert-True ($installScript -match 'Stop-OllamaManagedProcesses') 'Ollama should stop only Dev Config-managed servers before swapping the runtime'
+Assert-True ($installScript -match 'Stop-OllamaManagedProcesses -InstallRoot \$paths\.LegacyRoot') 'Managed upgrade should stop and migrate the prior Dev Config ARM64 runtime'
+Assert-True ($installScript -match '\[void\]\(Stop-OllamaManagedProcesses -InstallRoot \$paths\.InstallRoot\)') 'ARM64 validation should clean resolver-owned child processes before persistent startup'
+Assert-True ($installScript -match 'Install-VerifiedGitHubLatestAsset') 'Ollama ARM64 should use verified official release acquisition'
+Assert-True ($installScript -match '-CacheDirectory \$paths\.CacheDirectory') 'Ollama ARM64 should reuse a verified asset cache'
+Assert-True ($installScript -match 'Write-DevConfigTextFile -Path \$paths\.InstallManifest') 'Ollama ARM64 should persist tag, digest, files, and source metadata'
+Assert-True ($installScript -match 'Set-OllamaStartupRegistration') 'Ollama ARM64 should register current-user startup'
+Assert-True ($installScript -match 'Get-AiPeArchitecture') 'Ollama ARM64 should prove native executable architecture'
+Assert-True ($installScript -match 'persistentEndpoint') 'Ollama ARM64 should report the installed persistent endpoint'
+Assert-True ($installScript -match "source = 'official native ARM64 archive'") 'Managed install manifest should record its authoritative source'
+Assert-True ($installScript -match 'installedFiles = @\(') 'Managed install manifest should record installed files'
+Assert-True ($installScript -match 'Remove-UserPathEntry -Path \$paths\.LegacyRoot') 'Managed upgrade should remove the obsolete runtime path'
+$currentProcess = [pscustomobject]@{ ProcessId = 123 }
+$alternateProcess = [pscustomobject]@{ Id = 456 }
+$minimalProcess = [pscustomobject]@{}
+Assert-Equal (Get-AiProcessId -ProcessObject $currentProcess) 123 'Ollama cleanup should support CIM ProcessId'
+Assert-Equal (Get-AiProcessId -ProcessObject $alternateProcess) 456 'Ollama cleanup should support Process.Id'
+Assert-Equal (Get-AiProcessId -ProcessObject $minimalProcess) $null 'Missing process id should not throw under StrictMode'
+Assert-Equal @(Get-AiProcessIds -ProcessObjects @()).Count 0 'Empty process collection should produce an empty id list'
+Assert-Equal ((Get-AiProcessIds -ProcessObjects @($currentProcess)) -join ',') '123' 'Single process collection should project one id'
+Assert-Equal ((Get-AiProcessIds -ProcessObjects @($currentProcess, $alternateProcess, $minimalProcess)) -join ',') '123,456' 'Multiple process collection should project only usable ids'
+Assert-True ($installScript -match 'Get-AiProcessId') 'Ollama cleanup should use guarded process id extraction'
+Assert-True ($installScript -match 'Get-AiFreeTcpPort') 'Ollama ARM64 should allocate a resolver-owned API endpoint'
+Assert-True ($installScript -match '\$env:OLLAMA_HOST') 'Ollama ARM64 CLI and server should use the owned endpoint'
+Assert-True ($installScript -match 'expectedVersion') 'Ollama ARM64 should verify the managed server matches the acquired release'
+$freePort = Get-AiFreeTcpPort
+Assert-True ($freePort -gt 0 -and $freePort -le 65535) 'Free TCP port helper should return a usable loopback port'
+
+$assetRoot = Join-Path $env:TEMP "devconfig-ollama-asset-$([guid]::NewGuid().ToString('N'))"
+$payload = Join-Path $assetRoot 'payload'
+$fixtureArchive = Join-Path $assetRoot 'ollama-windows-arm64.zip'
+$destination = Join-Path $assetRoot 'managed'
+$cache = Join-Path $assetRoot 'cache'
+New-Item -ItemType Directory -Path $payload -Force | Out-Null
+$fakeExe = Join-Path $payload 'ollama.exe'
+$bytes = [byte[]]::new(256)
+$bytes[0] = 0x4D; $bytes[1] = 0x5A
+[BitConverter]::GetBytes([int]128).CopyTo($bytes, 0x3C)
+$bytes[128] = 0x50; $bytes[129] = 0x45
+[BitConverter]::GetBytes([uint16]0xAA64).CopyTo($bytes, 132)
+[IO.File]::WriteAllBytes($fakeExe, $bytes)
+Compress-Archive -Path (Join-Path $payload '*') -DestinationPath $fixtureArchive
+$script:fakeOllamaDigest = (Get-FileHash -LiteralPath $fixtureArchive -Algorithm SHA256).Hash.ToLowerInvariant()
+$script:ollamaDownloadCount = 0
+function Invoke-RestMethod {
+ return [pscustomobject]@{
+ tag_name = 'v99.0.0'
+ draft = $false
+ prerelease = $false
+ assets = @([pscustomobject]@{
+ name = 'ollama-windows-arm64.zip'
+ digest = "sha256:$script:fakeOllamaDigest"
+ browser_download_url = 'https://example.invalid/ollama-windows-arm64.zip'
+ })
+ }
+}
+function Invoke-WebRequest {
+ param($Uri, $Headers, $OutFile, [switch] $UseBasicParsing)
+ $script:ollamaDownloadCount++
+ Copy-Item -LiteralPath $script:fixtureArchive -Destination $OutFile
+}
+try {
+ $firstInstall = Install-VerifiedGitHubLatestAsset `
+ -Repository 'ollama/ollama' `
+ -AssetPattern '^ollama-windows-arm64\.zip$' `
+ -Destination $destination `
+ -VersionMarker '.devconfig-version' `
+ -RequiredFile 'ollama.exe' `
+ -CacheDirectory $cache
+ Assert-Equal $firstInstall.Action 'installed-or-upgraded' 'First managed archive application should install atomically'
+ Assert-Equal (Get-AiPeArchitecture -Path (Join-Path $destination 'ollama.exe')) 'Arm64' 'Installed official archive fixture should remain native ARM64'
+ $secondInstall = Install-VerifiedGitHubLatestAsset `
+ -Repository 'ollama/ollama' `
+ -AssetPattern '^ollama-windows-arm64\.zip$' `
+ -Destination $destination `
+ -VersionMarker '.devconfig-version' `
+ -RequiredFile 'ollama.exe' `
+ -CacheDirectory $cache
+ Assert-Equal $secondInstall.Action 'already-current' 'Matching managed archive installation should skip atomic replacement'
+ Assert-Equal $script:ollamaDownloadCount 1 'Verified archive cache should prevent repeat download'
+ Assert-True (Test-Path -LiteralPath $firstInstall.CachePath) 'Managed acquisition should report its verified archive cache'
+} finally {
+ Remove-Item -LiteralPath $assetRoot -Recurse -Force
+}
+
+$peRoot = Join-Path $env:TEMP "devconfig-pe-$([guid]::NewGuid().ToString('N'))"
+New-Item -ItemType Directory -Path $peRoot -Force | Out-Null
+try {
+ foreach ($fixture in @(
+ @{ Name = 'arm64.exe'; Machine = 0xAA64; Expected = 'Arm64' },
+ @{ Name = 'x64.exe'; Machine = 0x8664; Expected = 'X64' }
+ )) {
+ $bytes = [byte[]]::new(256)
+ $bytes[0] = 0x4D; $bytes[1] = 0x5A
+ [BitConverter]::GetBytes([int]128).CopyTo($bytes, 0x3C)
+ $bytes[128] = 0x50; $bytes[129] = 0x45
+ [BitConverter]::GetBytes([uint16]$fixture.Machine).CopyTo($bytes, 132)
+ $fixturePath = Join-Path $peRoot $fixture.Name
+ [IO.File]::WriteAllBytes($fixturePath, $bytes)
+ Assert-Equal (Get-AiPeArchitecture -Path $fixturePath) $fixture.Expected "PE architecture should identify $($fixture.Expected)"
+ }
+} finally {
+ Remove-Item -LiteralPath $peRoot -Recurse -Force
+}
+
+$uninstallRoot = Join-Path $env:TEMP "devconfig-ollama-uninstall-$([guid]::NewGuid().ToString('N'))"
+$testPaths = [pscustomobject]@{
+ InstallRoot = Join-Path $uninstallRoot 'Programs\Ollama'
+ LegacyRoot = Join-Path $uninstallRoot 'legacy'
+ CacheDirectory = Join-Path $uninstallRoot 'cache'
+ StartupRegistryPath = "HKCU:\Software\WindowsDeveloperConfigTests\$([guid]::NewGuid())"
+ StartupValueName = 'Ollama'
+}
+$models = Join-Path $uninstallRoot 'models'
+try {
+ New-Item -ItemType Directory -Path $testPaths.InstallRoot, $testPaths.LegacyRoot, $testPaths.CacheDirectory, $models -Force | Out-Null
+ Set-Content -LiteralPath (Join-Path $testPaths.InstallRoot 'ollama.exe') -Value 'runtime'
+ Set-Content -LiteralPath (Join-Path $models 'model') -Value 'preserve'
+ Set-OllamaStartupRegistration -RegistryPath $testPaths.StartupRegistryPath -ValueName $testPaths.StartupValueName -Executable (Join-Path $testPaths.InstallRoot 'ollama.exe') | Out-Null
+ $removed = Remove-OllamaManagedInstallation -Paths $testPaths -ModelRoot $models
+ Assert-True $removed.RuntimeRemoved 'Managed uninstall should remove its runtime'
+ Assert-True $removed.ModelsPreserved 'Managed uninstall should preserve models by default'
+ Assert-True (Test-Path -LiteralPath $models) 'Managed uninstall should leave model data'
+ Assert-True (-not (Get-ItemProperty -LiteralPath $testPaths.StartupRegistryPath -Name $testPaths.StartupValueName -ErrorAction SilentlyContinue)) 'Managed uninstall should remove startup registration'
+
+ New-Item -ItemType Directory -Path $testPaths.InstallRoot, $models -Force | Out-Null
+ Set-Content -LiteralPath (Join-Path $testPaths.InstallRoot 'ollama.exe') -Value 'runtime'
+ Set-Content -LiteralPath (Join-Path $models 'model') -Value 'remove'
+ $removedWithModels = Remove-OllamaManagedInstallation -Paths $testPaths -ModelRoot $models -RemoveModels
+ Assert-True (-not $removedWithModels.ModelsPreserved) 'Explicit model removal should be recorded'
+ Assert-True (-not (Test-Path -LiteralPath $models)) 'Explicit model removal should delete model data'
+} finally {
+ Remove-Item -LiteralPath $testPaths.StartupRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -LiteralPath $uninstallRoot -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+function Get-CimInstance {
+ @(
+ [pscustomobject]@{
+ ProcessId = 101
+ ExecutablePath = 'C:\Users\Test\AppData\Local\Programs\Ollama\ollama.exe'
+ CommandLine = '"C:\Users\Test\AppData\Local\Programs\Ollama\ollama.exe" serve'
+ }
+ [pscustomobject]@{
+ ProcessId = 202
+ ExecutablePath = 'C:\Program Files\Ollama\ollama.exe'
+ CommandLine = '"C:\Program Files\Ollama\ollama.exe" serve'
+ }
+ )
+}
+$managedProcesses = @(Get-OllamaManagedProcesses -InstallRoot 'C:\Users\Test\AppData\Local\Programs\Ollama')
+Assert-Equal $managedProcesses.Count 1 'Managed process discovery should not target unrelated Ollama installations'
+Assert-Equal $managedProcesses[0].ProcessId 101 'Managed process discovery should select only the Dev Config executable'
+
+Write-Host "UNIT_OK: ollama ($script:AssertionCount assertions)"
diff --git a/src/tests/pytorch/expected.txt b/src/tests/pytorch/expected.txt
new file mode 100644
index 00000000..dc020f75
--- /dev/null
+++ b/src/tests/pytorch/expected.txt
@@ -0,0 +1 @@
+PyTorch ready
diff --git a/src/tests/pytorch/probe.ps1 b/src/tests/pytorch/probe.ps1
new file mode 100644
index 00000000..6500fbc9
--- /dev/null
+++ b/src/tests/pytorch/probe.ps1
@@ -0,0 +1,23 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\..\windows-dev-config\steps\_environment.ps1')
+
+$python = Join-Path $env:LOCALAPPDATA 'DevConfig\pytorch\.venv\Scripts\python.exe'
+$statePath = Join-Path $env:LOCALAPPDATA 'DevConfig\pytorch\install-state.json'
+if (-not (Test-Path -LiteralPath $python)) {
+ throw "PyTorch environment was not found at '$python'."
+}
+if (-not (Test-Path -LiteralPath $statePath)) {
+ throw "PyTorch install state was not found at '$statePath'."
+}
+$state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json
+$deviceIndex = if ($state.PSObject.Properties['deviceIndex']) { [int]$state.deviceIndex } else { 0 }
+$result = Invoke-DevConfigNativeCommand -FilePath $python -Arguments @(
+ (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\smoke.py'), '--backend', $state.backend, '--device-index', $deviceIndex
+)
+if ($result.ExitCode -ne 0) {
+ throw "PyTorch $($state.backend) tensor probe failed with exit code $($result.ExitCode). $($result.Output)"
+}
+
+Write-Output 'PyTorch ready'
diff --git a/src/tests/pytorch/unit.ps1 b/src/tests/pytorch/unit.ps1
new file mode 100644
index 00000000..0206cb99
--- /dev/null
+++ b/src/tests/pytorch/unit.ps1
@@ -0,0 +1,284 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1')
+
+$cpu = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 -HasNvidia $false
+Assert-Equal $cpu.Backend 'CPU' 'Auto should select CPU without NVIDIA'
+Assert-Equal $cpu.IndexUrl 'https://download.pytorch.org/whl/cpu' 'CPU should use the official CPU index'
+Assert-True (-not $cpu.InstallTriton) 'CPU should not install Triton'
+
+$cuda12 = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 579 -ComputeCapability 8.9
+Assert-Equal $cuda12.Runtime 'cu126' 'Driver branches below 580 should select cu126'
+Assert-True $cuda12.InstallTriton 'Compatible CUDA x64 should install Triton'
+
+$cuda13 = Resolve-PyTorchPlan -Architecture X64 -Backend CUDA -PythonVersion 3.14 `
+ -HasNvidia $true -DriverMajor 580 -ComputeCapability 10.0
+Assert-Equal $cuda13.Runtime 'cu130' 'Driver branch 580 should select cu130'
+Assert-Equal $cuda13.TritonRequirement 'triton-windows==3.8.0.post28' 'PyTorch should pin the verified Triton build'
+
+$arm = Resolve-PyTorchPlan -Architecture Arm64 -Backend Auto -PythonVersion 3.13 -HasNvidia $false
+Assert-Equal $arm.Backend 'CPU' 'ARM64 should select the official CPU wheel'
+Assert-True (-not $arm.InstallTriton) 'ARM64 stable stack should skip Triton'
+
+$n1x = Resolve-PyTorchPlan -Architecture Arm64 -Backend Auto -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 616 -ComputeCapability 12.1
+Assert-Equal $n1x.Backend 'CUDA' 'RTX Spark N1X ARM64 should select CUDA'
+Assert-Equal $n1x.Runtime 'cu134' 'RTX Spark N1X should use CUDA 13.4 wheel'
+Assert-True ($n1x.TorchRequirement -like 'torch @ https://pypi.nvidia.com/*win_arm64.whl#sha256=*') 'N1X torch wheel should be direct, native, official, and hash pinned'
+Assert-True $n1x.InstallTriton 'Compatible ARM64 CUDA preview should run Triton verification'
+$nvidiaStableCandidate = (Get-AiCatalogData).Components.NvidiaPyTorchArm64.PromotionCandidate
+Assert-Equal $nvidiaStableCandidate.IndexUrl 'https://pypi.nvidia.com/nvtorch_oot/' 'ARM64 PyTorch should track the NVIDIA stable out-of-tree channel'
+Assert-Equal $nvidiaStableCandidate.TorchSha256 '4f781babc0e0e0722cc48d0b15107a28e6003fc2b6544f1578b6eb6f5177dcb5' 'Stable ARM64 torch candidate should retain its published hash'
+Assert-Equal $nvidiaStableCandidate.TorchvisionSha256 'e935037b6a97c32642d47f73da8cf62acf6453bfe15825314f774f62ec395d26' 'Stable ARM64 torchvision candidate should retain its published hash'
+Assert-Equal $nvidiaStableCandidate.TorchaudioSha256 'e4f18fa7359528416964d525ba620a0ca95ad231d6ab573b26c8b09c6ea8bf6b' 'Stable ARM64 torchaudio candidate should retain its published hash'
+Assert-True ($nvidiaStableCandidate.TrackingStatus -match 'awaiting N1X') 'Stable NVIDIA tuple should remain gated on N1X tensor/Triton qualification'
+
+$rocm = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 `
+ -GpuVendor AMD -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $rocm.Backend 'ROCm' 'Supported AMD hardware should select ROCm'
+Assert-Equal $rocm.TorchRequirement 'torch[device-gfx1201]==2.13.0+rocm10.0.0' 'ROCm should select the exact supported GPU package'
+Assert-Equal ($rocm.AdditionalRequirements -join ',') 'torchvision[device-gfx1201]==0.28.0+rocm10.0.0,torchaudio==2.11.0.2+rocm10.0.0' 'ROCm should install the complete official AMD Windows package tuple'
+Assert-Equal $rocm.IndexUrl 'https://stable.repo.amd.com/rocm/whl-next/' 'ROCm should use AMD official stable Windows feed'
+Assert-True (-not $rocm.InstallTriton) 'Native Windows AMD should not claim Triton support'
+Assert-Equal $rocm.Vendor 'AMD' 'ROCm plan should report AMD as selected vendor'
+Assert-Equal $rocm.DeviceName 'AMD Radeon RX 9070 XT' 'ROCm plan should retain the selected AMD device'
+Assert-Equal $rocm.AmdGfxTarget 'gfx1201' 'ROCm plan should retain the selected gfx target'
+
+$mixedFallback = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 500 -ComputeCapability 8.9 -GpuName 'NVIDIA GeForce RTX 4090' `
+ -HasAmd $true -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $mixedFallback.Backend 'ROCm' 'Auto should skip an unsupported NVIDIA driver and select supported AMD ROCm'
+Assert-Equal $mixedFallback.DeviceName 'AMD Radeon RX 9070 XT' 'Mixed-adapter fallback should report the AMD device actually selected'
+$newGpuFallback = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 579 -ComputeCapability 10.0 -GpuName 'NVIDIA next-generation GPU' `
+ -HasAmd $true -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $newGpuFallback.Backend 'ROCm' 'Auto should skip a CUDA 13-class GPU when its driver cannot support the required wheel'
+$preTuringNewDriver = Resolve-PyTorchPlan -Architecture X64 -Backend CUDA -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 580 -ComputeCapability 6.1 -GpuName 'NVIDIA GeForce GTX 1080'
+Assert-Equal $preTuringNewDriver.Runtime 'cu126' 'Pre-Turing GPUs should retain CUDA 12.6 wheels even with a branch 580 driver'
+
+$xpu = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 -GpuVendor Intel -GpuName 'Intel(R) Arc(TM) B580 Graphics'
+Assert-Equal $xpu.Backend 'XPU' 'Intel GPU should select XPU'
+Assert-Equal $xpu.TorchRequirement 'torch==2.14.0+xpu' 'XPU should use the official stable wheel'
+Assert-Equal ($xpu.AdditionalRequirements -join ',') 'torchvision==0.29.0+xpu' 'XPU should install its official torchvision tuple'
+Assert-Equal $xpu.IndexUrl 'https://download.pytorch.org/whl/xpu' 'XPU should use the official PyTorch XPU index'
+Assert-Equal $xpu.TritonRequirement 'triton-xpu==3.8.0' 'XPU should use PyTorch-managed Triton XPU'
+Assert-Equal $xpu.Vendor 'Intel' 'XPU plan should report Intel as selected vendor'
+Assert-Equal $xpu.DeviceName 'Intel(R) Arc(TM) B580 Graphics' 'XPU plan should retain the selected Intel device'
+
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture X64 -Backend CUDA -PythonVersion 3.13 -HasNvidia $false
+} '*nvidia-smi did not report*' 'Explicit CUDA should fail without usable hardware'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture X64 -Backend CUDA -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 580 -ComputeCapability 3.5 -GpuName 'NVIDIA Kepler GPU'
+} '*below the supported Windows CUDA wheel minimum*' 'Explicit CUDA should reject GPUs below the supported compute capability'
+
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture Arm64 -Backend Auto -PythonVersion 3.13 `
+ -HasNvidia $true -DriverMajor 615 -ComputeCapability 12.1
+} '*Use -Backend CPU to explicitly accept CPU-only*' 'ARM64 NVIDIA auto mode should never silently fall back to CPU'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture Arm64 -Backend ROCm -PythonVersion 3.13 -GpuVendor AMD -AmdGfxTarget gfx1201
+} '*not published for native Windows ARM64*' 'ROCm should reject Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture Arm64 -Backend XPU -PythonVersion 3.13 -GpuVendor Intel -IntelGpuName 'Intel Arc B580'
+} '*not published for native Windows ARM64*' 'XPU should reject Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture Arm64 -Backend Auto -PythonVersion 3.13 -HasAmd $true -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+} '*ROCm PyTorch is not published*Use -Backend CPU*' 'Auto should reject AMD acceleration on Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture Arm64 -Backend Auto -PythonVersion 3.13 -HasIntel $true -IntelGpuName 'Intel Arc B580'
+} '*XPU PyTorch is not published*Use -Backend CPU*' 'Auto should reject Intel acceleration on Windows ARM64'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 -GpuVendor AMD -AmdGpuName 'Unsupported Radeon'
+} '*not in the ROCm 10.0 Windows support matrix*' 'Unsupported AMD hardware should not silently fall back to CPU'
+Assert-ThrowsLike {
+ Resolve-PyTorchPlan -Architecture X64 -Backend XPU -PythonVersion 3.13 -GpuVendor Intel -GpuName 'Intel HD Graphics 4000'
+} '*not in the validated Windows PyTorch XPU families*' 'Unsupported Intel hardware should fail explicitly'
+Assert-True (Test-IntelXpuGpuSupported -GpuName 'Intel(R) Arc(TM) 140V GPU') 'Intel Arc 140V should be accepted for XPU'
+Assert-True (Test-IntelXpuGpuSupported -GpuName 'Intel(R) Arc(TM) 130V GPU') 'Intel Arc 130V should be accepted for XPU'
+Assert-Equal (Select-IntelGpuName -GpuNames @('Intel(R) HD Graphics 4000', 'Intel(R) Arc(TM) B580 Graphics')) 'Intel(R) Arc(TM) B580 Graphics' 'Intel selection should prefer a supported adapter regardless of enumeration order'
+$mixedXpu = Resolve-PyTorchPlan -Architecture X64 -Backend XPU -PythonVersion 3.13 `
+ -GpuVendor NVIDIA -HasNvidia $true -HasIntel $true -GpuName 'Intel(R) Arc(TM) B580 Graphics'
+Assert-Equal $mixedXpu.Backend 'XPU' 'Explicit XPU should select a supported secondary Intel GPU'
+$mixedRocm = Resolve-PyTorchPlan -Architecture X64 -Backend ROCm -PythonVersion 3.13 `
+ -GpuVendor NVIDIA -HasNvidia $true -HasAmd $true -GpuName 'NVIDIA GeForce RTX 4090' `
+ -AmdGpuName 'AMD Radeon RX 9070 XT' -AmdGfxTarget gfx1201
+Assert-Equal $mixedRocm.Backend 'ROCm' 'Explicit ROCm should select a supported secondary AMD GPU'
+Assert-Equal $mixedRocm.DeviceName 'AMD Radeon RX 9070 XT' 'Explicit ROCm should report the selected secondary AMD GPU'
+$autoIntelFallback = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 `
+ -GpuVendor AMD -HasAmd $true -AmdGpuName 'Unsupported Radeon' -HasIntel $true -IntelGpuName 'Intel(R) Arc(TM) B580 Graphics'
+Assert-Equal $autoIntelFallback.Backend 'XPU' 'Auto should skip unsupported AMD hardware and select a supported Intel accelerator'
+
+$skippedXpuTriton = Resolve-PyTorchPlan -Architecture X64 -Backend XPU -PythonVersion 3.13 `
+ -HasIntel $true -IntelGpuName 'Intel(R) Arc(TM) B580 Graphics' -SkipTriton
+Assert-True (-not $skippedXpuTriton.InstallTriton) 'XPU SkipTriton should disable Triton'
+Assert-True ($skippedXpuTriton.TritonReason -match 'disabled by the caller') 'XPU SkipTriton should report disabled rather than unsupported'
+
+Assert-PythonArchitecture -Architecture X64 -PythonMachine AMD64
+Assert-PythonArchitecture -Architecture Arm64 -PythonMachine aarch64
+Assert-ThrowsLike {
+ Assert-PythonArchitecture -Architecture Arm64 -PythonMachine AMD64
+} '*does not match Windows architecture*' 'Emulated or conflicting Python should fail before wheel installation'
+
+$arguments = Get-PipInstallArguments -Requirement 'torch==2.14.0+cpu' `
+ -IndexUrl 'https://download.pytorch.org/whl/cpu' -DryRun
+Assert-Equal ($arguments -join ' ') '-m pip install --dry-run --only-binary=:all: torch==2.14.0+cpu --index-url https://download.pytorch.org/whl/cpu' 'pip command should pin the exact CPU build on the official index'
+Assert-Equal $cuda12.TorchRequirement 'torch==2.14.0+cu126' 'CUDA 12 repair should require the exact backend build'
+Assert-Equal $cuda13.TorchRequirement 'torch==2.14.0+cu130' 'CUDA 13 repair should require the exact backend build'
+
+$n1xArguments = Get-PipInstallArguments -Requirement $n1x.TorchRequirement -DryRun
+Assert-True (($n1xArguments -join ' ') -notlike '*--index-url*') 'Direct N1X torch wheel should leave dependency resolution on the configured default index'
+Assert-True (($n1xArguments -join ' ') -like '*af0872854d183cb6894dbd5b1e5e9291875ce139d138b5fc0b501498828265d3*') 'N1X torch command should preserve the wheel hash'
+$localWheelArguments = Get-PipLocalWheelInstallArguments -WheelPath 'C:\cache\torch.whl'
+Assert-Equal ($localWheelArguments -join ' ') '-m pip install --only-binary=:all: C:\cache\torch.whl' 'Verified direct wheel should install from one local cached artifact'
+
+$matchingState = [ordered]@{
+ architecture = 'Arm64'
+ backend = 'CUDA'
+ torch = $n1x.TorchRequirement
+ torchVersion = $n1x.TorchVersion
+ index = $n1x.IndexUrl
+ triton = $n1x.TritonRequirement
+ tritonVersion = $n1x.TritonVersion
+ numpy = $n1x.NumpyRequirement
+ numpyVersion = $n1x.NumpyVersion
+ additionalRequirements = @()
+ python = '3.13'
+} | ConvertTo-Json -Compress
+$matchingVersions = [pscustomobject]@{
+ torch = '2.15.0.dev20260904+cu134'
+ numpy = '2.5.2'
+ triton = '3.8.0.post28'
+ torchvision = $null
+ torchaudio = $null
+}
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $matchingState -CurrentStateJson $matchingState -InstalledVersions $matchingVersions) 'VerifyOnly' 'Matching rerun should skip package resolution and installation'
+$deviceSelectionState = $matchingState.TrimEnd('}') + ',"deviceIndex":1,"selectedDevice":"NVIDIA secondary GPU"}'
+Assert-True (Test-PyTorchStateCompatible -DesiredStateJson $deviceSelectionState -CurrentStateJson $matchingState) 'Changing only the selected adapter should not reinstall the PyTorch package environment'
+$legacyState = [ordered]@{
+ architecture = 'Arm64'
+ backend = 'CUDA'
+ torch = $n1x.TorchRequirement
+ index = $n1x.IndexUrl
+ triton = 'triton-windows>=3.8,<3.9'
+ python = '3.13'
+} | ConvertTo-Json -Compress
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $matchingState -CurrentStateJson $legacyState -InstalledVersions $matchingVersions) 'VerifyOnly' 'Compatible legacy state with exact installed versions should migrate without downloading packages'
+$legacyCpuDesired = [ordered]@{
+ architecture = 'X64'; backend = 'CPU'; torch = 'torch==2.14.0+cpu'; torchVersion = '2.14.0+cpu'
+ index = 'https://download.pytorch.org/whl/cpu'; triton = $null; tritonVersion = $null
+ numpy = 'numpy==2.5.2'; numpyVersion = '2.5.2'; python = '3.13'
+} | ConvertTo-Json -Compress
+$legacyCpuState = [ordered]@{
+ architecture = 'X64'; backend = 'CPU'; torch = 'torch==2.14.0'
+ index = 'https://download.pytorch.org/whl/cpu'; triton = $null; python = '3.13'
+} | ConvertTo-Json -Compress
+$legacyCpuVersions = [pscustomobject]@{ torch = '2.14.0+cpu'; numpy = '2.5.2'; triton = $null; torchvision = $null; torchaudio = $null }
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $legacyCpuDesired -CurrentStateJson $legacyCpuState -InstalledVersions $legacyCpuVersions) 'VerifyOnly' 'Legacy stable CPU state should migrate without downloading'
+$wrongTorch = [pscustomobject]@{ torch = '2.14.0+cpu'; numpy = '2.5.2'; triton = '3.8.0.post28'; torchvision = $null; torchaudio = $null }
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $matchingState -CurrentStateJson $matchingState -InstalledVersions $wrongTorch) 'Install' 'Mismatched installed torch should repair the environment'
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $matchingState -CurrentStateJson $null -InstalledVersions $matchingVersions) 'Install' 'Missing state should not skip package installation'
+$wrongBackendState = $legacyState -replace '"backend":"CUDA"', '"backend":"CPU"'
+Assert-True (-not (Test-PyTorchStateCompatible -DesiredStateJson $matchingState -CurrentStateJson $wrongBackendState)) 'Backend plan changes should recreate the environment'
+$noTritonState = $matchingState -replace '"triton":"triton-windows==3.8.0.post28","tritonVersion":"3.8.0.post28"', '"triton":null,"tritonVersion":null'
+Assert-True (-not (Test-PyTorchStateCompatible -DesiredStateJson $noTritonState -CurrentStateJson $matchingState)) 'Disabling Triton should recreate an environment that still records Triton'
+Assert-True (Test-PyTorchEnvironmentRequiresRecreation -DesiredStateJson $noTritonState -CurrentStateJson $noTritonState -InstalledVersions $matchingVersions) 'Unexpected installed Triton should recreate the environment instead of repeating pip work'
+$rocmState = [ordered]@{
+ architecture = 'X64'; backend = 'ROCm'; torch = $rocm.TorchRequirement; torchVersion = $rocm.TorchVersion
+ index = $rocm.IndexUrl; triton = $null; tritonVersion = $null; numpy = $rocm.NumpyRequirement
+ numpyVersion = $rocm.NumpyVersion; additionalRequirements = @($rocm.AdditionalRequirements); python = '3.13'
+} | ConvertTo-Json -Compress
+$rocmVersionsMissingVision = [pscustomobject]@{
+ torch = $rocm.TorchVersion; numpy = $rocm.NumpyVersion; triton = $null; torchvision = $null; torchaudio = '2.11.0.2+rocm10.0.0'
+}
+Assert-Equal (Get-PyTorchPackageAction -DesiredStateJson $rocmState -CurrentStateJson $rocmState -InstalledVersions $rocmVersionsMissingVision) 'Install' 'ROCm rerun should repair missing additional packages'
+
+$installScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\install.ps1') -Raw
+Assert-True ($installScript -match 'Get-PipInstallArguments -Requirement \$plan\.NumpyRequirement') 'PyTorch environment should include pinned NumPy'
+Assert-True ($installScript -like "*if (`$packageAction -eq 'VerifyOnly')*") 'PyTorch should branch around package work on a matching rerun'
+Assert-True ($installScript -match 'Install-VerifiedDownload') 'Fresh direct-wheel install should use the verified download cache'
+Assert-True ($installScript -match 'Get-PipLocalWheelInstallArguments') 'Fresh direct-wheel install should install the one cached wheel'
+Assert-True ($installScript -match 'tensor smoke failed') 'Matching rerun should still execute the tensor readiness probe'
+Assert-True ($installScript -match 'GPU kernel smoke failed') 'Matching rerun should still execute the Triton readiness probe'
+Assert-True ($installScript -notmatch '\$LASTEXITCODE') 'PyTorch should not depend on inherited LASTEXITCODE state'
+$probeScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'probe.ps1') -Raw
+Assert-True ($probeScript -notmatch '\$LASTEXITCODE') 'PyTorch probe should not depend on inherited LASTEXITCODE state'
+Assert-True ($probeScript -match 'Invoke-DevConfigNativeCommand') 'PyTorch probe should use guarded native execution'
+Assert-True ($probeScript -match 'DeviceIndex') 'PyTorch probe should reuse the selected adapter index'
+Assert-True ($installScript -match 'deviceIndex = \$DeviceIndex') 'PyTorch should persist the selected adapter independently of the optional report path'
+Assert-True ($probeScript -match '\$state\.deviceIndex') 'PyTorch probe should read the durable selected adapter state'
+Assert-True ($installScript -match 'Get-Python313Path') 'PyTorch should select the installed Python 3.13 explicitly'
+Assert-True ($installScript -match 'Import-MsvcEnvironment') 'Triton path should import the architecture-native MSVC build environment'
+Assert-True ((Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-support.ps1') -Raw) -like '*PATH=$vsInstaller;%PATH%*') 'Triton compiler environment should put vswhere.exe on PATH before VsDevCmd runs'
+Assert-True ($installScript -match 'Ensure-AiVisualCppTools') 'Triton should acquire its JIT compiler through direct shared setup'
+Assert-True ($installScript -match 'Ensure-AiCudaToolkit') 'CUDA Triton should acquire its toolkit through direct shared setup'
+Assert-True ($installScript -notmatch 'apply-configuration') 'PyTorch should not use winget configure'
+Assert-True ($installScript -match "'ROCm', 'XPU'") 'PyTorch should expose AMD ROCm and Intel XPU backends'
+Assert-True ($installScript -match '\[int\]\s*\$DeviceIndex') 'PyTorch should expose same-vendor adapter selection'
+Assert-True ($installScript -match 'Auto supports only -DeviceIndex 0') 'PyTorch should reject ambiguous nonzero adapter indexes in Auto mode'
+Assert-True ($installScript.Contains("'--device-index', `$DeviceIndex")) 'PyTorch should pass the selected adapter index to tensor and Triton probes'
+Assert-True ($installScript.Contains("`$Backend -in @('Auto', 'ROCm')")) 'PyTorch Auto should resolve AMD hardware at the requested adapter index'
+Assert-True ($installScript.Contains("`$Backend -in @('Auto', 'XPU')")) 'PyTorch Auto should resolve Intel hardware at the requested adapter index'
+Assert-True ($installScript.Contains("`$Backend -in @('Auto', 'CUDA')")) 'PyTorch Auto should resolve NVIDIA hardware at the requested adapter index'
+Assert-True ($installScript -match 'xpu-smoke\.py') 'PyTorch XPU should execute torch.compile/Triton acceptance'
+Assert-True ($installScript -notmatch 'Intel\.OneAPI\.Toolkit') 'PyTorch XPU should not install the full oneAPI toolkit'
+Assert-True ($installScript -notmatch "Workloads\\rocm|rocm\\install\.ps1") 'PyTorch ROCm should not require the standalone native HIP SDK flow'
+Assert-True ($installScript -match 'amdGfxTarget = \$plan\.AmdGfxTarget') 'PyTorch reports should include the resolved AMD gfx target'
+Assert-True ($installScript -match 'runtimePackageTuple') 'PyTorch reports should include the exact runtime package tuple'
+Assert-True ($installScript -match 'selectedVendor = \$plan\.Vendor') 'PyTorch reports should include the selected hardware vendor'
+
+$smokeScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\smoke.py') -Raw
+Assert-True ($smokeScript -match 'ROCm/HIP') 'ROCm failure text should identify the AMD stack instead of CUDA'
+Assert-True ($smokeScript -match 'torch\.version\.hip is null') 'ROCm smoke should require a non-null HIP runtime'
+Assert-True ($smokeScript -match 'torch\.cuda\.get_device_name') 'ROCm smoke should report the actual AMD device through the HIP-compatible API'
+Assert-True ($smokeScript -match 'device-index') 'PyTorch tensor smoke should target an explicit adapter index'
+$xpuSmokeScript = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\pytorch\xpu-smoke.py') -Raw
+Assert-True ($xpuSmokeScript -match 'torch\.xpu\.is_available') 'XPU smoke should require an actual Intel XPU device'
+Assert-True ($xpuSmokeScript -match 'torch\.xpu\.get_device_name') 'XPU smoke should report the actual Intel device'
+Assert-True ($xpuSmokeScript -match 'torch\.compile') 'XPU smoke should execute PyTorch compile through Triton XPU'
+Assert-True ($xpuSmokeScript -match 'torch\.xpu\.set_device') 'XPU compile smoke should target the requested Intel adapter'
+
+$rocmSmoke = ConvertFrom-AiKeyedJsonLine -Prefix 'PYTORCH_SMOKE=' -Text 'PYTORCH_SMOKE={"backend":"ROCm","vendor":"AMD","device":"AMD Radeon RX 9070 XT","torch_hip_runtime":"7.15","tensor_operation_verified":true}'
+Assert-Equal $rocmSmoke.backend 'ROCm' 'ROCm smoke record should identify its backend'
+Assert-Equal $rocmSmoke.vendor 'AMD' 'ROCm smoke record should identify AMD'
+Assert-True ([bool]$rocmSmoke.torch_hip_runtime) 'ROCm smoke record should contain the HIP runtime'
+$xpuCompile = ConvertFrom-AiKeyedJsonLine -Prefix 'TRITON_XPU_READY=' -Text 'TRITON_XPU_READY={"backend":"XPU","vendor":"Intel","device":"Intel Arc B580","triton_xpu":"3.8.0","torch_compile_executed":true}'
+Assert-Equal $xpuCompile.backend 'XPU' 'XPU compile record should identify its backend'
+Assert-True $xpuCompile.torch_compile_executed 'XPU compile record should prove torch.compile execution'
+
+$catalog = Get-AiCatalogData
+Assert-True (-not $catalog.Components.PyTorchRocm.NativeToolkitRequired) 'PyTorch ROCm should not require the standalone ROCm SDK'
+Assert-True ($catalog.Components.PyTorchRocm.NativeToolkitRelationship -match 'standalone rocm flow is not a prerequisite') 'ROCm metadata should explain the native SDK separation'
+Assert-True (-not $catalog.Components.PyTorchXpu.NativeToolkitRequired) 'PyTorch XPU should not require full oneAPI'
+Assert-True ($catalog.Components.PyTorchXpu.NativeToolkitRelationship -match 'does not install full oneAPI') 'XPU metadata should explain the oneAPI separation'
+
+$readme = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\..\README.md') -Raw
+Assert-True ($readme -match 'pytorch-rocm-report\.json') 'README should provide the AMD partner report command'
+Assert-True ($readme -match 'pytorch-xpu-report\.json') 'README should provide the Intel partner report command'
+
+$fakeVs = Join-Path $env:TEMP "devconfig-vs-test-$([guid]::NewGuid().ToString('N'))"
+$fakeToolset = Join-Path $fakeVs 'VC\Tools\MSVC\14.99.0\bin\Hostarm64\arm64'
+$fakeVsDevCmd = Join-Path $fakeVs 'Common7\Tools\VsDevCmd.bat'
+New-Item -ItemType Directory -Path $fakeToolset -Force | Out-Null
+New-Item -ItemType Directory -Path (Split-Path -Parent $fakeVsDevCmd) -Force | Out-Null
+New-Item -ItemType File -Path (Join-Path $fakeToolset 'cl.exe') -Force | Out-Null
+New-Item -ItemType File -Path $fakeVsDevCmd -Force | Out-Null
+try {
+ $resolvedVsDevCmd = Resolve-VsDevCmdPath -InstallationPaths @('', $fakeVs) -Architecture Arm64
+ Assert-Equal $resolvedVsDevCmd $fakeVsDevCmd 'VS discovery should skip empty output and select a Build Tools instance with ARM64 cl.exe'
+ Assert-ThrowsLike {
+ Resolve-VsDevCmdPath -InstallationPaths @() -Architecture Arm64
+ } '*No Visual Studio Build Tools installation with an Arm64 MSVC compiler*' 'Empty vswhere output should produce an actionable error instead of a null dereference'
+} finally {
+ Remove-Item -LiteralPath $fakeVs -Recurse -Force -ErrorAction SilentlyContinue
+}
+
+$repeat = Resolve-PyTorchPlan -Architecture X64 -Backend Auto -PythonVersion 3.13 -HasNvidia $false
+Assert-Equal ($repeat | ConvertTo-Json -Compress) ($cpu | ConvertTo-Json -Compress) 'Plan resolution should be idempotent'
+
+Write-Host "UNIT_OK: pytorch ($script:AssertionCount assertions)"
diff --git a/src/tests/rocm/expected.txt b/src/tests/rocm/expected.txt
new file mode 100644
index 00000000..7b88bb53
--- /dev/null
+++ b/src/tests/rocm/expected.txt
@@ -0,0 +1 @@
+ROCm ready
diff --git a/src/tests/rocm/probe.ps1 b/src/tests/rocm/probe.ps1
new file mode 100644
index 00000000..9df66f17
--- /dev/null
+++ b/src/tests/rocm/probe.ps1
@@ -0,0 +1,25 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$hipcc = Join-Path $env:LOCALAPPDATA 'DevConfig\rocm\.venv\Scripts\hipcc.exe'
+if (-not (Test-Path -LiteralPath $hipcc)) { throw "hipcc not found at '$hipcc'." }
+$reportPath = Join-Path $env:LOCALAPPDATA 'DevConfig\reports\rocm-latest.json'
+$deviceIndex = 0
+if (Test-Path -LiteralPath $reportPath) {
+ $request = (Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json).request
+ if ($request.PSObject.Properties['DeviceIndex']) { $deviceIndex = [int]$request.DeviceIndex }
+}
+$temporary = Join-Path ([System.IO.Path]::GetTempPath()) "devconfig-hip-probe-$([guid]::NewGuid().ToString('N'))"
+New-Item -ItemType Directory -Path $temporary -Force | Out-Null
+try {
+ $executable = Join-Path $temporary 'hip-smoke.exe'
+ & $hipcc (Join-Path $PSScriptRoot '..\..\Workloads\rocm\hip-smoke.cpp') -O2 -o $executable
+ if ($LASTEXITCODE -ne 0) { throw "HIP compile failed with exit code $LASTEXITCODE." }
+ $output = (& $executable $deviceIndex 2>&1 | Out-String).Trim()
+ if ($LASTEXITCODE -ne 0 -or $output -notmatch '^HIP_KERNEL_READY') {
+ throw "HIP kernel failed: $output"
+ }
+} finally {
+ Remove-Item -LiteralPath $temporary -Recurse -Force -ErrorAction SilentlyContinue
+}
+Write-Output 'ROCm ready'
diff --git a/src/tests/rocm/unit.ps1 b/src/tests/rocm/unit.ps1
new file mode 100644
index 00000000..c9edbbe8
--- /dev/null
+++ b/src/tests/rocm/unit.ps1
@@ -0,0 +1,56 @@
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\_harness\assertions.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\direct-setup.ps1')
+. (Join-Path $PSScriptRoot '..\..\Workloads\_common\ai-report.ps1')
+
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon RX 9070 XT') 'gfx1201' 'RX 9070 XT should map to gfx1201'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon RX 7900 XTX') 'gfx1100' 'RX 7900 XTX should map to gfx1100'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon RX 7600') 'gfx1102' 'RX 7600 should map to gfx1102'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon RX 7650 GRE') 'gfx1102' 'RX 7650 GRE should map to gfx1102'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon 890M Graphics') 'gfx1150' 'Radeon 890M should map to its Ryzen AI gfx target'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon 8060S Graphics') 'gfx1151' 'Radeon 8060S should map to its Ryzen AI Max gfx target'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'AMD Radeon(TM) 890M Graphics') 'gfx1150' 'Trademark tokens should not break Radeon 890M matching'
+Assert-Equal (Select-AmdGpuName -GpuNames @('AMD Radeon Vega 8', 'AMD Radeon RX 9070 XT')) 'AMD Radeon RX 9070 XT' 'AMD selection should prefer a supported adapter regardless of enumeration order'
+Assert-Equal (Get-AmdGfxTarget -GpuName 'Unsupported AMD GPU') $null 'Unsupported AMD GPU should not infer compatibility'
+$plan = Resolve-RocmInstallPlan -Architecture X64 -GpuName 'AMD Radeon RX 9070 XT'
+Assert-Equal $plan.Requirement 'rocm[libraries,devel,device-gfx1201]==10.0.0' 'ROCm plan should construct the exact device package'
+Assert-ThrowsLike {
+ Resolve-RocmInstallPlan -Architecture Arm64 -GpuName 'AMD Radeon RX 9070 XT'
+} '*does not publish native Windows ARM64*' 'ROCm should reject ARM64'
+
+$catalog = Get-AiCatalog
+Assert-Equal $catalog.Components.AmdRocm.Architectures[0] 'X64' 'ROCm should be Windows x64 only'
+Assert-Equal $catalog.Components.AmdRocm.Version '10.0.0' 'ROCm should pin the production tuple'
+Assert-True ($catalog.Components.AmdRocm.IndexUrl -like 'https://stable.repo.amd.com/*') 'ROCm should use the official stable AMD feed'
+
+$wingetArgs = Get-DevConfigWingetInstallArguments -Id 'Python.Python.3.13'
+Assert-Equal ($wingetArgs -join ' ') 'install --id Python.Python.3.13 --exact --source winget --silent --accept-package-agreements --accept-source-agreements --disable-interactivity' 'Direct package command should be exact and noninteractive'
+
+$script = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\rocm\install.ps1') -Raw
+Assert-True ($script -match '\[int\]\s*\$DeviceIndex') 'ROCm should expose same-vendor adapter selection'
+Assert-True ($script -match 'Test-AiDeviceNameMatch') 'ROCm should verify that the executed HIP device matches the resolved gfx package'
+$hipSmoke = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\..\Workloads\rocm\hip-smoke.cpp') -Raw
+Assert-True ($hipSmoke -match 'hipSetDevice\(device_index\)') 'HIP kernel should execute on the requested AMD adapter'
+$probe = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'probe.ps1') -Raw
+Assert-True ($probe -match 'DeviceIndex') 'ROCm verification probe should reuse the selected adapter'
+
+function Get-CimInstance {
+ return @(
+ [pscustomobject]@{ Name = 'AMD Unsupported iGPU'; PNPDeviceID = 'PCI\VEN_1002&DEV_0001' },
+ [pscustomobject]@{ Name = 'AMD Radeon RX 9070 XT'; PNPDeviceID = 'PCI\VEN_1002&DEV_0002' }
+ )
+}
+Assert-Equal (Get-AmdGpuName -DeviceIndex 0) 'AMD Unsupported iGPU' 'ROCm indexed lookup should preserve exact adapter zero'
+Assert-Equal (Get-AmdGpuName -DeviceIndex 1) 'AMD Radeon RX 9070 XT' 'ROCm indexed lookup should preserve exact adapter one'
+Assert-True ($script -match '\[switch\]\s*\$PlanOnly') 'ROCm should support portable plan mode'
+Assert-True ($script -match 'hip-smoke\.cpp') 'ROCm should compile a real HIP kernel'
+Assert-True ($script -notmatch 'apply-configuration') 'ROCm should use direct acquisition'
+Assert-True ($script -notmatch '''--only-binary=:all:'', ''--index-url'', \$component\.IndexUrl, \$requirement') 'ROCm should allow AMD source-only metapackage while its dependencies remain wheels'
+Assert-True ($script -match 'rocm-sdk-device-\$gfx') 'ROCm rerun should verify the exact selected device package'
+Assert-True ($script -match 'Test-PythonDistributionVersions') 'ROCm rerun should verify exact SDK package versions'
+Assert-True ($script -match 'Ensure-AiVisualCppTools') 'ROCm should acquire the Windows host compiler and SDK'
+Assert-True ($script -match 'Import-MsvcEnvironment') 'ROCm should initialize the host compiler environment before hipcc'
+
+Write-Host "UNIT_OK: rocm ($script:AssertionCount assertions)"
diff --git a/src/tools/collect-ai-hardware.ps1 b/src/tools/collect-ai-hardware.ps1
new file mode 100644
index 00000000..2e99e38e
--- /dev/null
+++ b/src/tools/collect-ai-hardware.ps1
@@ -0,0 +1,14 @@
+<#
+.SYNOPSIS
+ Invoke the signed-package-compatible AI hardware inventory helper.
+#>
+[CmdletBinding()]
+param(
+ [string] $OutputPath = (Join-Path $env:LOCALAPPDATA 'DevConfig\reports\hardware-latest.json')
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+& (Join-Path $PSScriptRoot '..\Workloads\_common\collect-ai-hardware.ps1') `
+ -OutputPath $OutputPath
diff --git a/src/tools/get-ai-capabilities.ps1 b/src/tools/get-ai-capabilities.ps1
new file mode 100644
index 00000000..580dfd4b
--- /dev/null
+++ b/src/tools/get-ai-capabilities.ps1
@@ -0,0 +1,26 @@
+[CmdletBinding()]
+param([string] $OutputPath = '')
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+. (Join-Path $PSScriptRoot '..\Workloads\_common\ai-support.ps1')
+
+$document = [ordered]@{
+ schemaVersion = 1
+ generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
+ capabilities = @(Get-AiCapabilityMatrix)
+}
+$json = $document | ConvertTo-Json -Depth 20
+if (-not $OutputPath) {
+ $json
+ return
+}
+
+$resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
+$parent = Split-Path -Parent $resolvedPath
+if ($parent) {
+ New-Item -ItemType Directory -Path $parent -Force | Out-Null
+}
+[System.IO.File]::WriteAllText($resolvedPath, $json, [System.Text.UTF8Encoding]::new($false))
+Write-Host "AI_CAPABILITY_REPORT: $resolvedPath"
diff --git a/src/windows-dev-config/README.md b/src/windows-dev-config/README.md
index a2f9959f..e561f2eb 100644
--- a/src/windows-dev-config/README.md
+++ b/src/windows-dev-config/README.md
@@ -12,6 +12,7 @@ It is **idempotent** — every change is checked before it's made, so re-running
- [Quick start](#quick-start)
- [Setup actions](#setup-actions)
+- [Local AI scenario](#local-ai-scenario)
- [What to expect](#what-to-expect)
- [Requirements](#requirements)
- [Before you run this](#before-you-run-this)
@@ -99,6 +100,40 @@ repository-root release files, not `src/`. All three require `| iex` to execute.
The wrappers accept no setup options and verify the Microsoft signature of the
downloaded `bootstrap.ps1` before running it with the fixed action.
+## Local AI scenario
+
+`-Scenario local-ai` is a separate product-level entry point. It does not run
+the Full or Partial workstation setup. It detects AI hardware, installs the
+matching contained PyTorch backend and compatible Triton when available, then
+executes tensor and neural-network acceptance:
+
+```powershell
+$url = 'https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/main/src/windows-dev-config/bootstrap.ps1'
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai
+# Expected: PYTORCH_READY ... then LOCAL_AI_SCENARIO_READY
+```
+
+Optional model runtimes are selected explicitly:
+
+```powershell
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai -AiRuntime LlamaCpp
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai -AiRuntime Ollama
+& ([scriptblock]::Create((irm $url))) -Scenario local-ai -AiRuntime Foundry
+```
+
+Use `-PlanOnly -ReportRoot ` to inspect hardware, selected backend,
+transitive acquisitions, and blockers without installing. `-AiBackend` accepts
+`Auto`, `CPU`, `CUDA`, `ROCm`, or `XPU`; `-RequireTriton` makes compatible
+Triton execution mandatory. Drivers remain prerequisites and are not replaced.
+
+Bootstrap downloads the complete scenario dependency tree, verifies every
+Microsoft-signed PowerShell file and the signed hash manifest for non-PowerShell
+inputs, copies the payload into an administrator-protected scenario directory,
+reverifies it, and launches only `Workloads\local-ai\install.ps1`.
+Explicit `-AllowUnsigned` scenario tests use
+`%ProgramData%\CalmOS-Development`, keeping unsigned files out of the production
+`%ProgramData%\CalmOS` tree.
+
## What to expect
Roughly **30 minutes** on a clean machine with a good connection, most of it spent downloading Visual Studio Code, the .NET SDK, PowerToys, and Ubuntu.
diff --git a/src/windows-dev-config/bootstrap.ps1 b/src/windows-dev-config/bootstrap.ps1
index 07853147..0cd58fd3 100644
--- a/src/windows-dev-config/bootstrap.ps1
+++ b/src/windows-dev-config/bootstrap.ps1
@@ -24,7 +24,13 @@ param(
[string] $InstallRoot,
[switch] $AllowUnsigned,
[switch] $NoLaunch,
- [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full'
+ [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full',
+ [ValidateSet('', 'local-ai')] [string] $Scenario = '',
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $AiBackend = 'Auto',
+ [ValidateSet('None', 'LlamaCpp', 'Ollama', 'Foundry')] [string] $AiRuntime = 'None',
+ [switch] $RequireTriton,
+ [switch] $PlanOnly,
+ [string] $ReportRoot
)
function Invoke-CalmOsBootstrap {
@@ -34,7 +40,13 @@ function Invoke-CalmOsBootstrap {
[string] $InstallRoot,
[switch] $AllowUnsigned,
[switch] $NoLaunch,
- [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full'
+ [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full',
+ [ValidateSet('', 'local-ai')] [string] $Scenario = '',
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $AiBackend = 'Auto',
+ [ValidateSet('None', 'LlamaCpp', 'Ollama', 'Foundry')] [string] $AiRuntime = 'None',
+ [switch] $RequireTriton,
+ [switch] $PlanOnly,
+ [string] $ReportRoot
)
$ErrorActionPreference = 'Stop'
@@ -43,13 +55,26 @@ function Invoke-CalmOsBootstrap {
$repo = 'microsoft/WindowsDeveloperConfig'
$microsoftSignerSubject = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
+ $scenarioOptionNames = @('AiBackend', 'AiRuntime', 'RequireTriton', 'PlanOnly', 'ReportRoot')
+ if (-not $Scenario -and @($scenarioOptionNames | Where-Object { $PSBoundParameters.ContainsKey($_) }).Count -gt 0) {
+ throw 'AI backend/runtime/report options require -Scenario local-ai.'
+ }
+ if ($Scenario -and $PSBoundParameters.ContainsKey('Action')) {
+ throw '-Action configures the full workstation and cannot be combined with -Scenario local-ai.'
+ }
+
# Reject refs that could escape the repository path.
if ($Ref -notmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$' -or $Ref.Contains('..')) {
throw "'$Ref' is not a valid branch, tag or commit name. Use letters, digits, and . _ - / only."
}
if (-not $InstallRoot) {
- $InstallRoot = Join-Path ([Environment]::GetFolderPath('CommonApplicationData')) 'CalmOS'
+ $defaultInstallDirectory = if ($Scenario -and $AllowUnsigned) {
+ 'CalmOS-Development'
+ } else {
+ 'CalmOS'
+ }
+ $InstallRoot = Join-Path ([Environment]::GetFolderPath('CommonApplicationData')) $defaultInstallDirectory
}
$InstallRoot = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($InstallRoot)
if ($InstallRoot -notmatch '^[A-Za-z]:\\[^:]+$') {
@@ -79,7 +104,14 @@ function Invoke-CalmOsBootstrap {
[Parameter(Mandatory)] [string] $InstallRoot,
[switch] $AllowUnsigned,
[switch] $NoLaunch,
- [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full'
+ [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full',
+ [ValidateSet('', 'local-ai')] [string] $Scenario = '',
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $AiBackend = 'Auto',
+ [ValidateSet('None', 'LlamaCpp', 'Ollama', 'Foundry')] [string] $AiRuntime = 'None',
+ [switch] $RequireTriton,
+ [switch] $PlanOnly,
+ [string] $ReportRoot,
+ [Parameter(Mandatory)] [string] $ElevationErrorPath
)
$launcher = {
@@ -88,11 +120,27 @@ function Invoke-CalmOsBootstrap {
[string] $InstallRoot,
[switch] $AllowUnsigned,
[switch] $NoLaunch,
- [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full'
+ [ValidateSet('Full', 'Partial', 'Uninstall')] [string] $Action = 'Full',
+ [ValidateSet('', 'local-ai')] [string] $Scenario = '',
+ [ValidateSet('Auto', 'CPU', 'CUDA', 'ROCm', 'XPU')] [string] $AiBackend = 'Auto',
+ [ValidateSet('None', 'LlamaCpp', 'Ollama', 'Foundry')] [string] $AiRuntime = 'None',
+ [switch] $RequireTriton,
+ [switch] $PlanOnly,
+ [string] $ReportRoot,
+ [Parameter(Mandatory)] [string] $ElevationErrorPath
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
+ trap {
+ try {
+ [IO.File]::WriteAllText(
+ $ElevationErrorPath,
+ ($_ | Out-String),
+ [Text.UTF8Encoding]::new($false))
+ } catch { }
+ exit 1
+ }
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$flow = if ($AllowUnsigned) { 'src/windows-dev-config' } else { 'windows-dev-config' }
$baseUri = "https://raw.githubusercontent.com/microsoft/WindowsDeveloperConfig/$Ref/$flow"
@@ -131,19 +179,41 @@ function Invoke-CalmOsBootstrap {
$shellName = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' }
$arguments = @('-NoProfile')
if (-not $AllowUnsigned) { $arguments += '-ExecutionPolicy', 'RemoteSigned' }
- $arguments += '-File', $target, '-Ref', $Ref, '-InstallRoot', $InstallRoot, '-Action', $Action
+ $arguments += '-File', $target, '-Ref', $Ref, '-InstallRoot', $InstallRoot
+ if ($Scenario) {
+ $arguments += '-Scenario', $Scenario, '-AiBackend', $AiBackend, '-AiRuntime', $AiRuntime
+ if ($RequireTriton) { $arguments += '-RequireTriton' }
+ if ($PlanOnly) { $arguments += '-PlanOnly' }
+ if ($ReportRoot) { $arguments += '-ReportRoot', $ReportRoot }
+ } else {
+ $arguments += '-Action', $Action
+ }
if ($AllowUnsigned) { $arguments += '-AllowUnsigned' }
if ($NoLaunch) { $arguments += '-NoLaunch' }
- & (Join-Path $PSHOME $shellName) @arguments
- if ($LASTEXITCODE -ne 0) {
- throw "Bootstrap finished with exit code $LASTEXITCODE."
+ $bootstrapOutput = (& (Join-Path $PSHOME $shellName) @arguments 2>&1 | Out-String).Trim()
+ $bootstrapExitCode = $LASTEXITCODE
+ if ($bootstrapOutput) { Write-Host $bootstrapOutput }
+ if ($bootstrapExitCode -ne 0) {
+ throw "Bootstrap finished with exit code $bootstrapExitCode.`n$bootstrapOutput"
}
}
# PowerShell also recognizes smart quotes as string delimiters.
$escapedRef = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Ref)
$escapedRoot = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($InstallRoot)
- $command = "& {`n$launcher`n} -Ref '$escapedRef' -InstallRoot '$escapedRoot' -Action '$Action'"
+ $escapedErrorPath = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($ElevationErrorPath)
+ $command = "& {`n$launcher`n} -Ref '$escapedRef' -InstallRoot '$escapedRoot' -ElevationErrorPath '$escapedErrorPath'"
+ if ($Scenario) {
+ $command += " -Scenario '$Scenario' -AiBackend '$AiBackend' -AiRuntime '$AiRuntime'"
+ if ($RequireTriton) { $command += ' -RequireTriton' }
+ if ($PlanOnly) { $command += ' -PlanOnly' }
+ if ($ReportRoot) {
+ $escapedReportRoot = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($ReportRoot)
+ $command += " -ReportRoot '$escapedReportRoot'"
+ }
+ } else {
+ $command += " -Action '$Action'"
+ }
if ($AllowUnsigned) { $command += ' -AllowUnsigned' }
if ($NoLaunch) { $command += ' -NoLaunch' }
# Start-Process joins arguments; Windows quoting keeps the command intact.
@@ -168,15 +238,31 @@ function Invoke-CalmOsBootstrap {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
- $command = Get-CalmOsElevationCommand -Ref $Ref -InstallRoot $InstallRoot -AllowUnsigned:$AllowUnsigned -NoLaunch:$NoLaunch -Action $Action
+ $elevationErrorPath = Join-Path $env:TEMP "CalmOS-bootstrap-error-$([guid]::NewGuid().ToString('N')).txt"
+ $command = Get-CalmOsElevationCommand -Ref $Ref -InstallRoot $InstallRoot -AllowUnsigned:$AllowUnsigned -NoLaunch:$NoLaunch `
+ -Action $Action -Scenario $Scenario -AiBackend $AiBackend -AiRuntime $AiRuntime `
+ -RequireTriton:$RequireTriton -PlanOnly:$PlanOnly -ReportRoot $ReportRoot `
+ -ElevationErrorPath $elevationErrorPath
Write-Host 'Setup needs Administrator rights (a UAC prompt will appear)...' -ForegroundColor Yellow
$proc = Start-Process -FilePath $shell -ArgumentList ($arguments + @('-Command', $command)) -Verb RunAs -Wait -PassThru
if ($proc.ExitCode -ne 0) {
- throw "Elevated setup exited with code $($proc.ExitCode). No further setup was started."
+ $detail = if (Test-Path -LiteralPath $elevationErrorPath) {
+ (Get-Content -LiteralPath $elevationErrorPath -Raw).Trim()
+ } else {
+ 'The elevated process did not return diagnostic output.'
+ }
+ Remove-Item -LiteralPath $elevationErrorPath -Force -ErrorAction SilentlyContinue
+ throw "Elevated setup exited with code $($proc.ExitCode). No further setup was started.`n$detail"
}
+ Remove-Item -LiteralPath $elevationErrorPath -Force -ErrorAction SilentlyContinue
if ($NoLaunch) {
- $escapedTarget = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent((Join-Path $InstallRoot 'dev-config.ps1'))
- Write-Host "Run when ready: & '$escapedShell' $($arguments -join ' ') -File '$escapedTarget' -Action $Action$(if ($AllowUnsigned) { ' -AllowUnsigned' })"
+ if ($Scenario) {
+ $scenarioTarget = Join-Path $InstallRoot 'Scenarios\local-ai\Workloads\local-ai\install.ps1'
+ Write-Host "Scenario files are ready at $scenarioTarget." -ForegroundColor Cyan
+ } else {
+ $escapedTarget = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent((Join-Path $InstallRoot 'dev-config.ps1'))
+ Write-Host "Run when ready: & '$escapedShell' $($arguments -join ' ') -File '$escapedTarget' -Action $Action$(if ($AllowUnsigned) { ' -AllowUnsigned' })"
+ }
}
return
}
@@ -262,6 +348,62 @@ function Invoke-CalmOsBootstrap {
$InstallRoot = New-DevConfigProtectedDirectory -Path $InstallRoot
+ if ($Scenario) {
+ $workloadsDir = if ($AllowUnsigned) {
+ Join-Path (Join-Path $top.FullName 'src') 'Workloads'
+ } else {
+ Join-Path $top.FullName 'Workloads'
+ }
+ if (-not ((Test-Path (Join-Path $workloadsDir 'local-ai\install.ps1')) -and
+ (Test-Path (Join-Path $workloadsDir '_common\content-hashes.ps1')))) {
+ throw "'$Ref' does not contain the requested local-ai workload under the selected signed/source tree."
+ }
+ Assert-DevConfigProtectedTree -Directory $workloadsDir
+ if (-not $AllowUnsigned) {
+ Assert-DevConfigMicrosoftSigned -Directory $workloadsDir
+ }
+ . (Join-Path $workloadsDir '_common\content-hashes.ps1')
+ Assert-DevConfigWorkloadContent -WorkloadsRoot $workloadsDir
+
+ $scenariosRoot = New-DevConfigProtectedDirectory -Path (Join-Path $InstallRoot 'Scenarios')
+ $scenarioRoot = New-DevConfigProtectedDirectory -Path (Join-Path $scenariosRoot 'local-ai')
+ foreach ($existing in @('Workloads', 'windows-dev-config')) {
+ $existingPath = Join-Path $scenarioRoot $existing
+ if (Test-Path -LiteralPath $existingPath) {
+ Remove-Item -LiteralPath $existingPath -Recurse -Force
+ }
+ }
+ Copy-Item -LiteralPath $workloadsDir -Destination $scenarioRoot -Recurse -Force
+ $scenarioWindowsDevConfig = New-Item -ItemType Directory -Path (Join-Path $scenarioRoot 'windows-dev-config') -Force
+ Copy-Item -LiteralPath (Join-Path $setupDir 'steps') -Destination $scenarioWindowsDevConfig.FullName -Recurse -Force
+ Assert-DevConfigProtectedTree -Directory $scenarioRoot
+ if (-not $AllowUnsigned) {
+ Assert-DevConfigMicrosoftSigned -Directory $scenarioRoot
+ }
+ . (Join-Path $scenarioRoot 'Workloads\_common\content-hashes.ps1')
+ Assert-DevConfigWorkloadContent -WorkloadsRoot (Join-Path $scenarioRoot 'Workloads')
+ Get-ChildItem -LiteralPath $scenarioRoot -Recurse -Filter '*.ps1' -File | Unblock-File
+
+ Remove-Item -LiteralPath $work -Recurse -Force
+ $target = Join-Path $scenarioRoot 'Workloads\local-ai\install.ps1'
+ Write-Host " Scenario ready in $scenarioRoot" -ForegroundColor DarkGray
+ $scenarioArguments = @('-NoProfile')
+ if (-not $AllowUnsigned) { $scenarioArguments += '-ExecutionPolicy', 'RemoteSigned' }
+ $scenarioArguments += '-File', "`"$target`"", '-Backend', $AiBackend, '-Runtime', $AiRuntime
+ if ($RequireTriton) { $scenarioArguments += '-RequireTriton' }
+ if ($PlanOnly) { $scenarioArguments += '-PlanOnly' }
+ if ($ReportRoot) { $scenarioArguments += '-ReportRoot', "`"$ReportRoot`"" }
+ if ($NoLaunch) {
+ Write-Host "Run when ready: & '$escapedShell' $($scenarioArguments -join ' ')" -ForegroundColor Cyan
+ return
+ }
+ $proc = Start-Process -FilePath $shell -ArgumentList $scenarioArguments -Wait -PassThru -NoNewWindow
+ if ($proc.ExitCode -ne 0) {
+ throw "Local AI scenario finished with exit code $($proc.ExitCode)."
+ }
+ return
+ }
+
# Keep logs and progress when replacing setup scripts.
Copy-Item -LiteralPath (Join-Path $setupDir 'bootstrap.ps1'), (Join-Path $setupDir 'dev-config.ps1') -Destination $InstallRoot -Force
Copy-Item -LiteralPath (Join-Path $setupDir 'steps') -Destination $InstallRoot -Recurse -Force
diff --git a/src/windows-dev-config/steps/_environment.ps1 b/src/windows-dev-config/steps/_environment.ps1
index b001e85c..636bd2e8 100644
--- a/src/windows-dev-config/steps/_environment.ps1
+++ b/src/windows-dev-config/steps/_environment.ps1
@@ -24,8 +24,21 @@ function Invoke-DevConfigNativeCommand {
param($FilePath, $Arguments)
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
- $output = & $FilePath @Arguments 2>&1 | Out-String
- [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $output }
+ $previousExitCode = if (Test-Path Variable:global:LASTEXITCODE) { $global:LASTEXITCODE } else { $null }
+ Remove-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
+ try {
+ $output = & $FilePath @Arguments 2>&1 | Out-String
+ if (-not (Test-Path Variable:global:LASTEXITCODE)) {
+ throw "Native command '$FilePath' did not launch successfully."
+ }
+ [pscustomobject]@{ ExitCode = $global:LASTEXITCODE; Output = $output }
+ } finally {
+ if ($null -ne $previousExitCode) {
+ $global:LASTEXITCODE = $previousExitCode
+ } else {
+ Remove-Variable -Name LASTEXITCODE -Scope Global -ErrorAction SilentlyContinue
+ }
+ }
}
if ($TimeoutSeconds -eq 0) {
return & $invoke $FilePath $Arguments
diff --git a/src/windows-dev-config/steps/_winget.ps1 b/src/windows-dev-config/steps/_winget.ps1
index 3af48bb4..606f1260 100644
--- a/src/windows-dev-config/steps/_winget.ps1
+++ b/src/windows-dev-config/steps/_winget.ps1
@@ -274,29 +274,37 @@ function Update-DevConfigWinget {
}
function Test-DevConfigWingetPackageInstalled {
+ param(
+ [Parameter(Mandatory)] [string] $Id
+ )
+ return (Get-DevConfigWingetPackageState -Id $Id).State -eq 'Current'
+}
+
+function Get-DevConfigWingetPackageState {
param(
[Parameter(Mandatory)] [string] $Id
)
if ($Script:DevConfigWinGetMode -eq 'Cli') {
$listed = Invoke-DevConfigWingetCli -Arguments @('list', '--id', $Id, '--exact', '--accept-source-agreements')
if ($listed.ExitCode -eq $Script:DevConfigWingetNotFound) {
- return $false
+ return [pscustomobject]@{ State = 'Absent'; Package = $null }
}
if ($listed.ExitCode -ne 0) {
throw "winget list $Id failed with exit code $($listed.ExitCode)"
}
- # useLatest requires the package to be current, not only installed, so match the module path.
- return -not (Test-DevConfigWingetUpgradeAvailable -Id $Id)
+ $state = if (Test-DevConfigWingetUpgradeAvailable -Id $Id) { 'UpgradeAvailable' } else { 'Current' }
+ return [pscustomobject]@{ State = $state; Package = $null }
}
# EqualsCaseInsensitive avoids ambiguous substring matches.
$pkg = Get-WinGetPackage -Id $Id -Source winget -MatchOption EqualsCaseInsensitive
if (-not $pkg) {
- return $false
+ return [pscustomobject]@{ State = 'Absent'; Package = $null }
}
- # useLatest requires the package to be current, not only installed.
- return -not $pkg.IsUpdateAvailable
+ $updateProperty = $pkg.PSObject.Properties['IsUpdateAvailable']
+ $state = if ($updateProperty -and [bool]$updateProperty.Value) { 'UpgradeAvailable' } else { 'Current' }
+ return [pscustomobject]@{ State = $state; Package = $pkg }
}
# winget list exits 0 whether or not an upgrade exists, and every message it prints is localized.
@@ -314,25 +322,123 @@ function Test-DevConfigWingetUpgradeAvailable {
return @($upgrade.Output -split '\r?\n' | Where-Object { $_ -match ('(^|\s)' + [regex]::Escape($Id) + '(\s|$)') }).Count -gt 0
}
+function Get-DevConfigWingetInstallArguments {
+ param(
+ [Parameter(Mandatory)] [string] $Id
+ )
+ return @(
+ 'install', '--id', $Id, '--exact', '--source', 'winget', '--silent',
+ '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity'
+ )
+}
+
+function Get-DevConfigWingetUpgradeArguments {
+ param(
+ [Parameter(Mandatory)] [string] $Id
+ )
+ return @(
+ 'upgrade', '--id', $Id, '--exact', '--source', 'winget', '--silent',
+ '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity'
+ )
+}
+
function Install-DevConfigWingetPackage {
param(
[Parameter(Mandatory)] [string] $Id
)
Invoke-DevConfigRetry -Name "winget install $Id" -ScriptBlock {
if ($Script:DevConfigWinGetMode -eq 'Cli') {
- $r = Invoke-DevConfigWingetCli -Arguments @('install', '--id', $Id, '--exact', '--source', 'winget', '--silent', '--accept-package-agreements', '--accept-source-agreements')
+ $r = Invoke-DevConfigWingetCli -Arguments (Get-DevConfigWingetInstallArguments -Id $Id)
if ($r.ExitCode -ne 0 -and $r.ExitCode -ne $Script:DevConfigWingetNoUpgrade) {
throw "winget install $Id failed with exit code $($r.ExitCode)"
}
return
}
- $result = Install-WinGetPackage -Id $Id -Source winget -Mode Silent -MatchOption EqualsCaseInsensitive
- # NoApplicableUpgrade means the package is already installed and current.
- if (-not $result.Succeeded() -and $result.Status -ne 'NoApplicableUpgrade') {
- throw "winget install $Id failed: $($result.ErrorMessage())"
+ try {
+ $result = Install-WinGetPackage -Id $Id -Source winget -Mode Silent -MatchOption EqualsCaseInsensitive
+ # NoApplicableUpgrade means the package is already installed and current.
+ if (-not $result.Succeeded() -and $result.Status -ne 'NoApplicableUpgrade') {
+ throw "winget module install $Id failed: $($result.ErrorMessage())"
+ }
+ return
+ } catch {
+ $moduleError = $_.Exception.Message
+ if (-not (Test-DevConfigWingetCliUsable)) {
+ throw
+ }
+ Write-Host " WinGet module install failed; retrying with winget.exe ($moduleError)" -ForegroundColor Yellow
+ $r = Invoke-DevConfigWingetCli -Arguments (Get-DevConfigWingetInstallArguments -Id $Id)
+ if ($r.ExitCode -ne 0 -and $r.ExitCode -ne $Script:DevConfigWingetNoUpgrade) {
+ throw "winget install $Id failed after module fallback (module: $moduleError; CLI exit: $($r.ExitCode))"
+ }
+ $Script:DevConfigWinGetMode = 'Cli'
+ }
+ }
+}
+
+function Update-DevConfigWingetPackage {
+ param(
+ [Parameter(Mandatory)] [string] $Id
+ )
+ Invoke-DevConfigRetry -Name "winget upgrade $Id" -ScriptBlock {
+ if ($Script:DevConfigWinGetMode -eq 'Cli') {
+ $r = Invoke-DevConfigWingetCli -Arguments (Get-DevConfigWingetUpgradeArguments -Id $Id)
+ if ($r.ExitCode -ne 0 -and $r.ExitCode -ne $Script:DevConfigWingetNoUpgrade) {
+ throw "winget upgrade $Id failed with exit code $($r.ExitCode)"
+ }
+ return
+ }
+
+ try {
+ $result = Update-WinGetPackage -Id $Id -Source winget -Mode Silent -MatchOption EqualsCaseInsensitive
+ if (-not $result.Succeeded() -and $result.Status -ne 'NoApplicableUpgrade') {
+ throw "winget module upgrade $Id failed: $($result.ErrorMessage())"
+ }
+ return
+ } catch {
+ $moduleError = $_.Exception.Message
+ if (-not (Test-DevConfigWingetCliUsable)) {
+ throw
+ }
+ Write-Host " WinGet module upgrade failed; retrying with winget.exe ($moduleError)" -ForegroundColor Yellow
+ $r = Invoke-DevConfigWingetCli -Arguments (Get-DevConfigWingetUpgradeArguments -Id $Id)
+ if ($r.ExitCode -ne 0 -and $r.ExitCode -ne $Script:DevConfigWingetNoUpgrade) {
+ throw "winget upgrade $Id failed after module fallback (module: $moduleError; CLI exit: $($r.ExitCode))"
+ }
+ $Script:DevConfigWinGetMode = 'Cli'
+ }
+ }
+}
+
+function Ensure-DevConfigWingetPackage {
+ param(
+ [Parameter(Mandatory)] [string] $Id
+ )
+
+ $state = Get-DevConfigWingetPackageState -Id $Id
+ switch ($state.State) {
+ 'Current' {
+ return 'already-current'
+ }
+ 'UpgradeAvailable' {
+ Update-DevConfigWingetPackage -Id $Id
+ $action = 'upgraded'
+ }
+ 'Absent' {
+ Install-DevConfigWingetPackage -Id $Id
+ $action = 'installed'
}
+ default {
+ throw "Unknown WinGet package state '$($state.State)' for '$Id'."
+ }
+ }
+
+ Wait-DevConfigWingetPackageSettled -Id $Id
+ if ((Get-DevConfigWingetPackageState -Id $Id).State -ne 'Current') {
+ throw "WinGet did not verify '$Id' as installed and current after $action."
}
+ return $action
}
# Get-WinGetPackage catalog reads can lag after install, so wait before checking the result.
diff --git a/src/windows-dev-config/steps/packages.ps1 b/src/windows-dev-config/steps/packages.ps1
index 615d0794..a7f03009 100644
--- a/src/windows-dev-config/steps/packages.ps1
+++ b/src/windows-dev-config/steps/packages.ps1
@@ -231,8 +231,7 @@ function Invoke-PackagesPhase {
param($Id, $Large)
# Large packages can have several quiet download minutes because WinGet reports no progress here.
if ($Large) { Write-Host ' (Large download -- several quiet minutes here are normal.)' -ForegroundColor DarkGray }
- Install-DevConfigWingetPackage -Id $Id
- Wait-DevConfigWingetPackageSettled -Id $Id
+ [void](Ensure-DevConfigWingetPackage -Id $Id)
} `
-ArgumentList @($package.Id, $package.ContainsKey('Large'))
}