Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ tasks/flydsl2flydsl/*/.rocprofv3/
tasks/flydsl2flydsl/*/performance_report.json
# SIKL generation drafts, source snapshots and validation evidence.
sikl_task_builder_runs/
# Tasks generated from a SIKL v5 checkout; they track SIKL commits and are rebuilt, not committed.
/tasks/SIKL-v5/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The platform provides:
- **Resumable experiments**: Resume a run without repeating tasks whose framework completion and source evidence remain valid.
- **Held-out evaluation**: Test optimized kernels on unseen shapes and measure the generalization gap.
- **Task validation and visualization**: Validate task quality with a dedicated agent and compare local run reports in a dashboard.
- **SIKL task authoring**: Generate, repair, and validate standard Arena tasks from a dataset folder with [`sikl_task_builder`](docs/how-to/sikl-task-builder.md).
- **SIKL task authoring**: Generate and validate standard Arena tasks from a SIKL checkout or a legacy dataset folder with [`sikl_task_builder`](docs/how-to/sikl-task-builder.md).

AgentKernelArena supplies an environment and objective reward signals; it does not currently include an RL trainer, replay buffer, or policy-update loop. Its per-task workspaces provide reproducibility and concurrent-run separation, not a security sandbox: agent processes run permissively inside a privileged container and can access mounted repository and authentication state.

Expand Down
2 changes: 1 addition & 1 deletion agents/sikl_task_builder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""SIKL task generation campaign (runs before Arena task discovery)."""

BUILDER_VERSION = 2
BUILDER_VERSION = 3
69 changes: 52 additions & 17 deletions agents/sikl_task_builder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import argparse
import json
import logging
import sys
from pathlib import Path

from .bundle import inspect_bundle
from .config import load_config
from .orchestrator import REPO, resolve_paths, run
from .bundle import ImportProblem, inspect_bundle
from .checkout import check_mountable, inspect_checkout
from .config import CheckoutConfig, load_config
from .orchestrator import REPO, resolve_paths, run, run_checkout


def main(argv=None):
Expand All @@ -19,9 +21,19 @@ def main(argv=None):
parser.add_argument("--input-dir")
parser.add_argument("--run-id")
args = parser.parse_args(argv)
config = load_config(args.config)
if args.input_dir:
config.input_dir = args.input_dir
if args.action == "resume" and not args.run_id:
parser.error("resume requires --run-id")
if args.action == "run" and args.run_id:
parser.error("use resume with --run-id")
# The Docker runner captures mount-info's stdout, so its errors go where the user sees them.
errors = sys.stderr if args.action == "mount-info" else sys.stdout
try:
config = load_config(args.config, args.input_dir)
if isinstance(config, CheckoutConfig):
return checkout_action(args, config)
except ImportProblem as exc:
print(json.dumps({"ok": False, "code": exc.code, "message": str(exc)}), file=errors)
return 1
if args.action == "inspect":
tasks = inspect_bundle(Path(config.input_dir), config.selections)
result = {"ok": True, "tasks": [t.summary() for t in tasks],
Expand All @@ -30,24 +42,47 @@ def main(argv=None):
source, output, artifacts = resolve_paths(config)
if not source.is_dir():
raise ValueError(f"Missing input directory: {source}")
values = [str(source), str(args.config.resolve()), str(artifacts.relative_to(REPO)),
str(output.relative_to(REPO)), " ".join(sorted({"codex", config.validator.get("backend", "codex")}))]
if any("\n" in v or ":" in v for v in values):
raise ValueError("Mount paths cannot contain newline or colon characters")
artifacts.mkdir(parents=True, exist_ok=True)
output.mkdir(parents=True, exist_ok=True)
print("\n".join(values))
print_mounts(args, config, source, output, artifacts,
" ".join(sorted({"codex", config.validator.get("backend", "codex")})))
return 0
else:
if args.action == "resume" and not args.run_id:
parser.error("resume requires --run-id")
if args.action == "run" and args.run_id:
parser.error("use resume with --run-id")
logging.basicConfig(level=logging.INFO)
result = run(config, args.run_id)
print(json.dumps(result, indent=2))
return 0 if result["ok"] else 1


def checkout_action(args, config: CheckoutConfig) -> int:
from src.preprocessing import _resolve_gfx_arch
source, output, artifacts = resolve_paths(config)
if args.action == "inspect":
commit, tasks = inspect_checkout(source, config.tasks)
arch = _resolve_gfx_arch(config.target_gpu_model)
result = {"ok": True, "input": "sikl-v5", "commit": commit,
"tasks": [t.summary(arch, config.target_gpu_model) for t in tasks],
"task_count": len(tasks), "case_count": sum(len(t.cases) for t in tasks)}
elif args.action == "mount-info":
inspect_checkout(source, config.tasks)
check_mountable(source)
# A placeholder, not an empty line: `$(...)` would drop a trailing empty line.
print_mounts(args, config, source, output, artifacts, "none")
return 0
else:
logging.basicConfig(level=logging.INFO)
result = run_checkout(config, args.run_id)
print(json.dumps(result, indent=2))
return 0 if result["ok"] else 1


def print_mounts(args, config, source: Path, output: Path, artifacts: Path, agents: str) -> None:
values = [str(source), str(args.config.resolve()), str(artifacts.relative_to(REPO)),
str(output.relative_to(REPO)), agents]
if any("\n" in v or ":" in v for v in values):
raise ValueError("Mount paths cannot contain newline or colon characters")
artifacts.mkdir(parents=True, exist_ok=True)
output.mkdir(parents=True, exist_ok=True)
print("\n".join(values))


if __name__ == "__main__":
raise SystemExit(main())
162 changes: 162 additions & 0 deletions agents/sikl_task_builder/checkout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Read committed SIKL v5 schemas from a git checkout without importing or executing them."""

