Preserve exact float32 via explicit nvcc compilation and audited binary loading
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
The caller authenticates frozen sources, configuration, the complete KC list,
|
||||
and the candidate edge mapping. This module does not infer D from DAN activity.
|
||||
CuPy is imported only when CudaEligibility is instantiated. Set CUPY_CACHE_DIR
|
||||
to a dedicated candidate directory before doing so; this module writes no files.
|
||||
to a dedicated candidate directory before doing so. The only files this module
|
||||
writes are explicit nvcc build evidence and cubins beneath that cache directory.
|
||||
|
||||
API: prepare_events(...) once, then CudaEligibility(...).run(prepared, ...).
|
||||
All commands are sampled AFTER the current tick; only the LAST command snapshot
|
||||
@@ -14,9 +15,12 @@ replacement for comparison against the SHA-pinned frozen implementation.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -26,6 +30,7 @@ import numpy as np
|
||||
COMPARTMENTS = ("B2B'2a", "a1", "y2a'1", "y5B'2a")
|
||||
_INT32_MAX = np.iinfo(np.int32).max
|
||||
CUDA_OPTIONS = ("--std=c++11", "--fmad=false", "--ftz=false")
|
||||
DEFAULT_NVCC = "/usr/local/cuda/bin/nvcc"
|
||||
CUDA_SOURCE = r'''
|
||||
extern "C" __global__
|
||||
void eligibility_trace(const long long* ptr, const int* ticks,
|
||||
@@ -84,6 +89,105 @@ void eligibility_delta(const float* sampled, const int* source_local,
|
||||
'''
|
||||
|
||||
|
||||
def _file_sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _save_json_new(path: Path, value: Any) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, sort_keys=True, indent=2, allow_nan=False)
|
||||
stream.write("\n")
|
||||
|
||||
|
||||
def _nvcc_cubin(cache_root: Path, capability: str, nvcc_path: Path) -> tuple[Path, dict, dict]:
|
||||
"""Compile outside CuPy's compiler path, which injects -ftz=true in 13.6.
|
||||
|
||||
A cache entry is immutable. Failed/incomplete entries are retained and cause
|
||||
an error on reuse; the caller can select a fresh candidate cache directory.
|
||||
Only the subprocess's temporary-directory environment is changed.
|
||||
"""
|
||||
probe_started = time.perf_counter()
|
||||
_require(capability.isdigit() and 2 <= len(capability) <= 3, "invalid CUDA compute capability")
|
||||
_require(cache_root.is_absolute(), "candidate cache directory must be absolute")
|
||||
_require(nvcc_path.is_absolute() and nvcc_path.is_file(), "nvcc must be an existing absolute compiler path")
|
||||
_require(not any(os.environ.get(key, "").strip()
|
||||
for key in ("NVCC_PREPEND_FLAGS", "NVCC_APPEND_FLAGS")),
|
||||
"NVCC injected flags are incompatible with explicit candidate compiler options")
|
||||
cache_root = cache_root.resolve()
|
||||
nvcc_path = nvcc_path.resolve()
|
||||
version = subprocess.run([str(nvcc_path), "--version"], capture_output=True, text=True,
|
||||
timeout=30, check=False)
|
||||
_require(version.returncode == 0, "nvcc --version failed: " + version.stderr)
|
||||
options = ("--cubin", "-arch=sm_" + capability, *CUDA_OPTIONS, "--keep")
|
||||
identity = dict(schema="conditioning-cuda-nvcc-build-v1",
|
||||
source_sha256=hashlib.sha256(CUDA_SOURCE.encode("utf-8")).hexdigest(),
|
||||
nvcc_path=str(nvcc_path), nvcc_sha256=_file_sha256(nvcc_path),
|
||||
nvcc_version_stdout=version.stdout, nvcc_version_stderr=version.stderr,
|
||||
options=list(options), compute_capability=capability,
|
||||
nvcc_ccbin=os.environ.get("NVCC_CCBIN", ""))
|
||||
key = hashlib.sha256(json.dumps(identity, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
build_dir = cache_root / "eligibility-nvcc" / key
|
||||
_require(not build_dir.is_symlink() and build_dir.resolve().is_relative_to(cache_root),
|
||||
"candidate build cache path escapes the requested directory")
|
||||
cubin = build_dir / "eligibility.cubin"
|
||||
receipt_path = build_dir / "build-receipt.json"
|
||||
probe_seconds = time.perf_counter() - probe_started
|
||||
if build_dir.exists():
|
||||
_require(receipt_path.is_file(), "incomplete nvcc candidate build retained: " + str(build_dir))
|
||||
receipt = json.loads(receipt_path.read_text(encoding="utf-8"))
|
||||
_require(receipt.get("complete") is True and receipt["identity"] == identity,
|
||||
"failed or mismatched nvcc candidate build retained: " + str(build_dir))
|
||||
for name, expected in receipt["files_sha256"].items():
|
||||
path = build_dir / name
|
||||
_require(path.name == name and not path.is_symlink()
|
||||
and path.resolve().is_relative_to(build_dir)
|
||||
and _file_sha256(path) == expected, "nvcc cache artifact changed: " + name)
|
||||
_require(receipt["files_sha256"].get("eligibility.cubin") == _file_sha256(cubin)
|
||||
and receipt["files_sha256"].get("eligibility.cu") == identity["source_sha256"],
|
||||
"nvcc cache lacks authenticated source/cubin")
|
||||
return cubin, dict(cache_key=key, cache_directory=str(build_dir), cache_hit=True,
|
||||
receipt_sha256=_file_sha256(receipt_path), **receipt), dict(
|
||||
compiler_probe_seconds=probe_seconds, nvcc_compile_seconds=0.0)
|
||||
build_dir.mkdir(parents=True, exist_ok=False)
|
||||
source_path = build_dir / "eligibility.cu"
|
||||
with source_path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
stream.write(CUDA_SOURCE)
|
||||
command = [str(nvcc_path), *options, "--keep-dir", str(build_dir),
|
||||
str(source_path), "-o", str(cubin)]
|
||||
_save_json_new(build_dir / "compile-command.json", dict(argv=command, cwd=str(build_dir),
|
||||
temp_directory=str(build_dir), inherited_nvcc_injected_flags=False))
|
||||
_save_json_new(build_dir / "compiler-identity.json", identity)
|
||||
environment = dict(os.environ, TMPDIR=str(build_dir), TMP=str(build_dir), TEMP=str(build_dir))
|
||||
compile_started = time.perf_counter()
|
||||
returncode = None
|
||||
failure = None
|
||||
try:
|
||||
with (build_dir / "compiler.stdout.log").open("xb") as stdout, \
|
||||
(build_dir / "compiler.stderr.log").open("xb") as stderr:
|
||||
process = subprocess.run(command, cwd=build_dir, env=environment,
|
||||
stdout=stdout, stderr=stderr, timeout=120, check=False)
|
||||
returncode = process.returncode
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
failure = f"{type(exc).__name__}: {exc}"
|
||||
compile_seconds = time.perf_counter() - compile_started
|
||||
complete = returncode == 0 and cubin.is_file() and cubin.stat().st_size > 0
|
||||
receipt = dict(complete=complete, identity=identity, command=command,
|
||||
returncode=returncode, failure=failure,
|
||||
nvcc_compile_seconds=compile_seconds,
|
||||
files_sha256={path.name: _file_sha256(path) for path in sorted(build_dir.iterdir())
|
||||
if path.is_file() and not path.is_symlink()})
|
||||
_save_json_new(receipt_path, receipt)
|
||||
if not complete:
|
||||
raise RuntimeError("explicit nvcc candidate compilation failed; evidence retained at " + str(build_dir))
|
||||
return cubin, dict(cache_key=key, cache_directory=str(build_dir), cache_hit=False,
|
||||
receipt_sha256=_file_sha256(receipt_path), **receipt), dict(
|
||||
compiler_probe_seconds=probe_seconds, nvcc_compile_seconds=compile_seconds)
|
||||
|
||||
|
||||
def _require(condition: Any, message: str) -> None:
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
@@ -316,21 +420,24 @@ def cpu_reference(prepared: PreparedEvents, candidate_sources: np.ndarray,
|
||||
|
||||
|
||||
class CudaEligibility:
|
||||
"""CuPy RawKernel candidate with explicit round-to-nearest float32 math.
|
||||
"""Explicit nvcc cubin, loaded by CuPy, with separate float32 rounding.
|
||||
|
||||
Use a dedicated environment and set an absolute CUPY_CACHE_DIR outside any
|
||||
frozen tree before construction. The first run explicitly compiles/loads both
|
||||
kernels. cold_compile_seconds measures that first-use compile/cache-load,
|
||||
and does not prove an empty disk cache. Construction time is reported apart.
|
||||
frozen tree before construction. The first run builds or authenticates one
|
||||
nvcc cubin and loads its kernels with RawModule(path=...). CuPy's compiler
|
||||
is not called, so it cannot inject an incompatible FTZ option. Cold timing
|
||||
separates nvcc compilation, compiler probing and driver module loading.
|
||||
run includes validation, transfers and output ownership in end-to-end wall.
|
||||
"""
|
||||
|
||||
def __init__(self, device_index: int = 0):
|
||||
def __init__(self, device_index: int = 0, *, nvcc_path: str = DEFAULT_NVCC):
|
||||
started = time.perf_counter()
|
||||
self.device_index = _integer(device_index, "device_index", 0)
|
||||
cache_dir = os.environ.get("CUPY_CACHE_DIR", "")
|
||||
_require(bool(cache_dir) and Path(cache_dir).is_absolute(),
|
||||
"set CUPY_CACHE_DIR to an absolute candidate cache directory outside frozen trees")
|
||||
self.cache_root = Path(cache_dir)
|
||||
self.nvcc_path = Path(nvcc_path)
|
||||
try:
|
||||
import cupy as cp
|
||||
except ImportError as exc:
|
||||
@@ -339,8 +446,6 @@ class CudaEligibility:
|
||||
self.device = cp.cuda.Device(self.device_index)
|
||||
with self.device:
|
||||
self.stream = cp.cuda.Stream(non_blocking=True)
|
||||
self.trace_kernel = cp.RawKernel(CUDA_SOURCE, "eligibility_trace", options=CUDA_OPTIONS)
|
||||
self.delta_kernel = cp.RawKernel(CUDA_SOURCE, "eligibility_delta", options=CUDA_OPTIONS)
|
||||
self.device.synchronize()
|
||||
self.initialization_seconds = time.perf_counter() - started
|
||||
self._compiled = False
|
||||
@@ -366,12 +471,20 @@ class CudaEligibility:
|
||||
cp = self.cp
|
||||
first_run = not self._compiled
|
||||
compile_seconds = 0.0
|
||||
compilation_timing = dict(compiler_probe_seconds=0.0, nvcc_compile_seconds=0.0,
|
||||
module_load_seconds=0.0)
|
||||
with self.device, self.stream:
|
||||
if first_run:
|
||||
compile_started = time.perf_counter()
|
||||
self.trace_kernel.compile()
|
||||
self.delta_kernel.compile()
|
||||
cubin, self.compilation_audit, probe_timing = _nvcc_cubin(
|
||||
self.cache_root, str(self.device.compute_capability), self.nvcc_path)
|
||||
compilation_timing.update(probe_timing)
|
||||
load_started = time.perf_counter()
|
||||
self.module = cp.RawModule(path=str(cubin))
|
||||
self.trace_kernel = self.module.get_function("eligibility_trace")
|
||||
self.delta_kernel = self.module.get_function("eligibility_delta")
|
||||
self.stream.synchronize()
|
||||
compilation_timing["module_load_seconds"] = time.perf_counter() - load_started
|
||||
compile_seconds = time.perf_counter() - compile_started
|
||||
self._compiled = True
|
||||
transfer_started = time.perf_counter()
|
||||
@@ -438,7 +551,8 @@ class CudaEligibility:
|
||||
guard_seconds = time.perf_counter() - guard_started
|
||||
timing = dict(first_run=first_run, initialization_seconds=self.initialization_seconds,
|
||||
cold_compile_seconds=compile_seconds,
|
||||
compile_timing_semantics="first-use compile/cache-load; cache coldness externally controlled",
|
||||
**compilation_timing,
|
||||
compile_timing_semantics="first-use nvcc probe/build or authenticated cache, then driver module load",
|
||||
preparation_seconds=prepared.preparation_seconds,
|
||||
run_validation_seconds=validation_seconds,
|
||||
host_to_device_and_allocation_seconds=upload_seconds,
|
||||
@@ -450,9 +564,11 @@ class CudaEligibility:
|
||||
kernel_statistic="median of resident timed repeats",
|
||||
end_to_end_semantics="all requested warmups/repeats and transfers; preparation excluded",
|
||||
end_to_end_seconds=time.perf_counter() - started)
|
||||
audit = _audit(prepared, work, "cupy-rawkernel-independent-candidate")
|
||||
audit = _audit(prepared, work, "nvcc-cubin-cupy-independent-candidate")
|
||||
audit.update(numpy_version=str(np.__version__), cupy_version=str(cp.__version__),
|
||||
device_index=self.device_index, cuda_options=list(CUDA_OPTIONS),
|
||||
compiler_backend="external nvcc --cubin; CuPy RawModule path loading only",
|
||||
compilation=self.compilation_audit,
|
||||
float32_separate_rounding_intrinsics=True, tf32_used=False,
|
||||
numerical_equivalence_established_by_this_call=False)
|
||||
return EligibilityResult(sampled, final, snapshots, output, timing, audit)
|
||||
|
||||
Reference in New Issue
Block a user