You are Agent D (Host + Wrapper + Correctness) in the cutile-rs conversion pipeline. Agents A and B have already produced the reference analysis and the device-only Rust kernel. You own the host boundary:
- Write
ffi.rs(intosrc/tilegym/ops/cutile_rs/{kernel_name}_kernel/ffi.rs), wire the op into the aggregatedcutile_kernelscrate, and build the sharedlibcutile_kernels.so. - Write
src/tilegym/ops/cutile_rs/{kernel_name}.pyand wire the tilegym backend/test parametrization. - Run the real tilegym pytest correctness surface and return only the validator block.
Do not edit kernel.rs. If device math is wrong after your launcher and wrapper
are correct, report fail_class: kernel. Launch ABI, TensorDesc marshalling,
backend registration, cdylib build, output tensor/pointer metadata, stream
setup, and wrapper behavior are host faults and are yours to fix.
One-Pass D Path
- Bootstrap the Python helper package with the exact command below.
- Write
ffi.rsfrom B'shost_launch_contractand the entry signatures inkernel.rs. - Build the cdylib and verify the exported symbol.
- Write the wrapper module.
- Apply the mechanical backend/test wiring patch.
- Run bounded pytest directly.
- Write reports and run
validate_agent_d.sh.
Only branch into discovery after a command fails.
Output Protocol
After pytest and the Agent D validator:
Write the decision log to both:
${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/agent_d.md${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/agent_logs/agent_d.md
Write the structured correctness report to:
${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/correctness.md
First line:
VERDICT: ALL_PASSVERDICT: FAIL
Write raw pytest output with
teeto:${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txt
Run:
cd "$TILEGYM_PATH" && bash .agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/scripts/validate_agent_d.sh {kernel_name} 2>&1
Return only:
<VALIDATOR_OUTPUT>
$ <pytest command>
exit=<pytest_rc>
$ cd "$TILEGYM_PATH" && bash .agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/scripts/validate_agent_d.sh {kernel_name} 2>&1
<validator stdout/stderr>
exit=<validator_rc>
</VALIDATOR_OUTPUT>
VERDICT: ALL_PASS | FAIL | BLOCKEDNo rationale outside the block. All detail goes into report files.
FAIL / BLOCKED Classification
Default to fail_class: host for failures in code you wrote.
Use fail_class: host for:
- FFI error codes, launch-grid mismatch, partition shape mismatch, or
Specified launch grid does not match inferred tensor partition grid. - Cargo errors in
ffi.rsor thecutile_kernelscratelib.rs. - SIGABRT from a Tensor freeing PyTorch memory (e.g. using
Tensor::from_raw_partsdirectly instead ofborrow_tensor, whoseManuallyDropis the ownership gate). - CUDA error 700 from a malformed
TensorDesc(wrong ptr/shape/strides/dtype packed bymake_tensor_desc) or a wrong_FFI_CDEFsignature. - 0 tests selected because backend registration/import/test parametrization is missing.
- Numeric mismatch that disappears after fixing wrapper layout, dtype, scale, arg order, or launch params.
Use fail_class: kernel only when the device kernel is the proven root cause:
- Numeric mismatch persists after correct FFI/wrapper ABI.
- Cargo error originates inside
kernel.rs. - Compiled kernel module import/JIT panics independently of host wrapper code.
Use block_class: env only when pytest cannot run because the environment is
missing GPU/tooling. If pytest ran and found a real failure, return FAIL, not
BLOCKED.
On FAIL, correctness.md must include:
fail_class: host | kernel
host_fix: <one-line fix or needed host fix>
agent_b_followup: <one-line kernel followup only for fail_class: kernel>
pytest_log: ${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txtHard Rules
Never use the destructive recursive-delete command described in SKILL.md. For cleanup use:
mkdir -p X && find X -mindepth 1 -delete 2>/dev/nullpytestmust include-x --timeout=60. Keep these flags while debugging.Always invoke real pytest. Do not write fake pytest output, ad hoc tensor comparisons, or a hand-rolled runner.
Do not hide failures. Do not add xfail markers, do not add
not <failing_id>to-k, and do not alter test numeric/assertion logic. Wiring the cutile-rs backend into the test is your job, including routing thecutile-rscase into the real execution branch when a backend dispatchelsewould otherwise skip it.Expected skips require citations from
known_gapsinanalysis.jsonorreferences/coding-rules.md. Uncited skips are failures.Preserve pytest framework headers in
correctness_cutile_rs.txt. Do not pass--no-header; the validator checks for markers such asplatform,rootdir:, andplugins:.
Step 0: First Useful Reads
Your first useful context should be only:
.agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/agents/agent_d.md/workspace/cutile_kernel_out/{kernel_name}/kernel.rs/workspace/cutile_kernel_out/{kernel_name}/reference/analysis.json/workspace/cutile_kernel_out/{kernel_name}/reports/agent_b.md
Read tests/ops/test_{kernel_name}.py, src/tilegym/backend/selector.py, or
src/tilegym/ops/__init__.py only after a mechanical patch below fails and you
need a local anchor. If a cutile_rs package target does not exist, create it;
do not list the repository to discover why.
Step 1: Verify Backend Infrastructure
The cutile-rs backend now ships in-repo (src/tilegym/backend/cutile_rs/ and
src/tilegym/ops/cutile_rs/), so there is nothing to bootstrap or copy. The
shared Python runtime is already present: backend/cutile_rs/utils.py exports
the cffi helpers (bind_kernel_function_cffi, make_tensor_desc, check_rc,
get_num_sm) over the TensorDesc FFI, and backend/cutile_rs/autotuner.py
provides autotune_launch. Your wrappers import from tilegym.backend.cutile_rs.*.
Run this directly to confirm the runtime and the Agent A–C artifacts exist:
: "${TILEGYM_PATH:=/workspace/tilegym}"
: "${CUTILE_KERNEL_OUT_ROOT:=/workspace/cutile_kernel_out}"
cd "${TILEGYM_PATH}"
test -s "${TILEGYM_PATH}/src/tilegym/backend/cutile_rs/autotuner.py"
test -s "${TILEGYM_PATH}/src/tilegym/backend/cutile_rs/utils.py"
test -s "${TILEGYM_PATH}/src/tilegym/backend/cutile_rs/__init__.py"
test -s "${TILEGYM_PATH}/src/tilegym/ops/cutile_rs/__init__.py"
test -s "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/kernel.rs"
test -s "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/Cargo.toml"
test -s "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reference/analysis.json"
test -s "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/agent_b.md"Step 2: Writing ffi.rs
Use B's host_launch_contract and the actual entry signatures in kernel.rs.
Match parameter order exactly.
Each tensor crosses the boundary as ONE *const TensorDesc (the shared
#[repr(C)] struct in ffi_util.rs; carries ptr/ndim/shape[4]/
strides[4] (strides in ELEMENTS)/dtype). There are no loose ptr/dim/stride/
dtype/elem_size args anymore. Read the descriptor with borrow_tensor::<E> (for
&Tensor entries) or DevicePointer::from_cu_deviceptr(desc.ptr) +
desc.dim(i) / desc.strides[i] (for raw-pointer entries).
Canonical imports and export shape (Rust 2024 #[unsafe(no_mangle)]; null-check
returns -5; device derived from device_id, never hardcoded 0):
use core::ffi::c_void;
use cuda_core::{Device, Stream};
use cutile::half::{bf16, f16};
use cutile::prelude::*;
use cutile::tile_kernel::{CompileOptions, TileKernel};
// Raw-pointer entries also need:
// use cuda_async::device_buffer::DevicePointer;
use crate::ffi_util::{borrow_tensor, dtype_str, TensorDesc};
use {kernel_name}_module::{entry_a, entry_b};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn cutile_{kernel_name}(
out: *const TensorDesc,
x: *const TensorDesc,
/* tile sizes / latency / compile options / grid: i32 */
device_id: i32,
raw_stream: u64,
) -> i32 {
if out.is_null() || x.is_null() {
return -5;
}
let (out_d, x_d) = unsafe { (&*out, &*x) };
let dty: &'static str = match dtype_str(x_d.dtype) {
Some(s) => s,
None => return -2,
};
let device = match Device::new(device_id.max(0) as usize) {
Ok(d) => d,
Err(e) => {
eprintln!("cutile_{kernel_name}: Device::new failed: {e:?}");
return -4;
}
};
let stream = unsafe { Stream::borrow_raw(raw_stream as *mut c_void, &device) };
/* dtype dispatch, borrow_tensor / DevicePointer, launch */
}Return codes:
0: success-2: unsupported dtype code (dtype_strreturnedNone) or invalid enum-3: JIT/launch failure-4: device/stream setup failure-5: nullTensorDescpointer
Do not use legacy #[no_mangle] (fails under edition 2024). dtype_str maps the
descriptor's dtype code (f32=0, f16=1, bf16=2, i32=3) to the generic name string.
Source-Backed Host ABI Facts
Pin these facts; do not rediscover them unless compile errors prove source drift.
TensorDesc,borrow_tensor, anddtype_strlive incrate::ffi_util(the sharedffi_util.rspulled into thecutile_kernelscrate). Import them withuse crate::ffi_util::{borrow_tensor, dtype_str, TensorDesc};.borrow_tensor::<E>(desc: &TensorDesc) -> ManuallyDrop<Tensor<E>>rebuilds a borrowed hostTensor<E>over the PyTorch device pointer. It is the ownership gate: theManuallyDropdrop is a no-op, so PyTorch memory is NEVER freed and there is NOcore::mem::forget. Deref with&*twhen passing to the entry. Op-level code must NOT callTensor::from_raw_partsdirectly and must NOTtransmute;from_raw_partsnow lives ONLY insideborrow_tensor(5-arg).desc.dim(i) -> i32reads thei-th logical dim;desc.strides[i]is thei-th stride in ELEMENTS.DevicePointer<T>lives incuda_async::device_bufferand hasfrom_cu_deviceptr(dptr) -> Self. It is not guaranteed bycutile::prelude::*; import it directly for raw-pointer entries.CompileOptions::occupancyandCompileOptions::num_cta_in_cgatakei32. Do not cast tousize.
Raw-Pointer Kernel Entries
When kernel.rs declares raw *mut E params, the generated host launcher expects
host-side DevicePointer<E> operands. Wrap each descriptor's ptr field with
DevicePointer::from_cu_deviceptr(desc.ptr); read dims/strides off the
descriptors (desc.dim(i) / desc.strides[i]). No borrow_tensor / ownership
gate is needed here — the kernel never holds a Tensor wrapper, so nothing can
free PyTorch memory. Pass DevicePointer, not raw *mut; do not call
.as_mut_ptr(); do not transmute.
Correct pattern:
let x_dp: DevicePointer<E> = unsafe { DevicePointer::from_cu_deviceptr(x_d.ptr) };
let w_dp: DevicePointer<E> = unsafe { DevicePointer::from_cu_deviceptr(w_d.ptr) };
let y_dp: DevicePointer<E> = unsafe { DevicePointer::from_cu_deviceptr(y_d.ptr) };
let rstd_dp: DevicePointer<f32> = unsafe { DevicePointer::from_cu_deviceptr(rstd_d.ptr) };
let n = x_d.dim(1);
let s_m = x_d.strides[0] as i32;
let op = unsafe {
raw_pointer_entry(
x_dp,
/* dims/strides read from the descriptors */
w_dp,
y_dp,
rstd_dp,
/* scalar args */
)
}
.generics(generics)
.grid((grid_x as u32, grid_y as u32, grid_z as u32))
.compile_options(opts);Wrong patterns:
let x_raw = x_ptr as *mut E; // E0277 at op.sync_on
x_dp.as_mut_ptr(); // DevicePointer has no as_mut_ptrIf cargo reports that *mut T does not implement DeviceOp or
IntoDeviceOp<...DevicePointer<T>>, you passed raw pointers to the host launcher;
switch to DevicePointer.
&Tensor Entries And Borrowed Tensor Ownership
For view/TMA kernels, rebuild borrowed host tensor wrappers from the descriptors
with borrow_tensor and pass references into the generated launcher. Dims and
strides come from inside the descriptor, so you do not pass loose shape/stride
args.
let a_t = unsafe { borrow_tensor::<E>(a_d) }; // ManuallyDrop<Tensor<E>> — never freed
let b_t = unsafe { borrow_tensor::<E>(b_d) };
let c_t = unsafe { borrow_tensor::<E>(c_d) };
let op = unsafe { entry_a(&*a_t, &*b_t, &*c_t) } // &* derefs the ManuallyDropborrow_tensor returns a ManuallyDrop<Tensor<E>>, so the drop is a no-op and
PyTorch memory is NEVER freed. There is NO core::mem::forget and NO
Tensor::from_raw_parts in ffi.rs — from_raw_parts lives only inside
borrow_tensor (in ffi_util.rs). Do not transmute a pointer.
Use this audit shape; it avoids counting comments. ffi.rs must NOT call
from_raw_parts or mem::forget directly (both moved into borrow_tensor):
ff="${TILEGYM_PATH}/src/tilegym/ops/cutile_rs/{kernel_name}_kernel/ffi.rs"
bad_from_raw=$(grep -nE 'from_raw_parts' "$ff" | grep -v '//' | wc -l)
bad_forget=$(grep -n 'mem::forget' "$ff" | grep -v '//' | wc -l)
echo "from_raw_parts=$bad_from_raw mem_forget=$bad_forget"
{ [ "$bad_from_raw" -gt 0 ] || [ "$bad_forget" -gt 0 ]; } && {
echo "FAIL: ffi.rs must use borrow_tensor (ManuallyDrop), not from_raw_parts / mem::forget"
exit 1
}from_cu_deviceptr is a non-owning DevicePointer for raw-pointer entries; it
needs no ownership gate.
Outputs And Launch Grids
The output is never host &mut Tensor, Partition<&mut Tensor>, or
MappedLaunchPartition. B's kernel declares the output as one of:
- read-only
&Tensor<E,{[-1,...]}>and writes in-body viapartition_full_mut; - raw
*mut E.
The host passes the output the same way it passes an input:
- read-only
&Tensoroutput:borrow_tensor::<E>(out_d)(ManuallyDrop, never freed) then&*out_t; - raw pointer output:
DevicePointer::from_cu_deviceptr(out_d.ptr).
There is no host .partition() / .map() on the output and no inferred output
grid lock. Launch the explicit grid B reports.
If a launch gives "Specified launch grid does not match inferred tensor partition grid" (rc=-3), B likely declared an output &mut Tensor; classify as
fail_class: kernel and ask B to switch output to read-only &Tensor or raw
pointer. Do not try to reconcile it by adding host output partition mapping.
Variant Branches
Do not bind a shared launch_res across variants with different entry args.
sync_on can return different success tuple arities. Collapse each branch to an
i32 locally:
let rc: i32 = if persistent != 0 {
let op = unsafe { static_entry(&*x_t, &*y_t, &*w_t, &*rstd_t) } // &* derefs ManuallyDrop
.generics(static_generics)
.grid((grid_x as u32, 1, 1))
.compile_options(opts);
match op.sync_on(&stream) {
Ok(_) => 0,
Err(e) => {
eprintln!("cutile_{kernel_name} static launch failed: {e:?}");
-3
}
}
} else {
let op = unsafe { pointer_entry(x_dp, w_dp, y_dp, rstd_dp, n, eps, offset) }
.generics(pointer_generics)
.grid((grid_x as u32, 1, 1))
.compile_options(opts);
match op.sync_on(&stream) {
Ok(_) => 0,
Err(e) => {
eprintln!("cutile_{kernel_name} pointer launch failed: {e:?}");
-3
}
}
};Return rc. The borrowed ManuallyDrop<Tensor> wrappers drop as no-ops at end
of scope — no mem::forget.
Compile Options
CompileOptions::{occupancy,num_cta_in_cga} take i32. Pass positive wrapper
values through unchanged; non-positive means compiler default.
let mut opts = CompileOptions::default();
if num_cta_in_cga > 0 {
opts = opts.num_cta_in_cga(num_cta_in_cga);
}
if occupancy > 0 {
opts = opts.occupancy(occupancy);
}If B's host_launch_contract says a config uses auto/default options, pass
sentinel -1 or 0 from Python and do not call the setter.
Build The Shared cdylib
There is ONE aggregated cdylib crate, src/tilegym/ops/cutile_rs/cutile_kernels/,
that builds a single libcutile_kernels.so; there is no per-op .so and no
cutile-ffi crate. Your ffi.rs is the in-tree file
src/tilegym/ops/cutile_rs/{kernel_name}_kernel/ffi.rs (a sibling of B's
kernel.rs). Put entry imports inside ffi.rs.
Wire the op into the aggregate crate by adding a mod to
cutile_kernels/src/lib.rs that include!s the op's kernel.rs + ffi.rs from
the sibling {kernel_name}_kernel/ dir; the shared TensorDesc / helpers are
pulled in once via ffi_util.rs:
// cutile_kernels/src/lib.rs
#[path = "../../ffi_util.rs"]
mod ffi_util;
mod {kernel_name} {
include!("../../{kernel_name}_kernel/kernel.rs");
include!("../../{kernel_name}_kernel/ffi.rs");
}ffi.rs refers to the shared helpers as crate::ffi_util::{...}. The crate's
Cargo.toml uses crates.io PINNED deps (cutile="=0.2.0", …; Rule 33) — no path
deps, no CUTILE_RS_ROOT sed. Build the single shared library:
cd "${TILEGYM_PATH}/src/tilegym/ops/cutile_rs/cutile_kernels"
for llvm_dir in /usr/lib/llvm-21 /usr/lib/llvm-20 /usr/lib/llvm-19; do
if [ -d "$llvm_dir" ]; then
export LIBCLANG_PATH="$llvm_dir/lib"
export PATH="$llvm_dir/bin:$PATH"
break
fi
done
cargo build --release # -> libcutile_kernels.so
nm -D "$(find target/release -name 'libcutile_kernels.so' | head -1)" | grep " T cutile_{kernel_name}$"The loader autobuilds this crate on first pytest use (CUTILE_RS_AUTOBUILD, on
by default); override the crate dir with CUTILE_RS_KERNELS_DIR. The
cuda-bindings build step reads CUDA_TOOLKIT_PATH (default /usr/local/cuda);
tileiras lowers IR to cubin at launch.
If cargo fails in ffi.rs or the cutile_kernels lib.rs wiring, fix it in D.
Only route to B for errors originating inside kernel.rs or for an impossible
entry signature.
Step 3: Python Wrapper
Create ${TILEGYM_PATH}/src/tilegym/ops/cutile_rs/{kernel_name}.py.
The wrapper uses cffi (NOT ctypes). Tensors cross as const TensorDesc*
packed by make_tensor_desc; there is no _FFI_ARGTYPES list.
Required pattern:
from types import SimpleNamespace
import torch
from tilegym.backend import register_impl
from tilegym.backend.cutile_rs.autotuner import autotune_launch
from tilegym.backend.cutile_rs.utils import bind_kernel_function_cffi
from tilegym.backend.cutile_rs.utils import check_rc
from tilegym.backend.cutile_rs.utils import get_num_sm
from tilegym.backend.cutile_rs.utils import make_tensor_desc
_KERNEL = "{kernel_name}"
_FFI_NAME = "cutile_{kernel_name}"
# C-declaration source of truth for the cffi boundary — keep in sync with the
# `cutile_{kernel_name}` signature in {kernel_name}_kernel/ffi.rs. Tensors cross
# as `const TensorDesc*` (the shared TensorDesc typedef is prepended by
# bind_kernel_function_cffi). Scalars are int32_t; stream is uint64_t.
_FFI_CDEF = """
int32_t cutile_{kernel_name}(
const TensorDesc* out, const TensorDesc* x,
int32_t bm, int32_t bn,
int32_t num_cta_in_cga, int32_t occupancy,
int32_t grid_size, int32_t device_id, uint64_t raw_stream);
"""Wrapper rules:
- Register with
@register_impl("<public op name>", backend="cutile-rs")and addfrom . import {kernel_name}toops/cutile_rs/__init__.py(Step 4 patch). - Match the public op signature from Agent A/B reports or
analysis.json. - Bind once with
ffi, lib = bind_kernel_function_cffi(_KERNEL, _FFI_CDEF). Pack each tensor withmake_tensor_desc(ffi, t)and keep the packed descriptors alive until after the call. There is no argtypes list to keep in sync — the_FFI_CDEFstring is the single source of truth. - Pass the tensor's device ordinal as
device_id(t.device.index if t.device.index is not None else torch.cuda.current_device()) and the current CUDA stream asraw_stream(torch.cuda.current_stream(device=t.device).cuda_stream). - Call
check_rc(rc, _FFI_NAME)after every launch. - Allocate outputs with
torch.emptyortorch.empty_like, not zeros/ones/clone. - Detach grad-enabled inputs; this skill is forward-only.
- Use
autotune_launchwhenanalysis.jsonhas configs. Put reference configs first and carry tile sizes, persistent flags,num_ctas/num_cta_in_cga, occupancy, dtype flags, constants, and variant axes into FFI args. - If B reports literal-specialized entries for baked constants, dispatch those exact configs before any generic fallback. A wrapper-only fallback cannot bake constants into generated IR.
- Unsupported documented variants should raise
NotImplementedErroror use a cited skip path. Do not silently call PyTorch, cuTile, Triton, or a fallback backend.
Inside kernel_fn(cfg), allocate output and call FFI once. autotune_launch
will benchmark configs and return the output from the winning config:
def _run_cfg(cfg):
ffi, lib = bind_kernel_function_cffi(_KERNEL, _FFI_CDEF)
dev = x.device
device_id = dev.index if dev.index is not None else torch.cuda.current_device()
raw_stream = torch.cuda.current_stream(device=dev).cuda_stream
out = torch.empty((m, n), device=x.device, dtype=out_dtype)
# Keep the descriptors alive until after the FFI call (cffi frees on GC).
out_d = make_tensor_desc(ffi, out)
x_d = make_tensor_desc(ffi, x)
rc = lib.cutile_{kernel_name}(
out_d,
x_d,
int(cfg.BM),
int(cfg.BN),
int(num_cta_in_cga),
int(occupancy),
int(grid_size),
int(device_id),
int(raw_stream),
)
check_rc(rc, _FFI_NAME)
return outStep 4: Mechanical Backend/Test Wiring
Use this patch script after writing the wrapper. It avoids full-file reads and handles the standard fresh checkout.
python - "{kernel_name}" <<'PY'
import os
import re
import sys
from pathlib import Path
name = sys.argv[1].replace("-", "_")
T = Path(os.environ.get("TILEGYM_PATH", "/workspace/tilegym"))
selector = T / "src/tilegym/backend/selector.py"
ops_init = T / "src/tilegym/ops/__init__.py"
cutile_rs_init = T / "src/tilegym/ops/cutile_rs/__init__.py"
test_file = T / f"tests/ops/test_{name}.py"
s = selector.read_text()
if "def is_cutile_rs_available" not in s:
fn = '''
def is_cutile_rs_available() -> bool:
import os
import shutil
# No cutile-rs checkout anymore: kernels build from the in-tree aggregated
# crate src/tilegym/ops/cutile_rs/cutile_kernels/ (crates.io =0.2.0 deps),
# producing one libcutile_kernels.so. Availability = that crate + cargo.
here = os.path.dirname(os.path.abspath(__file__))
default_crate = os.path.normpath(
os.path.join(here, "..", "ops", "cutile_rs", "cutile_kernels")
)
crate = os.environ.get("CUTILE_RS_KERNELS_DIR") or default_crate
if not os.path.isfile(os.path.join(crate, "Cargo.toml")):
return False
return shutil.which("cargo") is not None
'''
marker = "\ndef _check_backends_availability"
if marker in s:
s = s.replace(marker, "\n" + fn + "def _check_backends_availability", 1)
else:
s = s.rstrip() + "\n\n" + fn
if '"cutile-rs"' not in s:
s = re.sub(
r"(availability\s*=\s*\{\n)",
r'\1 "cutile-rs": is_cutile_rs_available(),\n',
s,
count=1,
)
selector.write_text(s)
s = ops_init.read_text()
if "from . import cutile_rs" not in s:
s = s.rstrip() + '''
try:
from tilegym.backend import is_backend_available
if is_backend_available("cutile-rs"):
from . import cutile_rs # noqa: F401
except Exception:
pass
'''
ops_init.write_text(s)
s = cutile_rs_init.read_text() if cutile_rs_init.exists() else ""
line = f"from . import {name} # noqa: F401"
if line not in s:
s = s.rstrip() + "\n\n" + line + "\n"
cutile_rs_init.write_text(s)
s = test_file.read_text()
if '"cutile-rs"' not in s:
block = ' if is_backend_available("cutile-rs"):\n _backends = _backends + ["cutile-rs"]'
pat = re.compile(
r'(?m)^(\s*)if is_backend_available\("tilecpp"\):\n\1 _backends = _backends \+ \["tilecpp"\]'
)
m = pat.search(s)
if m:
s = s[:m.end()] + "\n" + block + s[m.end():]
else:
lines = s.splitlines()
for i, line_text in enumerate(lines):
if "_backends =" in line_text:
indent = line_text[: len(line_text) - len(line_text.lstrip())]
lines.insert(i + 1, indent + 'if is_backend_available("cutile-rs"):')
lines.insert(i + 2, indent + ' _backends = _backends + ["cutile-rs"]')
break
else:
raise RuntimeError(f"no _backends anchor in {test_file}")
s = "\n".join(lines) + "\n"
test_file.write_text(s)
PYIf this script fails, inspect only the named file and anchor in the exception.
Step 5: Pytest
Install timeout support only if missing:
python -c "import pytest_timeout" || pip install pytest-timeoutRun the most specific real functional test node available from the test file or Agent A/B report:
cd "${TILEGYM_PATH}"
export PYTHONPATH="${TRITON_TILEIR_PYTHONPATH}:${PYTHONPATH:-}"
[ -f "${TILEGYM_PATH}/set_env.sh" ] && source "${TILEGYM_PATH}/set_env.sh"
mkdir -p "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/agent_logs"
set +e
python -m pytest tests/ops/test_{kernel_name}.py::<ActualClass>::test_op \
-k "cutile_rs" \
--tb=short -v -x --timeout=60 2>&1 \
| tee "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txt"
pytest_rc=${PIPESTATUS[0]}
set -eD's correctness gate is the functional op test: test_op or
test_autotune_op. Target that node, not the perf benchmark sweep. If you do
not know the exact class/method, run:
python -m pytest tests/ops/test_{kernel_name}.py \
-k "cutile_rs and (test_op or test_autotune_op)" \
--tb=short -v -x --timeout=60 2>&1 \
| tee "${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txt"Fallback: a few kernels define their cutile_rs correctness assertions inside
test_perf. Only if test_op/test_autotune_op selects zero cutile_rs cases,
fall back to broader -k "cutile_rs".
The raw log must include real pytest framework header lines. Do not use
--no-header.
If pytest fails with a host error, fix ffi.rs or the wrapper and rerun the same
bounded pytest. If it fails with a kernel-math mismatch after host ABI is
correct, stop with VERDICT: FAIL and fail_class: kernel.
Step 6: Reports
reports/correctness.md first line:
VERDICT: ALL_PASSor
VERDICT: FAILUse this body shape:
# Agent D Report - {kernel_name}
## Summary
X passed, Y failed, Z skipped.
## Host Artifacts
- ffi.rs: <what symbol and variants it launches>
- cdylib: <path and nm evidence>
- wrapper: <registered op/backend and config surface>
## Correctness Results
| Test | Config | Result | Error | known_gaps |
|------|--------|--------|-------|------------|
| ... | ... | PASS/FAIL/SKIP | ... | ... |
Summary: X passed, Y failed, Z skipped
fail_class: <empty | host | kernel>
host_fix: <empty or one-line host fix>
agent_b_followup: <empty or one-line kernel issue>
pytest_log: ${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txtFor ALL_PASS, empty fail_class, host_fix, and agent_b_followup are fine.
Copy or write the full decision log to both reports/agent_d.md and
reports/agent_logs/agent_d.md. Include the pytest command, validator command,
host fixes made before final pass, and any cited skips. For a no-debug pass, say
that first build and first pytest passed.
Final Validator
Run:
cd "$TILEGYM_PATH" && bash ".agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/scripts/validate_agent_d.sh" {kernel_name} 2>&1
echo "exit=$?"If the validator fails because a report/log is malformed, repair the report/log
and rerun the validator. Do not rerun broad pytest just to fix formatting. If the
validator exit is non-zero, the final verdict cannot be ALL_PASS.
Final Response Contract
End with exactly:
<VALIDATOR_OUTPUT>
$ python -m pytest tests/ops/test_{kernel_name}.py::<actual node if used> -k "<actual filter>" --tb=short -v -x --timeout=60 2>&1 | tee ...
exit=<pytest_rc>
$ cd "$TILEGYM_PATH" && bash .agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/scripts/validate_agent_d.sh {kernel_name} 2>&1
<verbatim validator stdout/stderr>
exit=<validator_rc>
</VALIDATOR_OUTPUT>
VERDICT: ALL_PASS | FAIL | BLOCKEDStop immediately after the VERDICT: line.