diff --git a/docs/WINDOWS_INTEL_MONITOR.md b/docs/WINDOWS_INTEL_MONITOR.md new file mode 100644 index 00000000000..70ba5a6f49a --- /dev/null +++ b/docs/WINDOWS_INTEL_MONITOR.md @@ -0,0 +1,33 @@ +# Intel GPU monitoring on Windows via IGCL + +The SYCL server can read Intel GPU telemetry from an optional native helper. +Build and install the helper explicitly; inference does not require it. +The helper loads the driver's ControlLib.dll from the Windows system directory. + +Obtain `igcl_api.h` from Intel's +[GPU Control Library](https://github.com/intel/drivers.gpu.control-library). +The tested header revision is `b6c462933502e13d1537dd5024949a51be30e63d`. +From an x64 Visual Studio developer PowerShell: + +```powershell +./sycl/tools/build-windows-telemetry.ps1 -IgclIncludeDir C:/SDK/igcl/include +./build-sycl-windows/strata-intel-telemetry.exe --samples 3 +``` + +The SYCL server discovers the helper in `build-sycl-windows`, or at the path +specified by `STRATA_INTEL_TELEMETRY_EXE`. `STRATA_INTEL_PCI_BDF` selects an +adapter by decimal bus:device.function, for example `3:0.0`. + +Readings include GPU activity, device memory allocation, GPU and memory +temperature, GPU chip power, and PCIe generation/width where supported. +Chip power is derived from energy deltas and is not total board power. +PCIe link information is not traffic throughput. Unsupported fields stay empty. +Missing, exited or stale helpers do not block inference. Restart the server +after resolving a failed helper; automatic helper restart is not implemented. + +Validated on Windows 10 with one Intel Arc Pro B70: real API readings under +inference load, collector unit tests, monitor tests and JavaScript label tests. +Observed peak readings included 100% activity, 119 W chip power and 64 C GPU +temperature. Helper cleanup after parent exit was also checked. +Windows 11 and multi-GPU mapping remain untested. Linux behavior has not been +revalidated by execution. The shared CUDA/HIP server is not modified. diff --git a/sycl/probe/windows_telemetry.cpp b/sycl/probe/windows_telemetry.cpp new file mode 100644 index 00000000000..8c982a565c3 --- /dev/null +++ b/sycl/probe/windows_telemetry.cpp @@ -0,0 +1,137 @@ +// Read-only IGCL sampler. Driver calls live outside the API server process. +// Build with Intel's igcl_api.h; ControlLib.dll comes from the graphics driver. +#include +#define CTL_APIEXPORT +#include "igcl_api.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static std::string quote(const char* text) { + std::ostringstream out; + out << '"'; + for (const unsigned char* p = reinterpret_cast(text); *p; ++p) { + if (*p == '"' || *p == '\\') out << '\\' << *p; + else if (*p < 32) out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << unsigned(*p); + else out << *p; + } + return out.str() + '"'; +} + +static void sensor(std::ostream& out, const char* key, const ctl_oc_telemetry_item_t& item, ctl_units_t units) { + if (!item.bSupported || item.units != units) return; + double v; + switch (item.type) { + case CTL_DATA_TYPE_DOUBLE: v = item.value.datadouble; break; + case CTL_DATA_TYPE_FLOAT: v = item.value.datafloat; break; + case CTL_DATA_TYPE_UINT64: v = static_cast(item.value.datau64); break; + case CTL_DATA_TYPE_INT64: v = static_cast(item.value.data64); break; + case CTL_DATA_TYPE_UINT32: v = item.value.datau32; break; + case CTL_DATA_TYPE_INT32: v = item.value.data32; break; + default: return; + } + if (std::isfinite(v) && v >= 0) out << ',' << quote(key) << ':' << std::setprecision(16) << v; +} + +int main(int argc, char** argv) { + unsigned index = 0, samples = 0; + DWORD parent_pid = 0; + std::string bdf; + for (int i = 1; i < argc; ++i) { + if (i + 1 >= argc) return 2; + std::string arg = argv[i++]; + if (arg == "--index") index = std::stoul(argv[i]); + else if (arg == "--parent") parent_pid = std::stoul(argv[i]); + else if (arg == "--samples") samples = std::stoul(argv[i]); + else if (arg == "--pci-bdf") bdf = argv[i]; + else return 2; + } + HANDLE parent = parent_pid ? OpenProcess(SYNCHRONIZE, FALSE, parent_pid) : nullptr; + if (parent_pid && !parent) return 3; + auto lib = LoadLibraryExW(L"ControlLib.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (!lib) { std::cerr << "Intel ControlLib.dll unavailable\n"; return 4; } +#define FN(name) auto name = reinterpret_cast(GetProcAddress(lib, #name)) + FN(ctlInit); FN(ctlEnumerateDevices); FN(ctlGetDeviceProperties); FN(ctlClose); + FN(ctlPowerTelemetryGet); FN(ctlEnumMemoryModules); FN(ctlMemoryGetProperties); FN(ctlMemoryGetState); + FN(ctlPciGetProperties); FN(ctlPciGetState); +#undef FN + if (!ctlInit || !ctlEnumerateDevices || !ctlGetDeviceProperties) return 5; + ctl_init_args_t args{}; + args.Size = sizeof(args); args.AppVersion = CTL_IMPL_VERSION; args.flags = CTL_INIT_FLAG_USE_LEVEL_ZERO; + ctl_api_handle_t api{}; + if (ctlInit(&args, &api) != CTL_RESULT_SUCCESS) return 6; + uint32_t count = 0; + if (ctlEnumerateDevices(api, &count, nullptr) || !count) return 7; + std::vector devices(count); + if (ctlEnumerateDevices(api, &count, devices.data())) return 7; + ctl_device_adapter_handle_t device = nullptr; + std::string name, address; + unsigned matched = 0; + for (auto candidate : devices) { + ctl_device_adapter_properties_t p{}; + p.Size = sizeof(p); p.Version = 2; + LUID luid{}; p.pDeviceID = &luid; p.device_id_size = sizeof(luid); + if (ctlGetDeviceProperties(candidate, &p) || p.pci_vendor_id != 0x8086 || p.device_type != CTL_DEVICE_TYPE_GRAPHICS) continue; + std::string candidate_bdf = std::to_string(p.adapter_bdf.bus) + ":" + std::to_string(p.adapter_bdf.device) + "." + std::to_string(p.adapter_bdf.function); + if ((!bdf.empty() && bdf == candidate_bdf) || (bdf.empty() && matched == index)) { + device = candidate; p.name[sizeof(p.name) - 1] = '\0'; name = p.name; address = candidate_bdf; break; + } + ++matched; + } + if (!device) { std::cerr << "Requested Intel adapter unavailable\n"; return 8; } + std::vector memories; + if (ctlEnumMemoryModules && ctlMemoryGetProperties && ctlMemoryGetState) { + uint32_t n = 0; + if (!ctlEnumMemoryModules(device, &n, nullptr) && n) { + std::vector all(n); + if (!ctlEnumMemoryModules(device, &n, all.data())) for (auto mem : all) { + ctl_mem_properties_t p{}; p.Size = sizeof(p); + if (!ctlMemoryGetProperties(mem, &p) && p.location == CTL_MEM_LOC_DEVICE) memories.push_back(mem); + } + } + } + unsigned emitted = 0; + do { + if (parent && WaitForSingleObject(parent, 0) != WAIT_TIMEOUT) break; + std::ostringstream out; + out << "{\"name\":" << quote(name.c_str()) << ",\"pci_bdf\":" << quote(address.c_str()); + ctl_power_telemetry_t t{}; t.Size = sizeof(t); + if (ctlPowerTelemetryGet && !ctlPowerTelemetryGet(device, &t)) { + sensor(out, "timestamp", t.timeStamp, CTL_UNITS_TIME_SECONDS); + sensor(out, "energy", t.gpuEnergyCounter, CTL_UNITS_ENERGY_JOULES); + sensor(out, "activity", t.globalActivityCounter, CTL_UNITS_TIME_SECONDS); + sensor(out, "temp", t.gpuCurrentTemperature, CTL_UNITS_TEMPERATURE_CELSIUS); + sensor(out, "vram_temp", t.vramCurrentTemperature, CTL_UNITS_TEMPERATURE_CELSIUS); + } + uint64_t total = 0, free = 0; + bool valid = !memories.empty(); + for (auto mem : memories) { + ctl_mem_state_t s{}; s.Size = sizeof(s); + if (ctlMemoryGetState(mem, &s) || !s.size || s.free > s.size) { valid = false; break; } + total += s.size; free += s.free; + } + if (valid) out << ",\"mem_total\":" << total << ",\"mem_used\":" << total - free; + ctl_pci_properties_t p{}; p.Size = sizeof(p); + p.address.Size = sizeof(p.address); p.maxSpeed.Size = sizeof(p.maxSpeed); + if (ctlPciGetProperties && !ctlPciGetProperties(device, &p) && p.maxSpeed.gen > 0) + out << ",\"pcie_gen_max\":" << p.maxSpeed.gen; + ctl_pci_state_t state{}; state.Size = sizeof(state); state.speed.Size = sizeof(state.speed); + if (ctlPciGetState && !ctlPciGetState(device, &state)) { + if (state.speed.gen > 0) out << ",\"pcie_gen\":" << state.speed.gen; + if (state.speed.width > 0) out << ",\"pcie_width\":" << state.speed.width; + } + std::cout << out.str() << "}\n" << std::flush; + if (!std::cout || (samples && ++emitted >= samples)) break; + if (parent) { if (WaitForSingleObject(parent, 1000) != WAIT_TIMEOUT) break; } + else Sleep(1000); + } while (true); + if (ctlClose) ctlClose(api); + if (parent) CloseHandle(parent); + FreeLibrary(lib); + return 0; +} diff --git a/sycl/serve/server_intel.py b/sycl/serve/server_intel.py index baae26c2368..cf2941a64aa 100644 --- a/sycl/serve/server_intel.py +++ b/sycl/serve/server_intel.py @@ -5,7 +5,7 @@ Every argument is serve/server.py's. Nothing in serve/ is edited (the SYCL port keeps out of the shared files so upstream merges stay clean); this wrapper adds, at run time: - - the Monitor tab's GPU readings on an Intel Arc (sycl/serve/xe_telemetry.py) (also on a PC with an NVIDIA card: the engine runs on the Intel one); + - Intel GPU readings: IGCL on Windows, xe on Linux, preferring the Intel card even with NVIDIA present; - a model menu in the web app's header, for a host that swaps models on one GPU: the config names the RPC that does the swap as "model_switcher" (it takes {"mode": m} and answers {"mode", "up", "starting", "choices", "urls"}); the menu offers the models served on this server's port. Without that key nothing changes; @@ -35,17 +35,52 @@ def install_xe_reader(): - """telemetry.gpu_reader -> the xe reader whenever an Intel Arc is there. #2001: this server runs the SYCL engine, - so the cards it reports are the Intel ones; before, an NVIDIA card in the same PC answered first and the Monitor - (and the benchmark report, which takes the card's name from it) showed the GeForce the engine never used.""" + """Report the Intel card used by SYCL, with native Windows IGCL or Linux xe.""" orig = T.gpu_reader def gpu_reader(index=0, amd=False): + if sys.platform == "win32" and not amd: + from windows_telemetry import _WindowsIntelGpu + return _WindowsIntelGpu(index) xe = _XeGpu(index) return xe if xe.ok() else orig(index, amd) T.gpu_reader = gpu_reader + class IntelTelemetry(T.Telemetry): + def close(self): + super().close() + for _, reader in self.gpus: + close = getattr(reader, "close", None) + if close: + close() + + T.Telemetry = IntelTelemetry + + +def install_windows_monitor(): + """Explain IGCL power scope and current PCIe link in the Windows Monitor.""" + if sys.platform != "win32": + return + original = S.make_handler + + def make(svc): + class Handler(original(svc)): + def do_GET(self): + if self.path.split("?")[0] == "/web/app.js": + body = (ROOT / "serve/web/app.js").read_bytes() + b"\n" + (HERE / "web/windows-monitor.js").read_bytes() + self.send_response(200) + self.send_header("Content-Type", "text/javascript; charset=utf-8") + self.send_header("Cache-Control", "no-cache") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + super().do_GET() + return Handler + + S.make_handler = make + def model_switcher(url: str, port, model, mode: str = "") -> dict: """The host's model swapper: the models it serves on THIS port, which one holds the card, whether one is loading. @@ -308,6 +343,7 @@ def top_logprobs(req: dict): if __name__ == "__main__": install_xe_reader() + install_windows_monitor() install_narrator() install_switcher(sys.argv[1:]) install_logprobs() diff --git a/sycl/serve/test_windows_telemetry.py b/sycl/serve/test_windows_telemetry.py new file mode 100644 index 00000000000..4c8d967d40e --- /dev/null +++ b/sycl/serve/test_windows_telemetry.py @@ -0,0 +1,107 @@ +import io +import json +from pathlib import Path +import sys +import time +import unittest +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from windows_telemetry import _Rates, _WindowsIntelGpu + + +class RatesTest(unittest.TestCase): + def test_rates_and_idle(self): + rates = _Rates() + self.assertNotIn("power", rates.convert(dict(timestamp=10, energy=100, activity=2))) + sample = rates.convert(dict(timestamp=12, energy=180, activity=3)) + self.assertEqual(sample["power"], 40) + self.assertEqual(sample["util"], 50) + sample = rates.convert(dict(timestamp=13, energy=180, activity=3)) + self.assertEqual(sample["power"], 0) + self.assertEqual(sample["util"], 0) + + def test_reset_missing_stale_and_nonfinite(self): + rates = _Rates() + for sample in [dict(timestamp=10, energy=100), dict(timestamp=11, energy=10), + dict(timestamp=11, energy=20), dict(timestamp=20, energy=30), + dict(timestamp=21, energy=float("nan")), dict(timestamp=22, energy=40), + dict(timestamp=23), dict(timestamp=24, energy=60)]: + self.assertNotIn("power", rates.convert(sample)) + + def test_bad_memory_and_sensors(self): + rates = _Rates() + sample = rates.convert(dict(mem_used=11, mem_total=10, temp=float("inf"), vram_temp=-1)) + for key in ("mem_used", "mem_total", "temp", "vram_temp"): + self.assertNotIn(key, sample) + sample = rates.convert(dict(mem_used=0, mem_total=10, temp=0)) + self.assertEqual(sample["mem_used"], 0) + self.assertEqual(sample["temp"], 0) + + +class ReaderTest(unittest.TestCase): + def reader(self): + with patch("windows_telemetry.sys.platform", "linux"): + reader = _WindowsIntelGpu() + self.addCleanup(reader.close) + return reader + + def test_stale_dead_and_snapshot_copy(self): + reader = self.reader() + process = Mock() + process.poll.return_value = None + reader._process = process + reader._latest = {"temp": 55} + reader._received = time.monotonic() + self.assertEqual(reader.read(), {"temp": 55}) + reader.read()["temp"] = 0 + self.assertEqual(reader.read()["temp"], 55) + reader._received -= 6 + self.assertEqual(reader.read(), {}) + reader._received = time.monotonic() + process.poll.return_value = 1 + self.assertEqual(reader.read(), {}) + + def test_consumer_ignores_bad_lines(self): + reader = self.reader() + process = Mock() + process.stdout = io.StringIO('broken\n[]\n' + json.dumps(dict(name="Arc", temp=50)) + '\n') + reader._process = process + reader._consume() + self.assertTrue(reader.ok()) + self.assertEqual(reader.name(), "Arc") + self.assertEqual(reader._latest["temp"], 50) + + def test_missing_helper(self): + with patch("windows_telemetry.sys.platform", "win32"), patch("windows_telemetry.subprocess.Popen", side_effect=OSError), patch("windows_telemetry.subprocess.CREATE_NO_WINDOW", 0, create=True): + reader = _WindowsIntelGpu() + self.assertFalse(reader.ok()) + self.assertEqual(reader.read(), {}) + + def test_failed_startup_is_cleaned_up_and_bdf_forwarded(self): + process = Mock() + process.stdout = io.StringIO('') + process.poll.return_value = None + with patch("windows_telemetry.sys.platform", "win32"), patch("windows_telemetry.subprocess.Popen", return_value=process) as popen, patch("windows_telemetry.subprocess.CREATE_NO_WINDOW", 0, create=True), patch.dict("os.environ", {"STRATA_INTEL_PCI_BDF": "3:0.0"}): + reader = _WindowsIntelGpu(2) + self.assertFalse(reader.ok()) + self.assertIsNone(reader._process) + process.terminate.assert_called_once() + command = popen.call_args.args[0] + self.assertEqual(command[command.index("--index") + 1], "2") + self.assertEqual(command[command.index("--pci-bdf") + 1], "3:0.0") + + def test_close_terminates_child_and_is_repeatable(self): + reader = self.reader() + process = Mock() + process.poll.side_effect = [None, 0] + reader._process = process + reader.close() + process.terminate.assert_called_once() + process.wait.assert_called_once_with(timeout=2) + reader.close() + self.assertIsNone(reader._process) + + +if __name__ == "__main__": + unittest.main() diff --git a/sycl/serve/web/test-windows-monitor.cjs b/sycl/serve/web/test-windows-monitor.cjs new file mode 100644 index 00000000000..adf34c111f2 --- /dev/null +++ b/sycl/serve/web/test-windows-monitor.cjs @@ -0,0 +1,26 @@ +// Run with node sycl/serve/web/test-windows-monitor.cjs. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const cards = {}; +let calls = 0; +const scope = {renderMonitor: () => calls++, fmt: x => String(x), + setMetric: (key, value, unit, sub) => { cards[key] = {value, unit, sub}; }}; +vm.createContext(scope); +vm.runInContext(fs.readFileSync(__dirname + '/windows-monitor.js', 'utf8'), scope); +scope.renderMonitor({}, {gpu_backend: 'igcl', gpu_power: 0, gpu_temp: 56, gpu_vram_temp: 58, + gpu_pcie_gen: 3, gpu_pcie_gen_max: 5, gpu_pcie_width: 16}); +assert.equal(cards.power.value, '0'); +assert.equal(cards.power.sub, 'GPU chip power (Intel)'); +assert.equal(cards.temp.sub, 'VRAM 58 °C'); +assert.equal(cards.pcie.value, 'Gen3'); +assert.equal(cards.pcie.unit, 'x16'); +assert.match(cards.pcie.sub, /Gen5.*Traffic unavailable/); +scope.renderMonitor({}, {gpu_backend: 'igcl'}); +assert.equal(cards.power.value, null); +assert.equal(cards.pcie.value, null); +const before = JSON.stringify(cards); +scope.renderMonitor({}, {gpu_power: 120}); +assert.equal(JSON.stringify(cards), before); +assert.equal(calls, 3); +console.log('Windows Monitor labels: passed'); diff --git a/sycl/serve/web/windows-monitor.js b/sycl/serve/web/windows-monitor.js new file mode 100644 index 00000000000..011f36e3e10 --- /dev/null +++ b/sycl/serve/web/windows-monitor.js @@ -0,0 +1,12 @@ +// Loaded only by the Windows Intel wrapper; the shared web app is unchanged. +const renderIntelMonitorBase = renderMonitor; +renderMonitor = function(live, hw, st, eng, h, last, requests, totals, kept) { + renderIntelMonitorBase(live, hw, st, eng, h, last, requests, totals, kept); + if (hw.gpu_backend !== "igcl") return; + setMetric("power", hw.gpu_power == null ? null : fmt(hw.gpu_power), "W", "GPU chip power (Intel)"); + setMetric("temp", hw.gpu_temp == null ? null : fmt(hw.gpu_temp), "°C", + hw.gpu_vram_temp == null ? "" : `VRAM ${fmt(hw.gpu_vram_temp)} °C`); + setMetric("pcie", hw.gpu_pcie_gen ? `Gen${hw.gpu_pcie_gen}` : null, + hw.gpu_pcie_width ? `x${hw.gpu_pcie_width}` : "", + (hw.gpu_pcie_gen_max ? `GPU supports Gen${hw.gpu_pcie_gen_max} · ` : "") + "Traffic unavailable"); +}; diff --git a/sycl/serve/windows_telemetry.py b/sycl/serve/windows_telemetry.py new file mode 100644 index 00000000000..1ac1223cc8a --- /dev/null +++ b/sycl/serve/windows_telemetry.py @@ -0,0 +1,127 @@ +"""Read-only Intel Windows telemetry, isolated from the server in an IGCL helper.""" +from __future__ import annotations + +import atexit +import json +import math +import os +from pathlib import Path +import subprocess +import sys +import threading +import time + + +def _number(value): + return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) and value >= 0 + + +class _Rates: + def __init__(self): + self.previous = None + + def convert(self, sample): + result = {k: sample[k] for k in ("temp", "vram_temp", "mem_used", "mem_total", "pcie_gen", + "pcie_gen_max", "pcie_width") if _number(sample.get(k))} + used, total = result.get("mem_used"), result.get("mem_total") + if total is None or total <= 0 or used is None or used > total: + result.pop("mem_used", None) + result.pop("mem_total", None) + result.update(backend="igcl", power_scope="gpu", pci_bdf=sample.get("pci_bdf")) + previous, self.previous = self.previous, sample + if previous and _number(sample.get("timestamp")) and _number(previous.get("timestamp")): + dt = sample["timestamp"] - previous["timestamp"] + if 0 < dt <= 5: + for counter, key, scale in (("energy", "power", 1), ("activity", "util", 100)): + current, old = sample.get(counter), previous.get(counter) + if _number(current) and _number(old) and current >= old: + value = (current - old) / dt * scale + if math.isfinite(value): + result[key] = min(value, 100) if key == "util" else value + return result + + +class _WindowsIntelGpu: + STALE_SECONDS = 5 + + def __init__(self, index=0): + self._process = None + self._lock = threading.Lock() + self._ready = threading.Event() + self._latest = {} + self._received = 0 + self._name = None + self._thread = None + if sys.platform != "win32": + return + helper = os.environ.get("STRATA_INTEL_TELEMETRY_EXE") or str( + Path(__file__).resolve().parents[2] / "build-sycl-windows" / "strata-intel-telemetry.exe") + command = [helper, "--index", str(index), "--parent", str(os.getpid())] + if os.environ.get("STRATA_INTEL_PCI_BDF"): + command += ["--pci-bdf", os.environ["STRATA_INTEL_PCI_BDF"]] + try: + self._process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + encoding="utf-8", errors="replace", + creationflags=subprocess.CREATE_NO_WINDOW) + self._thread = threading.Thread(target=self._consume, name="intel-telemetry", daemon=True) + self._thread.start() + self._ready.wait(3) + if not self._name: + self.close() + else: + atexit.register(self.close) + except OSError: + self.close() + + def _consume(self): + rates = _Rates() + try: + for line in self._process.stdout: + try: + sample = json.loads(line) + if not isinstance(sample, dict) or not isinstance(sample.get("name"), str): + continue + result = rates.convert(sample) + except (ValueError, TypeError, OverflowError): + continue + with self._lock: + self._name = sample["name"] + self._latest = result + self._received = time.monotonic() + self._ready.set() + except (OSError, ValueError): + pass + finally: + self._ready.set() + + def ok(self): + return self._name is not None + + def name(self): + return self._name + + def read(self): + with self._lock: + process = self._process + if process is None or process.poll() is not None or time.monotonic() - self._received > self.STALE_SECONDS: + return {} + return dict(self._latest) + + def close(self): + atexit.unregister(self.close) + process = self._process + if process is not None: + try: + if process.poll() is None: + process.terminate() + process.wait(timeout=2) + except (OSError, subprocess.TimeoutExpired): + try: + process.kill() + except OSError: + pass + if self._thread and self._thread is not threading.current_thread(): + self._thread.join(timeout=1) + if process.poll() is not None and process.stdout: + process.stdout.close() + self._process = None diff --git a/sycl/tools/build-windows-telemetry.ps1 b/sycl/tools/build-windows-telemetry.ps1 new file mode 100644 index 00000000000..2e96b81acb1 --- /dev/null +++ b/sycl/tools/build-windows-telemetry.ps1 @@ -0,0 +1,12 @@ +param( + [Parameter(Mandatory=$true)][string]$IgclIncludeDir, + [string]$OutputPath +) +$ErrorActionPreference = 'Stop' +if (!$OutputPath) { $OutputPath = Join-Path $PSScriptRoot '..\..\build-sycl-windows\strata-intel-telemetry.exe' } +if (!(Test-Path -LiteralPath "$IgclIncludeDir\igcl_api.h")) { throw 'igcl_api.h not found' } +if (!(Get-Command cl.exe -ErrorAction SilentlyContinue)) { throw 'Run from an x64 Visual Studio developer environment' } +$destination = [System.IO.Path]::GetFullPath($OutputPath) +New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetDirectoryName($destination)) | Out-Null +& cl.exe /nologo /EHsc /std:c++17 /O2 /W4 "/I$IgclIncludeDir" "$PSScriptRoot\..\probe\windows_telemetry.cpp" "/Fo$destination.obj" "/Fe$destination" +if ($LASTEXITCODE -ne 0) { throw "Telemetry build failed: $LASTEXITCODE" }