from __future__ import annotations

import json
import os
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path

from .bundle import ImportProblem, parse_json

SCHEMA_PATH = re.compile(r"^sikl/ops/(?P<op>[a-z0-9_]+)/impls/(?P<language>flydsl|triton)/"
r"(?P<name>[a-z0-9_]+)/(?P<uuid>[0-9a-f]{16})\.json$")
SOURCE_PATH = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\.py$")
IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
# SIKL's code-lint check-added-large-files limit (default maxkb=500).
MAX_SCHEMA_BYTES = 500 * 1024


def is_checkout(path: Path) -> bool:
return (Path(path) / "sikl" / "ops").is_dir()


def git(root: Path, *args: str) -> bytes:
# Neither inherited GIT_* variables nor global/system configuration or hooks may steer these reads.
env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM=os.devnull, GIT_CONFIG_GLOBAL=os.devnull,
GIT_TERMINAL_PROMPT="0")
command = ["git", "-c", "core.hooksPath=/dev/null", "-c", f"safe.directory={root}", "-C", str(root), *args]
result = subprocess.run(command, env=env, capture_output=True)
if result.returncode:
raise ImportProblem("not_a_sikl_checkout",
f"git {' '.join(args[:2])} in {root}: {result.stderr.decode(errors='replace').strip()}")
return result.stdout


