"""Consumer artifacts: a caller's CUDA sources compiled against a generated `grid.cuh` under
the store's rules (HJCD-IK ask G9, 2026-10-23).
A consumer such as an IK or trajectory-optimization solver builds one kernel translation unit
per robot against the header GRiD generates for that robot. `build_artifact` runs that build
the way `register_robot` builds its own wrapper: the sources are generated into a staging
directory, hashed with every compile input into a content key, compiled only if that key has
no artifact yet (each unit from the object cache when its exact inputs were compiled before),
and published atomically under a namespace of the cache directory that belongs to the
consumer:
<cache_dir>/consumers/<consumer>/store/<content_key>/ grid.cuh, <artifact_name>, meta.json,
build_inputs.json, build.log
<cache_dir>/consumers/<consumer>/bykey/<input_key> pointer from the request to the content
<cache_dir>/consumers/<consumer>/objects/ per-unit objects, keyed by their inputs
<cache_dir>/consumers/<consumer>/manifest.json name -> content key, under a lock
The identity covers the URDF bytes, the codegen options, the codegen tree, the consumer's
sources and headers by content, the defines and flags, the toolchain, the CUDA architecture
and the package version; a pointer is honoured only when the identity recorded beside the
artifact equals the current one. `load_artifact` returns a prepared artifact without a
toolchain, refusing one recorded for another architecture or key schema. The caller loads the
artifact itself (dlopen, ctypes, its own binding); GRiD never binds the consumer's symbols.
"""
from __future__ import annotations
import concurrent.futures
import fcntl
import hashlib
import json
import logging
import os
import re
import shutil
import subprocess
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence
from . import _cache
from ._cache import (_KEY_SCHEMA, _arch_loadable, _sweep_dead_staging_dirs, canonical_options,
default_cache_dir, detect_cuda_arch, effective_cuda_archs, keymap_lookup,
keymap_record, package_version, read_build_inputs, stale_hit_reasons,
write_build_inputs)
from ._compile import _NVCC_DEFAULT_FLAGS, _build_jobs, find_nvcc, generate_grid_cuh
_log = logging.getLogger("grid_rbd")
ARTIFACT_SCHEMA = 1
_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
# What a load must match: the key schema and the device the artifact was compiled for. The
# toolchain, GLASS content and the codegen tree are provenance, recorded but not required to
# load a prepared artifact on a box that never had them.
LOAD_IDENTITY_KEYS = ("key_schema", "artifact_schema", "cuda_arch")
_SOURCE_SUFFIXES = (".cu", ".cpp", ".cc", ".cxx")
[docs]
class ConsumerArtifactError(RuntimeError):
"""Base of the artifact errors."""
[docs]
class ArtifactNotFoundError(ConsumerArtifactError, KeyError):
"""No artifact of that name in the consumer's manifest."""
[docs]
class ArtifactIncompatibleError(ConsumerArtifactError):
"""The recorded artifact cannot be loaded here (another device or key schema)."""
[docs]
@dataclass(frozen=True)
class ConsumerArtifact:
consumer: str
name: str
path: Path # the shared object
header: Path # the generated grid.cuh it was compiled against
store_dir: Path
content_key: str
input_key: str | None
cuda_arch: int
meta: dict[str, Any]
compiled: bool # nvcc ran in this call (False: reused or loaded)
# ─── paths ──────────────────────────────────────────────────────────────────
def consumer_root(cache_dir: Path, consumer: str) -> Path:
return cache_dir / "consumers" / consumer
def _check_name(value: str, what: str) -> str:
if not isinstance(value, str) or not _NAME_RE.match(value):
raise ValueError(f"{what} must be a short identifier (letters, digits, '_', '.', '-'), got {value!r}")
return value
def _artifact_module_hash() -> str:
try:
return hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
except OSError:
return ""
def _collision_include_dir() -> Path:
import grid_codegen
return Path(grid_codegen.__file__).resolve().parent / "collision"
def _glass_include_dirs() -> list[Path]:
from grid_codegen.resources import resource_path
root = Path(resource_path("GLASS"))
return [root, root / "src"]
def artifact_identity(cuda_arch: int) -> dict[str, Any]:
"""Every artifact-shaping input that is not a caller argument, recorded beside each entry."""
identity: dict[str, Any] = {
"key_schema": _KEY_SCHEMA,
"artifact_schema": ARTIFACT_SCHEMA,
"cuda_arch": int(cuda_arch),
"nvcc": _cache._nvcc_identity(),
"host_cxx": _cache._host_cxx_tag(),
"glass_content": _cache._glass_content_hash(),
"glass_commit": _cache._glass_tag(),
"codegen_tree": _cache._codegen_source_hash(),
"compile_py": _cache._compile_source_hash(),
"artifacts_py": _artifact_module_hash(),
"grid_rbd": package_version(),
"generation_env": _cache._generation_env(),
}
archs = effective_cuda_archs(cuda_arch)
if archs is not None:
identity["cuda_archs"] = archs
return identity
def _file_digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _resolve_files(values: Sequence[str | Path], what: str, suffixes: tuple[str, ...] | None) -> list[Path]:
out: list[Path] = []
for v in values:
p = Path(v).expanduser().resolve()
if not p.is_file():
raise FileNotFoundError(f"{what} not found: {p}")
if suffixes is not None and p.suffix not in suffixes:
raise ValueError(f"{what} {p.name}: expected one of {suffixes}")
out.append(p)
if len({p.name for p in out}) != len(out):
raise ValueError(f"{what} must have distinct file names (each becomes one object): {[p.name for p in out]}")
return out
def _normalize_defines(defines: Sequence[str] | dict[str, Any] | None) -> list[str]:
if not defines:
return []
if isinstance(defines, dict):
items = [k if v is None else f"{k}={v}" for k, v in defines.items()]
else:
items = [str(d) for d in defines]
for d in items:
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*(=.*)?$", d):
raise ValueError(f"define {d!r} is not NAME or NAME=VALUE")
return sorted(items)
# ─── code options (the subset of register_robot's that shape grid.cuh) ────────
def _code_options(urdf_path: Path, cuda_arch: int, *, floating_base, ee_joint_names, dtype, algorithm_list,
enable_mujoco_kernels, cost_frames, contact_frames, use_joint_dynamics, friction_model,
friction_width, runtime_inertia, runtime_transform, runtime_joint_dynamics, emit_plant,
collision, collision_res, collision_native, spherized_urdf, launch_config_profile) -> dict[str, Any]:
if dtype not in ("float32", "float64"):
raise ValueError(f"dtype must be 'float32' or 'float64', got {dtype!r}")
opts: dict[str, Any] = {
"floating_base": bool(floating_base),
"ee_joint_names": list(ee_joint_names) if ee_joint_names else [],
"cuda_arch": int(cuda_arch),
}
if dtype == "float64":
opts["dtype"] = "float64"
if algorithm_list is not None:
if isinstance(algorithm_list, str):
algos = [a.strip() for a in algorithm_list.replace(";", ",").split(",") if a.strip()]
else:
algos = [str(a).strip() for a in algorithm_list if str(a).strip()]
if not algos:
raise ValueError("algorithm_list must name at least one algorithm or profile")
opts["algorithm_list"] = sorted(set(algos))
if not enable_mujoco_kernels:
opts["enable_mujoco_kernels"] = False
if cost_frames:
opts["cost_frames"] = [str(n) for n in cost_frames]
if contact_frames:
opts["contact_frames"] = [str(n) for n in contact_frames]
if use_joint_dynamics:
opts["use_joint_dynamics"] = True
if friction_model not in ("sign", "tanh"):
raise ValueError(f"friction_model must be 'sign' or 'tanh', got {friction_model!r}")
if friction_model == "tanh" or friction_width is not None:
if not use_joint_dynamics:
raise ValueError("friction_model='tanh' / friction_width require use_joint_dynamics=True")
if friction_model != "tanh":
raise ValueError("friction_width applies to friction_model='tanh' only")
opts["friction_model"] = "tanh"
if friction_width is not None:
try:
opts["friction_width"] = [float(w) for w in friction_width]
except TypeError:
opts["friction_width"] = float(friction_width)
for flag, value in (("runtime_inertia", runtime_inertia), ("runtime_transform", runtime_transform),
("runtime_joint_dynamics", runtime_joint_dynamics)):
if value:
opts[flag] = True
if emit_plant is not None:
opts["emit_plant"] = bool(emit_plant)
else:
opts["emit_plant"] = None # the generator's rule (E1) decides; recorded so it is keyed
if sum(bool(x) for x in (collision, collision_native, spherized_urdf)) > 1:
raise ValueError("pass one of collision=True, collision_native=True or spherized_urdf=")
if spherized_urdf:
sp = Path(spherized_urdf).expanduser().resolve()
if not sp.is_file():
raise FileNotFoundError(f"spherized_urdf not found: {sp}")
opts["spherized_urdf"] = str(sp)
opts["spherized_urdf_sha256"] = _file_digest(sp) # the key follows the model's content
elif collision_native:
opts["collision_native"] = True
opts["collision_res"] = float(collision_res) if not isinstance(collision_res, (list, tuple)) \
else [float(r) for r in collision_res]
elif collision:
opts["collision"] = True
opts["collision_res"] = float(collision_res) if not isinstance(collision_res, (list, tuple)) \
else [float(r) for r in collision_res]
if launch_config_profile is not None:
opts["launch_config_profile"] = str(launch_config_profile)
# The baked per-algorithm launch configuration is not derivable from the URDF bytes; fold
# the resolved values in so a re-autotune re-keys the artifact (as register_robot does).
try:
from grid_codegen.GRiDCodeGenerator import load_launch_config
from grid_codegen.launch_config import select_launch_config_gpu
from ._compile import _resolve_launch_config_robot
lc_robot = _resolve_launch_config_robot(str(urdf_path))
lc_gpu = select_launch_config_gpu(lc_robot, cuda_arch)
opts["launch_config_gpu"] = lc_gpu
lc = load_launch_config(lc_robot, bool(floating_base), gpu=lc_gpu,
profile=opts.get("launch_config_profile", "ffi"))
if lc:
opts["launch_config"] = {k: dict(v) for k, v in sorted(lc.items())}
except Exception:
pass
return opts
# ─── keys ───────────────────────────────────────────────────────────────────
def _input_key(urdf_bytes: bytes, code_options: dict[str, Any], sources: list[Path], headers: list[Path],
include_dirs: list[Path], defines: list[str], extra_flags: list[str], link_flags: list[str],
artifact_name: str, identity: dict[str, Any]) -> str:
h = hashlib.sha256()
h.update(urdf_bytes)
h.update(canonical_options(code_options).encode())
for p in sources:
h.update(b"source:" + p.name.encode() + b"\0" + p.read_bytes() + b"\0")
for p in headers:
h.update(b"header:" + p.name.encode() + b"\0" + p.read_bytes() + b"\0")
h.update(("include_dirs=" + ";".join(str(d) for d in include_dirs)).encode())
h.update(("defines=" + ";".join(defines)).encode())
h.update(("extra_flags=" + ";".join(extra_flags)).encode())
h.update(("link_flags=" + ";".join(link_flags)).encode())
h.update(("artifact_name=" + artifact_name).encode())
h.update(("identity=" + canonical_options(identity)).encode())
return h.hexdigest()
def _toolchain_tag() -> str:
return (f"nvcc={_cache._nvcc_identity()};host_cxx={_cache._host_cxx_tag()};"
f"glass={_cache._glass_content_hash()};key_schema={_KEY_SCHEMA};artifact_schema={ARTIFACT_SCHEMA}")
def _content_key(gen_dir: Path, sources: list[Path], headers: list[Path], compile_cmd: list[str],
link_flags: list[str], artifact_name: str) -> str:
"""Exactly what nvcc sees: the generated header, the static collision header, the
consumer's sources and headers, the flag list (the staging dir's own -I removed; its
content is hashed), the link flags and the toolchain."""
h = hashlib.sha256()
h.update((gen_dir / "grid.cuh").read_bytes())
h.update(_collision_include_dir().joinpath("grid_collision_geometry.cuh").read_bytes())
for p in sources:
h.update(b"source:" + p.name.encode() + b"\0" + p.read_bytes() + b"\0")
for p in headers:
h.update(b"header:" + p.name.encode() + b"\0" + p.read_bytes() + b"\0")
src = str(gen_dir)
for a in compile_cmd[1:]:
if a == f"-I{src}" or a.startswith(src + "/"):
continue
h.update(a.encode() + b"\0")
h.update(("link_flags=" + ";".join(link_flags) + ";artifact_name=" + artifact_name).encode())
h.update(_toolchain_tag().encode())
return h.hexdigest()
def _unit_object_key(unit: Path, gen_dir: Path, headers: list[Path], compile_cmd: list[str]) -> str:
h = hashlib.sha256()
src = str(gen_dir)
for a in compile_cmd[1:]:
if a == f"-I{src}" or a.startswith(src + "/"):
continue
h.update(a.encode() + b"\0")
h.update((gen_dir / "grid.cuh").read_bytes()); h.update(b"\0")
h.update(_collision_include_dir().joinpath("grid_collision_geometry.cuh").read_bytes()); h.update(b"\0")
for p in headers:
h.update(p.read_bytes()); h.update(b"\0")
h.update(unit.read_bytes())
h.update(_toolchain_tag().encode())
return h.hexdigest()
# ─── the compile half ─────────────────────────────────────────────────────────
def _compile_command(gen_dir: Path, cuda_arch: int, include_dirs: list[Path], defines: list[str],
extra_flags: list[str]) -> list[str]:
nvcc = find_nvcc()
archs = effective_cuda_archs(cuda_arch)
gencode_archs = archs if archs is not None else [cuda_arch]
cmd = [nvcc] + list(_NVCC_DEFAULT_FLAGS) + [f"-gencode=arch=compute_{a},code=sm_{a}" for a in gencode_archs]
cmd.append(f"-I{gen_dir}")
cmd.append(f"-I{_collision_include_dir()}")
for d in _glass_include_dirs():
cmd.append(f"-I{d}")
for d in include_dirs:
cmd.append(f"-I{d}")
for d in defines:
cmd.append(f"-D{d}")
cmd.extend(extra_flags)
return cmd
def _build(gen_dir: Path, sources: list[Path], headers: list[Path], compile_cmd: list[str], link_flags: list[str],
out_so: Path, object_cache_dir: Path, reuse_objects: bool) -> dict[str, Any]:
"""Compile each consumer source with -c (from the object cache when its key exists), then
host-link the objects into `out_so`. Returns a record of what ran."""
nvcc = compile_cmd[0]
jobs = _build_jobs()
build_dir = gen_dir / "build"
build_dir.mkdir(exist_ok=True)
object_cache_dir.mkdir(parents=True, exist_ok=True)
log_path = gen_dir / "build.log"
lock = threading.Lock()
with log_path.open("w") as log:
log.write("$ " + " ".join(compile_cmd) + " -c <unit> -o <unit>.o # x" + str(len(sources)) + "\n")
log.write("$ " + " ".join([nvcc, "--shared", "-o", str(out_so), "<units>.o"] + link_flags) + "\n\n")
record: dict[str, Any] = {"units": {}}
def one(unit: Path) -> tuple[Path, int, float, str]:
key = _unit_object_key(unit, gen_dir, headers, compile_cmd)
cached = object_cache_dir / f"{unit.stem}-{key}.o"
if reuse_objects and cached.is_file():
os.utime(cached)
with lock, log_path.open("a") as log:
log.write(f"[{unit.stem}] cached {cached.name}\n")
record["units"][unit.name] = {"object": cached.name, "compiled": False}
return cached, 0, 0.0, ""
obj = build_dir / (unit.stem + ".o")
ulog = build_dir / (unit.stem + ".log")
t0 = time.monotonic()
with ulog.open("w") as f:
r = subprocess.run(compile_cmd + ["-c", str(unit), "-o", str(obj)], stdout=f, stderr=subprocess.STDOUT)
secs = time.monotonic() - t0
with lock, log_path.open("a") as log:
log.write(f"[{unit.stem}] rc={r.returncode} {secs:.1f}s\n")
if r.returncode == 0:
tmp = cached.with_name(cached.name + f".{os.getpid()}.tmp")
shutil.move(str(obj), str(tmp))
os.replace(tmp, cached)
obj = cached
record["units"][unit.name] = {"object": cached.name, "compiled": True, "seconds": round(secs, 1)}
return obj, r.returncode, secs, ulog.read_text()
with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as ex:
results = list(ex.map(one, sources))
failed = [(obj, rc, out) for obj, rc, _, out in results if rc != 0]
if failed:
obj, rc, out = failed[0]
raise RuntimeError(f"nvcc exit {rc} compiling the consumer unit {obj.stem}:\n{out}")
objs = [str(obj) for obj, _, _, _ in results]
link = [nvcc, "--shared", "-o", str(out_so)] + objs + link_flags
t0 = time.monotonic()
with log_path.open("a") as log:
log.write("\n$ " + " ".join(link) + "\n")
log.flush()
r = subprocess.run(link, stdout=log, stderr=subprocess.STDOUT)
log.write(f"[link] rc={r.returncode} {time.monotonic() - t0:.1f}s\n")
if r.returncode != 0:
raise RuntimeError(f"nvcc exit {r.returncode} linking the consumer artifact; build log:\n" + log_path.read_text())
shutil.rmtree(build_dir, ignore_errors=True)
record["compiled_units"] = sum(1 for _, _, secs, _ in results if secs > 0)
return record
# ─── manifest (one per consumer, under a lock) ───────────────────────────────
def _manifest_path(root: Path) -> Path:
return root / "manifest.json"
def _load_manifest(root: Path) -> dict[str, Any]:
path = _manifest_path(root)
if not path.exists():
return {"artifact_schema": ARTIFACT_SCHEMA, "artifacts": {}}
data = json.loads(path.read_text())
if data.get("artifact_schema") != ARTIFACT_SCHEMA:
raise ConsumerArtifactError(
f"{path} was written with artifact schema {data.get('artifact_schema')!r}; this grid_rbd expects "
f"{ARTIFACT_SCHEMA}. Point cache_dir= at a fresh directory and build the artifacts again.")
return data
def _manifest_register(root: Path, name: str, content_key: str, meta: dict[str, Any]) -> None:
root.mkdir(parents=True, exist_ok=True)
with (root / "manifest.lock").open("a+") as lock:
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
try:
manifest = _load_manifest(root)
manifest["artifacts"][name] = {"content_key": content_key, **meta}
tmp = _manifest_path(root).with_suffix(f".json.{os.getpid()}.tmp")
tmp.write_text(json.dumps(manifest, indent=2, sort_keys=True))
tmp.replace(_manifest_path(root))
finally:
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
# ─── public operations ───────────────────────────────────────────────────────
[docs]
def build_artifact(consumer: str, name: str, urdf_path: str | Path, *,
sources: Sequence[str | Path],
headers: Sequence[str | Path] = (),
include_dirs: Sequence[str | Path] = (),
defines: Sequence[str] | dict[str, Any] | None = None,
extra_flags: Sequence[str] = (),
link_flags: Sequence[str] = (),
artifact_name: str = "artifact.so",
cache_dir: str | Path | None = None,
cuda_arch: int | None = None,
force_rebuild: bool = False,
plan_only: bool = False,
floating_base: bool = False,
ee_joint_names: Sequence[str] | None = None,
dtype: str = "float32",
algorithm_list: Sequence[str] | str | None = None,
enable_mujoco_kernels: bool = True,
cost_frames: Sequence[str] | None = None,
contact_frames: Sequence[str] | None = None,
use_joint_dynamics: bool = False,
friction_model: str = "sign",
friction_width=None,
runtime_inertia: bool = False,
runtime_transform: bool = False,
runtime_joint_dynamics: bool = False,
emit_plant: bool | None = None,
collision: bool = False,
collision_res: float | Sequence[float] = 0.05,
collision_native: bool = False,
spherized_urdf: str | Path | None = None,
launch_config_profile: str | None = None) -> ConsumerArtifact | dict[str, Any]:
"""Build (or reuse) the consumer's artifact for one robot and return where it is.
`consumer` names the namespace in the cache directory (one per consuming project);
`name` the artifact within it (one per robot and configuration). `sources` are the
consumer's translation units, each compiled with `-c` against the generated `grid.cuh`
(plus the static collision header and the GLASS include directories) and linked with
`--shared` into `artifact_name`; `headers` are the consumer's own header files, folded
into the identity by content (an include directory is folded by path only, so list the
headers whose edits must re-key the build); `defines` are `NAME` or `NAME=VALUE`;
`extra_flags` go to every unit compile, `link_flags` to the link.
The codegen keywords (`floating_base`, `ee_joint_names`, `dtype`, `algorithm_list`,
`enable_mujoco_kernels`, `cost_frames`, `contact_frames`, the joint-dynamics and runtime
knobs) mean what they mean on `register_robot`; `emit_plant` is the generator's plant-block
switch (None: the generator's rule), and `collision`, `collision_res`, `collision_native`
and `spherized_urdf` select the generated `grid_collision` namespace (a sphere model is
validated against the URDF). `launch_config_profile` (default: the bindings' "ffi"
profile) names the baked launch-configuration profile.
`plan_only=True` reports what a build would use and whether it is already there, without
generating or compiling. `force_rebuild=True` recompiles every unit."""
_check_name(consumer, "consumer")
_check_name(name, "name")
_check_name(artifact_name, "artifact_name")
urdf_p = Path(urdf_path).expanduser().resolve()
if not urdf_p.is_file():
raise FileNotFoundError(f"URDF file not found: {urdf_p}")
urdf_bytes = urdf_p.read_bytes()
srcs = _resolve_files(list(sources), "source", _SOURCE_SUFFIXES)
if not srcs:
raise ValueError("sources must name at least one translation unit")
hdrs = _resolve_files(list(headers), "header", None)
incs = [Path(d).expanduser().resolve() for d in include_dirs]
for d in incs:
if not d.is_dir():
raise FileNotFoundError(f"include_dirs entry is not a directory: {d}")
defs = _normalize_defines(defines)
xflags = [str(f) for f in extra_flags]
lflags = [str(f) for f in link_flags]
cache_root = Path(cache_dir).expanduser() if cache_dir else default_cache_dir()
root = consumer_root(cache_root, consumer)
if cuda_arch is None:
cuda_arch = detect_cuda_arch()
if cuda_arch == 0:
raise RuntimeError("Could not detect the GPU compute capability; pass cuda_arch= explicitly "
"(for example cuda_arch=120 for an sm_120 card).")
code_options = _code_options(
urdf_p, cuda_arch, floating_base=floating_base, ee_joint_names=ee_joint_names, dtype=dtype,
algorithm_list=algorithm_list, enable_mujoco_kernels=enable_mujoco_kernels, cost_frames=cost_frames,
contact_frames=contact_frames, use_joint_dynamics=use_joint_dynamics, friction_model=friction_model,
friction_width=friction_width, runtime_inertia=runtime_inertia, runtime_transform=runtime_transform,
runtime_joint_dynamics=runtime_joint_dynamics, emit_plant=emit_plant, collision=collision,
collision_res=collision_res, collision_native=collision_native, spherized_urdf=spherized_urdf,
launch_config_profile=launch_config_profile)
identity = artifact_identity(cuda_arch)
input_key = _input_key(urdf_bytes, code_options, srcs, hdrs, incs, defs, xflags, lflags, artifact_name, identity)
store = root / "store"
def _entry_state(entry_dir: Path) -> list[str]:
if not ((entry_dir / artifact_name).exists() and (entry_dir / "meta.json").exists()):
return ["entry incomplete (artifact or meta.json missing)"]
return stale_hit_reasons(entry_dir, identity)
if plan_only:
mapped = keymap_lookup(root, input_key)
entry_dir = store / mapped if mapped else None
stale = _entry_state(entry_dir) if entry_dir is not None else None
return {
"consumer": consumer, "name": name, "cache_dir": str(cache_root), "cuda_arch": cuda_arch,
"code_options": code_options, "identity": identity, "input_key": input_key,
"sources": [str(p) for p in srcs], "headers": [str(p) for p in hdrs], "defines": defs,
"content_key": mapped if (mapped and stale == []) else None,
"cached": bool(mapped) and stale == [], "stale_reasons": stale,
"store_dir": str(entry_dir) if (mapped and stale == []) else None,
"would": "load" if (mapped and stale == []) else "generate+compile (the content key decides whether nvcc runs)",
}
if not force_rebuild:
mapped = keymap_lookup(root, input_key)
if mapped:
entry_dir = store / mapped
stale = _entry_state(entry_dir)
if not stale:
meta = json.loads((entry_dir / "meta.json").read_text())
_manifest_register(root, name, mapped, meta)
return ConsumerArtifact(consumer, name, entry_dir / artifact_name, entry_dir / "grid.cuh",
entry_dir, mapped, input_key, cuda_arch, meta, False)
_log.info("grid_rbd: artifact pointer for %s/%s rejected (%s); regenerating", consumer, name, "; ".join(stale))
store.mkdir(parents=True, exist_ok=True)
_sweep_dead_staging_dirs(store)
gen_dir = store / f".gen_{os.getpid()}_{input_key[:8]}"
shutil.rmtree(gen_dir, ignore_errors=True)
gen_dir.mkdir(parents=True, exist_ok=True)
compiled = False
try:
generate_grid_cuh(urdf_p, code_options, gen_dir / "grid.cuh")
compile_cmd = _compile_command(gen_dir, cuda_arch, incs, defs, xflags)
content_key = _content_key(gen_dir, srcs, hdrs, compile_cmd, lflags, artifact_name)
entry_dir = store / content_key
if (entry_dir / artifact_name).exists() and (entry_dir / "meta.json").exists() and not force_rebuild:
meta = json.loads((entry_dir / "meta.json").read_text())
if read_build_inputs(entry_dir) is None:
write_build_inputs(entry_dir, identity)
else:
_log.info("grid_rbd: building consumer artifact %s/%s (%d unit(s))", consumer, name, len(srcs))
t0 = time.monotonic()
record = _build(gen_dir, srcs, hdrs, compile_cmd, lflags, gen_dir / artifact_name,
root / "objects", reuse_objects=not force_rebuild)
compiled = record["compiled_units"] > 0
meta = {
"consumer": consumer, "name": name, "artifact": artifact_name,
"urdf": str(urdf_p), "urdf_sha256": hashlib.sha256(urdf_bytes).hexdigest(),
"sources": {p.name: _file_digest(p) for p in srcs},
"headers": {p.name: _file_digest(p) for p in hdrs},
"include_dirs": [str(d) for d in incs], "defines": defs,
"extra_flags": xflags, "link_flags": lflags,
"code_options": code_options, "cuda_arch": int(cuda_arch),
"grid_rbd": package_version(), "artifact_schema": ARTIFACT_SCHEMA,
"build_seconds": round(time.monotonic() - t0, 1), "build": record,
}
archs = effective_cuda_archs(cuda_arch)
if archs is not None:
meta["cuda_archs"] = archs
(gen_dir / "meta.json").write_text(json.dumps(meta, indent=2, sort_keys=True))
write_build_inputs(gen_dir, identity)
if force_rebuild and entry_dir.exists():
shutil.rmtree(entry_dir)
try:
os.replace(gen_dir, entry_dir)
except OSError:
meta = json.loads((entry_dir / "meta.json").read_text())
finally:
shutil.rmtree(gen_dir, ignore_errors=True)
keymap_record(root, input_key, content_key)
_manifest_register(root, name, content_key, meta)
return ConsumerArtifact(consumer, name, entry_dir / artifact_name, entry_dir / "grid.cuh",
entry_dir, content_key, input_key, cuda_arch, meta, compiled)
[docs]
def load_artifact(consumer: str, name: str, cache_dir: str | Path | None = None,
cuda_arch: int | None = None) -> ConsumerArtifact:
"""The prepared artifact `name` of `consumer`, without a toolchain: the manifest names it,
the entry must be whole and recorded for this device (or one of a fat build's devices) and
key schema. With no detectable GPU and no `cuda_arch`, the recorded device is trusted."""
_check_name(consumer, "consumer")
_check_name(name, "name")
cache_root = Path(cache_dir).expanduser() if cache_dir else default_cache_dir()
root = consumer_root(cache_root, consumer)
entry = _load_manifest(root)["artifacts"].get(name) if _manifest_path(root).exists() else None
if entry is None:
raise ArtifactNotFoundError(f"no artifact {name!r} for consumer {consumer!r} under {cache_root}")
entry_dir = root / "store" / entry["content_key"]
artifact_name = entry.get("artifact", "artifact.so")
if not (entry_dir / artifact_name).exists() or not (entry_dir / "meta.json").exists():
raise ArtifactIncompatibleError(f"artifact {name!r} of {consumer!r} is incomplete at {entry_dir}")
recorded = read_build_inputs(entry_dir)
if recorded is None:
raise ArtifactIncompatibleError(f"artifact {name!r} of {consumer!r} predates the identity record")
if cuda_arch is None:
cuda_arch = detect_cuda_arch()
reasons = []
if recorded.get("key_schema") != _KEY_SCHEMA:
reasons.append(f"key_schema: recorded {recorded.get('key_schema')!r} != current {_KEY_SCHEMA!r}")
if recorded.get("artifact_schema") != ARTIFACT_SCHEMA:
reasons.append(f"artifact_schema: recorded {recorded.get('artifact_schema')!r} != current {ARTIFACT_SCHEMA!r}")
if cuda_arch and not _arch_loadable(recorded, int(cuda_arch)):
reasons.append(f"cuda_arch: recorded {recorded.get('cuda_arch')!r} (archs {recorded.get('cuda_archs')!r}) != current {cuda_arch!r}")
if reasons:
raise ArtifactIncompatibleError(f"artifact {name!r} of {consumer!r} cannot be loaded here: " + "; ".join(reasons))
meta = json.loads((entry_dir / "meta.json").read_text())
return ConsumerArtifact(consumer, name, entry_dir / artifact_name, entry_dir / "grid.cuh", entry_dir,
entry["content_key"], None, int(recorded.get("cuda_arch", cuda_arch or 0)), meta, False)
[docs]
def list_artifacts(consumer: str, cache_dir: str | Path | None = None) -> list[dict[str, Any]]:
"""Every artifact the consumer's manifest names, with its metadata."""
_check_name(consumer, "consumer")
cache_root = Path(cache_dir).expanduser() if cache_dir else default_cache_dir()
root = consumer_root(cache_root, consumer)
if not _manifest_path(root).exists():
return []
return [{"name": n, **e} for n, e in _load_manifest(root)["artifacts"].items()]