"""Reproducible GPU Batch01 V1.1 evidence runner for the ARM64 CUDA image. The runner deliberately evaluates the CPU historical port and the CUDA engine separately. CUDA unavailability is an error: GPU results are never replaced by CPU results. """ from __future__ import annotations import argparse import csv import hashlib import json import os import platform import sys import time from collections.abc import Mapping from pathlib import Path from typing import Any import numpy as np import torch from gpu_feature_engine_v1 import evaluate_batch, psar_trace, supertrend_trace from gpu_feature_parity_contract_v1_1 import ( calibrate, canonical_bytes, compare_stateful_trace, corpus_manifest, cpu_psar_trace, cpu_supertrend_trace, deterministic_adversarial_ohlcv, freeze_contract, historical_cpu_oracle, nan_gap_semantics_manifest, validate_frozen_contract, ) ARTIFACT_PREFIX = "gpu_batch01_v1_1" def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for block in iter(lambda: handle.read(1024 * 1024), b""): digest.update(block) return digest.hexdigest() def write_json(path: Path, payload: Mapping[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(canonical_bytes(dict(payload)) + b"\n") def read_ohlcv(path: Path) -> dict[str, np.ndarray]: with path.open(newline="", encoding="utf-8") as handle: rows = list(csv.DictReader(handle)) required = ("close", "high", "low", "volume") if not rows or not set(required).issubset(rows[0]): raise ValueError("CSV must contain non-empty close, high, low, volume columns") values = { name: np.asarray([float(row[name]) for row in rows], dtype=np.float64) for name in required } if not all(np.isfinite(value).all() for value in values.values()): raise ValueError( "historical NaN/gap semantics are undefined; non-finite OHLCV inputs are rejected" ) return values def write_ohlcv_csv(path: Path, ohlcv: Mapping[str, np.ndarray]) -> None: with path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=["close", "high", "low", "volume"]) writer.writeheader() writer.writerows( {name: float(ohlcv[name][index]) for name in writer.fieldnames} for index in range(len(ohlcv["close"])) ) def gpu_outputs( request: Mapping[str, Any], ohlcv: Mapping[str, np.ndarray], device: torch.device ) -> tuple[dict[str, np.ndarray], dict[str, float]]: torch.cuda.reset_peak_memory_stats(device) transfer_start = time.perf_counter() tensors = tuple( torch.as_tensor(ohlcv[name], dtype=torch.float64, device=device) for name in ("close", "high", "low", "volume") ) torch.cuda.synchronize(device) transfer_seconds = time.perf_counter() - transfer_start compute_start = time.perf_counter() outputs = evaluate_batch(dict(request), *tensors) torch.cuda.synchronize(device) compute_seconds = time.perf_counter() - compute_start host_start = time.perf_counter() result = {key: value.detach().cpu().numpy() for key, value in outputs.items()} torch.cuda.synchronize(device) return result, { "host_to_device_seconds": transfer_seconds, "compute_seconds": compute_seconds, "device_to_host_seconds": time.perf_counter() - host_start, "max_gpu_memory_bytes": int(torch.cuda.max_memory_allocated(device)), } def traces( requests: list[Mapping[str, Any]], ohlcv: Mapping[str, np.ndarray], device: torch.device ) -> dict[str, Any]: close, high, low = (ohlcv[name] for name in ("close", "high", "low")) gpu_values = tuple( torch.as_tensor(value, dtype=torch.float64, device=device) for value in (close, high, low) ) records = [] for item in requests: indicator = int(item["indicator_id"]) if indicator == 19: expected = cpu_supertrend_trace( close, high, low, int(item["period"]), float(item["p1"]) ) actual = supertrend_trace(*gpu_values, int(item["period"]), float(item["p1"])) trace_type = "supertrend" elif indicator == 28: expected = cpu_psar_trace(close, high, low, float(item["p1"])) actual = psar_trace(*gpu_values, float(item["p1"])) trace_type = "psar" else: continue torch.cuda.synchronize(device) compared = compare_stateful_trace( expected, {key: value.detach().cpu().numpy() for key, value in actual.items()}, trace_type=trace_type, ) records.append({"request_id": str(item["request_id"]), "family": trace_type, **compared}) return { "artifact": "GPU_BATCH01_STATEFUL_TRACE_COMPARISON_V1_1", "passed": all(item["exact"] for item in records), "records": records, } def performance( request: Mapping[str, Any], ohlcv: Mapping[str, np.ndarray], device: torch.device ) -> dict[str, Any]: # Each family is measured through the same CUDA evaluate_batch entry point. families: dict[str, list[Mapping[str, Any]]] = {} names = { 17: "bollinger", 18: "bollinger", 19: "supertrend", 20: "donchian", 21: "donchian", 23: "keltner", 24: "keltner", 28: "psar", } for item in request["requests"]: families.setdefault(names[int(item["indicator_id"])], []).append(item) results = [] for family, items in sorted(families.items()): family_request = {"requests": items} _, cold = gpu_outputs(family_request, ohlcv, device) _, warm = gpu_outputs(family_request, ohlcv, device) values = len(ohlcv["close"]) * len(items) results.append( { "family": family, "variants": len(items), "values": values, "cold": cold, "warm": warm, "cold_values_per_second": values / cold["compute_seconds"] if cold["compute_seconds"] else None, "warm_values_per_second": values / warm["compute_seconds"] if warm["compute_seconds"] else None, } ) return { "artifact": "GPU_BATCH01_PERFORMANCE_METRICS_V1_1", "measurement": ( "CUDA synchronize before each interval; cold is first family invocation, warm is second" ), "families": results, } def role_semantics(path: Path | None) -> dict[str, Any]: if path is None: return { "status": "not_reconstructable", "reason": "Batch01 request records contain no strategy-role assignment; provide " "--role-usage-artifact generated from historical lineage.", } if not path.is_file(): raise ValueError(f"role usage artifact does not exist: {path}") return {"status": "source_artifact_recorded", "path": str(path), "sha256": sha256_file(path)} def implementation_hashes() -> dict[str, str]: root = Path(__file__).resolve().parent return { path.name: sha256_file(path) for path in ( root / "gpu_batch01_v1_1_runner.py", root / "gpu_feature_engine_v1.py", root / "gpu_feature_parity_contract_v1_1.py", ) } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--calibration-csv", type=Path, required=True) parser.add_argument("--holdout-csv", type=Path, required=True) parser.add_argument("--request-json", type=Path, required=True) parser.add_argument("--output-dir", type=Path, required=True) parser.add_argument("--adversarial-length", type=int, default=256) parser.add_argument("--seed", type=int, default=0) parser.add_argument("--role-usage-artifact", type=Path) parser.add_argument("--image-env", default=os.getenv("GPU_BATCH01_IMAGE_ENV")) args = parser.parse_args() if not torch.cuda.is_available(): raise SystemExit("CUDA GPU is required; no CPU fallback is available") args.output_dir.mkdir(parents=True, exist_ok=True) request = json.loads(args.request_json.read_text(encoding="utf-8")) requests = request.get("requests") if not isinstance(requests, list) or not requests: raise ValueError("request JSON must contain a non-empty requests list") device = torch.device("cuda") # Calibration is the only stage allowed to derive and freeze acceptance limits. calibration_ohlcv = read_ohlcv(args.calibration_csv) calibration_cpu = historical_cpu_oracle(args.calibration_csv, requests) calibration_gpu, calibration_timing = gpu_outputs(request, calibration_ohlcv, device) calibration_manifest = corpus_manifest( {"calibration": calibration_ohlcv}, { "source_csv": str(args.calibration_csv), "source_sha256": sha256_file(args.calibration_csv), }, ) calibration_evidence = calibrate( calibration_gpu, calibration_cpu["outputs"], requests, calibration_ohlcv["close"], calibration_manifest, ) contract = freeze_contract(calibration_evidence) write_json(args.output_dir / "gpu_feature_parity_calibration_v1_1.json", calibration_evidence) write_json(args.output_dir / "gpu_feature_parity_contract_v1_1.json", contract) np.savez_compressed( args.output_dir / f"{ARTIFACT_PREFIX}_calibration_cpu_oracle.npz", **calibration_cpu["outputs"], ) np.savez_compressed( args.output_dir / f"{ARTIFACT_PREFIX}_calibration_gpu_outputs.npz", **calibration_gpu ) adversarial = deterministic_adversarial_ohlcv(length=args.adversarial_length, seed=args.seed) adversarial_csv = args.output_dir / f"{ARTIFACT_PREFIX}_adversarial.csv" write_ohlcv_csv(adversarial_csv, adversarial) corpora = { "calibration": calibration_ohlcv, "holdout": read_ohlcv(args.holdout_csv), "adversarial": adversarial, } corpus = corpus_manifest( corpora, { "adversarial_generator": "deterministic_adversarial_ohlcv", "seed": args.seed, "role_semantics": role_semantics(args.role_usage_artifact), }, ) write_json(args.output_dir / "gpu_feature_parity_corpus_manifest_v1_1.json", corpus) write_json( args.output_dir / "gpu_feature_parity_nan_gap_semantics_v1_1.json", nan_gap_semantics_manifest(), ) validations, trace_reports = [], [] for name in ("calibration", "holdout", "adversarial"): if name == "calibration": csv_path, cpu, gpu, timing = ( args.calibration_csv, calibration_cpu, calibration_gpu, calibration_timing, ) else: csv_path = args.holdout_csv if name == "holdout" else adversarial_csv cpu = historical_cpu_oracle(csv_path, requests) gpu, timing = gpu_outputs(request, corpora[name], device) validation = validate_frozen_contract(contract, gpu, cpu["outputs"], corpora[name]["close"]) validation.update( { "corpus": name, "cpu_oracle": {key: value for key, value in cpu.items() if key != "outputs"}, "gpu_timing": timing, } ) validations.append(validation) trace_reports.append({"corpus": name, **traces(requests, corpora[name], device)}) if name != "calibration": np.savez_compressed( args.output_dir / f"{ARTIFACT_PREFIX}_{name}_cpu_oracle.npz", **cpu["outputs"] ) np.savez_compressed( args.output_dir / f"{ARTIFACT_PREFIX}_{name}_gpu_outputs.npz", **gpu ) validation_payload = { "artifact": "GPU_FEATURE_PARITY_VALIDATION_REPORT_V1_1", "contract_sha256": hashlib.sha256(canonical_bytes(contract)).hexdigest(), "passed": all(value["passed"] for value in validations), "corpora": validations, } trace_payload = { "artifact": "GPU_BATCH01_STATEFUL_TRACE_REPORT_V1_1", "passed": all(value["passed"] for value in trace_reports), "corpora": trace_reports, } write_json(args.output_dir / "gpu_feature_parity_validation_v1_1.json", validation_payload) write_json(args.output_dir / "gpu_batch01_stateful_trace_comparison_v1_1.json", trace_payload) write_json( args.output_dir / "gpu_batch01_performance_metrics_v1_1.json", performance(request, corpora["holdout"], device), ) root = Path(__file__).resolve().parent manifest = { "artifact": "GPU_BATCH01_V1_1_EVIDENCE_MANIFEST", "schema_version": "1.1", "python": sys.version, "platform": platform.platform(), "torch": torch.__version__, "cuda": torch.version.cuda, "gpu": torch.cuda.get_device_properties(device).name, "image_env": args.image_env, "source_hashes": { "historical_band_channel.py": sha256_file( root / "control_plane/trading_studio/indicators/historical_band_channel.py" ) }, "implementation_hashes": implementation_hashes(), "data_hashes": { "request_json": sha256_file(args.request_json), "calibration_csv": sha256_file(args.calibration_csv), "holdout_csv": sha256_file(args.holdout_csv), "adversarial_csv": sha256_file(adversarial_csv), }, "calibration_gpu_timing": calibration_timing, "role_semantics": role_semantics(args.role_usage_artifact), } write_json(args.output_dir / "gpu_batch01_v1_1_evidence_manifest.json", manifest) print( json.dumps( { "output_dir": str(args.output_dir), "validation_passed": validation_payload["passed"], "trace_passed": trace_payload["passed"], }, allow_nan=False, ) ) if __name__ == "__main__": main()