def _validate_schema(path: str, match: re.Match, data) -> None:
def invalid(reason):
return ImportProblem("invalid_schema", f"{path}: {reason}")
if not isinstance(data, dict):
raise invalid("not a JSON object")
if type(data.get("schema_version")) is not int or data["schema_version"] != 5:
raise ImportProblem("unsupported_schema_version", f"{path}: schema_version {data.get('schema_version')!r}")
for key, part in (("name", "name"), ("op_type", "op")):
if data.get(key) != match[part]:
raise invalid(f"{key} {data.get(key)!r} != {match[part]!r}")
kernel = data.get("kernel")
if not isinstance(kernel, dict) or kernel.get("language") != match["language"]:
raise invalid(f"kernel.language is not {match['language']!r}")
sources = kernel.get("sources")
if not isinstance(sources, list) or not sources:
raise invalid("kernel.sources is empty")
paths = []
for source in sources:
if not isinstance(source, dict) or set(source) != {"path", "content"}:
raise invalid("a kernel source is not {path, content}")
name, content = source["path"], source["content"]
if not isinstance(name, str) or not SOURCE_PATH.match(name) or name == "__init__.py":
raise invalid(f"source path {name!r} is not one <identifier>.py segment")
if not isinstance(content, str) or not content:
raise invalid(f"source {name} is empty")
paths.append(name)
if paths != sorted(set(paths)):
raise invalid("kernel.sources paths are not sorted and unique")
entry = kernel.get("entry_point")
file, _, symbol = entry.partition("::") if isinstance(entry, str) else ("", "", "")
if file not in paths or not IDENTIFIER.match(symbol):
raise invalid(f"entry_point {entry!r} is not <source path>::<identifier>")
targets = kernel.get("targets")
if not isinstance(targets, list) or not targets or any(
not isinstance(t, dict) or not t or set(t) - {"arch", "hardware"}
or any(not isinstance(v, str) or not v for v in t.values()) for t in targets):
raise invalid("kernel.targets must be a non-empty list of {arch, hardware} subsets")
workloads = data.get("workloads")
if not isinstance(workloads, list) or not workloads:
raise invalid("workloads is empty")
seen = set()
for workload in workloads:
axes = workload.get("axes") if isinstance(workload, dict) else None
if not isinstance(axes, dict) or not axes or any(
not isinstance(k, str) or type(v) is not int for k, v in axes.items()):
raise invalid("a workload's axes is not a non-empty {str: int}")
key = json.dumps(axes, sort_keys=True)
if key in seen:
raise invalid(f"duplicate workload axes {key}")
seen.add(key)


@dataclass(frozen=True)
class SchemaTask:
path: str
op: str
language: str
name: str
uuid: str
oid: str
text: str
data: dict

@property
def task_id(self) -> str:
return f"{self.name}/{self.uuid}"

@property
def cases(self) -> list[str]:
return [",".join(f"{a}={v}" for a, v in w["axes"].items()) for w in self.data["workloads"]]

def supports(self, arch: str | None, gpu_model: str) -> bool:
# SIKL's DeviceTarget.normalize_hardware: drop the "AMD Instinct " prefix, compare upper case.
targets = self.data["kernel"]["targets"]
hardware = [t.get("hardware", "").strip().upper().removeprefix("AMD INSTINCT ") for t in targets]
return any(t.get("arch") == arch for t in targets) or gpu_model.upper() in hardware

def summary(self, arch: str | None, gpu_model: str) -> dict:
return {"task_id": self.task_id, "path": self.path, "op_type": self.op, "language": self.language,
"cases": len(self.cases), "targets": self.data["kernel"]["targets"],
"platform_supported": self.supports(arch, gpu_model)}


