All skills
nvidia avatar

/tilegym-converting-cutile-triton-to-cutile-rs

@48a26cf
by NVIDIA Corporationnvidia/tilegym821 stars
89

Use this skill to convert, port, or translate Triton-TileIR or cuTile-Python GPU kernels to cutile-rs (Rust). The orchestrator runs scripts/preflight.sh, then drives a bounded Agent A -> B -> D -> E pipeline (Agent C is diagnostic, Agent F optional), delegating all kernel/host/correctness/perf work to sub-agents and routing by each stage's single-line VERDICT.

  • 44 files
  • 452.9 KB
  • CC-BY-4
  • Updated 2 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/nvidia/tilegym/tilegym-converting-cutile-triton-to-cutile-rs

This session only. Nothing lands on disk.

agentsagent_d.md

≈7.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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:

  1. Write ffi.rs (into src/tilegym/ops/cutile_rs/{kernel_name}_kernel/ffi.rs), wire the op into the aggregated cutile_kernels crate, and build the shared libcutile_kernels.so.
  2. Write src/tilegym/ops/cutile_rs/{kernel_name}.py and wire the tilegym backend/test parametrization.
  3. 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

  1. Bootstrap the Python helper package with the exact command below.
  2. Write ffi.rs from B's host_launch_contract and the entry signatures in kernel.rs.
  3. Build the cdylib and verify the exported symbol.
  4. Write the wrapper module.
  5. Apply the mechanical backend/test wiring patch.
  6. Run bounded pytest directly.
  7. 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:

  1. 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
  2. Write the structured correctness report to:

    • ${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/reports/correctness.md

    First line:

    • VERDICT: ALL_PASS
    • VERDICT: FAIL
  3. Write raw pytest output with tee to:

    • ${CUTILE_KERNEL_OUT_ROOT}/{kernel_name}/correctness_cutile_rs.txt
  4. Run:

    • cd "$TILEGYM_PATH" && bash .agents/skills/tilegym-converting-cutile-triton-to-cutile-rs/scripts/validate_agent_d.sh {kernel_name} 2>&1
  5. 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 | BLOCKED

No 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.rs or the cutile_kernels crate lib.rs.
  • SIGABRT from a Tensor freeing PyTorch memory (e.g. using Tensor::from_raw_parts directly instead of borrow_tensor, whose ManuallyDrop is the ownership gate).
  • CUDA error 700 from a malformed TensorDesc (wrong ptr/shape/strides/dtype packed by make_tensor_desc) or a wrong _FFI_CDEF signature.
  • 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.txt

Hard Rules

  1. Never use the destructive recursive-delete command described in SKILL.md. For cleanup use:

    mkdir -p X && find X -mindepth 1 -delete 2>/dev/null
  2. pytest must include -x --timeout=60. Keep these flags while debugging.

  3. Always invoke real pytest. Do not write fake pytest output, ad hoc tensor comparisons, or a hand-rolled runner.

  4. 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 the cutile-rs case into the real execution branch when a backend dispatch else would otherwise skip it.

  5. Expected skips require citations from known_gaps in analysis.json or references/coding-rules.md. Uncited skips are failures.

  6. Preserve pytest framework headers in correctness_cutile_rs.txt. Do not pass --no-header; the validator checks for markers such as platform, rootdir:, and plugins:.

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_str returned None) or invalid enum
  • -3: JIT/launch failure
  • -4: device/stream setup failure
  • -5: null TensorDesc pointer

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, and dtype_str live in crate::ffi_util (the shared ffi_util.rs pulled into the cutile_kernels crate). Import them with use crate::ffi_util::{borrow_tensor, dtype_str, TensorDesc};.
  • borrow_tensor::<E>(desc: &TensorDesc) -> ManuallyDrop<Tensor<E>> rebuilds a borrowed host Tensor<E> over the PyTorch device pointer. It is the ownership gate: the ManuallyDrop drop is a no-op, so PyTorch memory is NEVER freed and there is NO core::mem::forget. Deref with &*t when passing to the entry. Op-level code must NOT call Tensor::from_raw_parts directly and must NOT transmute; from_raw_parts now lives ONLY inside borrow_tensor (5-arg).
  • desc.dim(i) -> i32 reads the i-th logical dim; desc.strides[i] is the i-th stride in ELEMENTS.
  • DevicePointer<T> lives in cuda_async::device_buffer and has from_cu_deviceptr(dptr) -> Self. It is not guaranteed by cutile::prelude::*; import it directly for raw-pointer entries.
  • CompileOptions::occupancy and CompileOptions::num_cta_in_cga take i32. Do not cast to usize.

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_ptr

If 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 ManuallyDrop

borrow_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):

<!-- MIGRATION-NOTE (resolved): DEFAULT — author ffi.rs at the in-tree path $TILEGYM_PATH/src/tilegym/ops/cutile_rs/{kernel_name}_kernel/ffi.rs (a sibling of B's kernel.rs, include!()d by the cutile_kernels crate), per output-structure.md. The per-kernel working dir $CUTILE_KERNEL_OUT_ROOT/{kernel_name}/ holds A–C artifacts (reports/, reference/, generated/), NOT the authored ffi.rs. The audit below uses the in-tree path; only repoint `ff` if a non-standard harness keeps ffi.rs solely under $CUTILE_KERNEL_OUT_ROOT/{kernel_name}/. -->
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 via partition_full_mut;
  • raw *mut E.

The host passes the output the same way it passes an input:

  • read-only &Tensor output: 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 add from . import {kernel_name} to ops/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 with make_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_CDEF string 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 as raw_stream (torch.cuda.current_stream(device=t.device).cuda_stream).
  • Call check_rc(rc, _FFI_NAME) after every launch.
  • Allocate outputs with torch.empty or torch.empty_like, not zeros/ones/clone.
  • Detach grad-enabled inputs; this skill is forward-only.
  • Use autotune_launch when analysis.json has 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 NotImplementedError or 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 out

Step 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)
PY

If 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-timeout

Run 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 -e

D'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_PASS

or

VERDICT: FAIL

Use 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.txt

For 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 | BLOCKED

Stop immediately after the VERDICT: line.

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 48a26cf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 4 days ago.

Activeupdated 2 months ago
metadata
{
  "author": "TileGym Team <TileGym@nvidia.com>"
}

README badge

README badge for nvidia/tilegym/tilegym-converting-cutile-triton-to-cutile-rs