def inspect_checkout(root: Path, selectors: list[str]) -> tuple[str, list[SchemaTask]]:
"""Return HEAD and its schemas; uncommitted files never become tasks."""
root = Path(root).resolve()
if not root.is_dir() or Path(git(root, "rev-parse", "--show-toplevel").decode().strip()).resolve() != root:
raise ImportProblem("not_a_sikl_checkout", f"{root} is not the top level of a git checkout")
commit = git(root, "rev-parse", "--verify", "HEAD^{commit}").decode().strip()
tasks = []
for entry in git(root, "ls-tree", "-r", "-z", "-l", "--full-tree", "HEAD", "--", "sikl/ops").split(b"\0"):
meta, _, path = entry.decode(errors="replace").partition("\t")
match = SCHEMA_PATH.match(path)
if not match:
continue
mode, kind, oid, size = meta.split()
if mode != "100644" or kind != "blob":
raise ImportProblem("bad_mode", f"{path}: mode {mode} {kind}")
if int(size) > MAX_SCHEMA_BYTES:
raise ImportProblem("schema_too_large", f"{path}: {size} bytes > {MAX_SCHEMA_BYTES}")
try:
text = git(root, "cat-file", "blob", oid).decode("utf-8")
data = parse_json(text, path)
except (UnicodeDecodeError, ImportProblem) as exc:
raise ImportProblem("invalid_schema", f"{path}: {exc}") from exc
_validate_schema(path, match, data)
tasks.append(SchemaTask(path, match["op"], match["language"], match["name"], match["uuid"], oid, text, data))
if not tasks:
raise ImportProblem("no_schemas", f"no sikl/ops/<op>/impls/<language>/<name>/<uuid>.json at {commit}")
if selectors:
missing = sorted(set(selectors) - {t.name for t in tasks} - {t.task_id for t in tasks})
if missing:
raise ValueError(f"Unknown task selectors: {missing}")
tasks = [t for t in tasks if t.name in selectors or t.task_id in selectors]
return commit, tasks


def check_mountable(root: Path) -> None:
# The Docker runner mounts only this directory; its object store must be self-contained.
if not (root / ".git").is_dir():
raise ImportProblem("checkout_not_mountable", f"{root}/.git is not a directory (git worktree?)")
if (root / ".git" / "objects" / "info" / "alternates").exists():
raise ImportProblem("checkout_not_mountable",
f"{root} borrows objects through alternates; run `git repack -a -d` and remove them")
39 changes: 38 additions & 1 deletion agents/sikl_task_builder/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,45 @@ def arena_task_id(self, definition: str) -> str:
return (output.relative_to(repo / "tasks") / definition).as_posix()


def load_config(path: Path) -> Config:
@dataclass
class CheckoutConfig:
"""A SIKL v5 checkout input; legacy bundle keys have no meaning here."""

input_dir: str
output_dir: str = "tasks/SIKL-v5"
artifact_root: str = "sikl_task_builder_runs"
target_gpu_model: str = "MI355X"
tasks: list[str] = field(default_factory=list)
command_timeout: int = 3600
max_task_seconds: int = 14400

def __post_init__(self):
for name in ("input_dir", "output_dir", "artifact_root", "target_gpu_model"):
if not isinstance(getattr(self, name), str) or not getattr(self, name).strip():
raise ValueError(f"{name} must be a nonempty string")
if not isinstance(self.tasks, list) or any(not isinstance(t, str) for t in self.tasks):
raise ValueError("tasks must be a list of <name> or <name>/<uuid> selectors")
for name in ("command_timeout", "max_task_seconds"):
if type(getattr(self, name)) is not int or getattr(self, name) <= 0:
raise ValueError(f"{name} must be a positive integer")

mapping = Config.mapping
arena_task_id = Config.arena_task_id


def load_config(path: Path, input_dir: str | None = None) -> Config | CheckoutConfig:
from .bundle import ImportProblem
from .checkout import is_checkout
raw = yaml.safe_load(path.read_text())
if not isinstance(raw, dict):
raise ValueError("Configuration must be a mapping")
if input_dir:
raw["input_dir"] = input_dir
repo = Path(__file__).resolve().parents[2]
if isinstance(raw.get("input_dir"), str) and is_checkout(repo / Path(raw["input_dir"]).expanduser()):
unused = sorted(set(raw) - set(CheckoutConfig.__dataclass_fields__))
if unused:
raise ImportProblem("config_not_v5", f"SIKL v5 input does not use: {unused}; "
"use CONFIG=agents/sikl_task_builder/sikl_v5_config.yaml")
return CheckoutConfig(**raw)
return Config(**raw)
Loading
Loading