AMDMIGraphX
AGENTS.md337 starsChanged 2 months agoMITView on GitHub
This file provides guidance for agents and contributors working with code in this repository. MIGraphX is AMD's graph inference engine that accelerates machine learning model inference on ROCm. It uses a three-level IR hierarchy: - Programs (migraphx::program): Top-level containers for entire neural networks - Modules (migraphx::module): DAGs of instructions representing computational subgraphs - Instructions (migraphx::instruction): Atomic operations wrapping an operation with inputs The compilation flow transforms high-level models (ONNX, TensorFlow) through optimization passes into target-specific code (GPU kernels via HIP/MIOpen/rocBLAS,…
We split this AGENTS.md into the rules it gives an agent
69 rules. About 0 are common, 28 are distinctive, and 41 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈28
Distinctive · estimate
This author's own choices. Worth a look.
41
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 107
Prefer clear names over abbreviations
- Very specific: line 108
Use using type_alias = int not typedef int type_alias
- Very specific: line 109
Use nullptr not NULL or 0
- Very specific: line 110
Use or and and instead of && and ||
- Very specific: line 125
Running the analyzers: use make -C build analyze (runs both cppcheck and clang-tidy with the project's flags and suppressions) rather than …
- Very specific: line 145
Never redefine a parent .clang-tidy key in a child config: clang-tidy merges per key, so the child silently drops the parent's value.
- Very specific: line 157
When cppcheck's preprocessor cannot parse code (recursive macros), guard it with #ifndef CPPCHECK — never // cppcheck-suppress syntaxError, …
- Very specific: line 168
Avoid raw loops: - Prefer algorithms from <migraphx/algorithm.hpp> and STL <algorithm>:
- Very specific: line 192
If no suitable algorithm exists: Add a new algorithm to migraphx/algorithm.hpp rather than using raw loops
- Very specific: line 194
Memory management: - Use std::make_unique/shared, avoid raw new/delete
- Very specific: line 195
Non-memory resources: - Use MIGRAPHX_MANAGE_PTR for C-style acquire/release APIs:
- Distinctive: line 202
No need to inherit from base class - just implement required interface methods
- Distinctive: line 207
Keep passes idempotent (running twice = running once)
- Distinctive: line 208
Keep passes deterministic (same input → same output)
- Very specific: line 209
Always handle dynamic shapes in compute_shape (unknown dimensions)
- Distinctive: line 211
Generic programming: - Write reusable, type-independent code using templates and STL algorithms
- Distinctive: line 213
Avoid Casts: - Don't use a cast unless absolutely necessary.
- Distinctive: line 215
Encapsulate Bit Manipulation: - Put bit-twiddling and low-level operations behind well-named utility functions:
- Very specific: line 216
Prefer std::bitset for bit manipulation
- Very specific: line 218
Use std::tie for Lexicographical Comparisons: - Use std::tie or std::lexicographical_compare instead of manually writing lexicographical …
- Very specific: line 234
Narrow predicates do not go in core headers: - Do not add a GPU- or conv-specific helper such as is_nhwc(const shape&) to a widely included …
- Very specific: line 236
JIT kernel source templates stay explicit: - Keep one raw-string template per kernel rather than a shared ${...} shell.
- Distinctive: line 242
Prefer correct, complete implementations over minimal ones.
- Distinctive: line 243
Use appropriate data structures and algorithms — don't brute-force what has a known better solution.
- Distinctive: line 244
When fixing a bug, fix the root cause, not the symptom.
- Distinctive: line 245
If something I asked for requires error handling or validation to work reliably, include it without asking.
- Very specific: line 258
Add instructions via module::add_instruction(op, inputs...)
- Very specific: line 259
Add parameters via module::add_parameter(name, shape)
- Very specific: line 267
Must implement: name(), compute_shape(inputs)
- Very specific: line 277
Run optimization passes (pass_manager.cpp)
- Distinctive: line 309
Add parameters to modules for runtime inputs:
- Very specific: line 350
Place headers in src/include/migraphx/op/ and implementation in src/ if not header-only.
- Very specific: line 371
Create a matcher struct with matcher() and apply() methods:
- Very specific: line 419
Add tests mirroring test/eliminate_concat_test.cpp
- Very specific: line 545
No helpers for instruction creation, shape construction, parameter setup, or expected-module construction - write …
- Distinctive: line 580
Tests over several types: write the test as a function template and register each instantiation, so a failure names the type and each one …
- Very specific: line 591
Do not write an expect_* helper template called from one plain TEST_CASE.
- Distinctive: line 598
When adding a pass that rewrites structure:
- Distinctive: line 600
Run only the new pass (plus prerequisites and dead_code_elimination for cleanup)
- Distinctive: line 601
Create a new module with the expected final structure and assert that it is the same
- Distinctive: line 607
When the pass must leave a module unchanged, build it once, copy it, and compare instead of constructing it twice:
- Very specific: line 615
Use driver for inspection: migraphx-driver compile --text --apply-pass your_pass (-p is the short form.
- Very specific: line 621
Run only the tests that exercise the code you changed.
- Distinctive: line 626
Do not modify a failing test to match the new output.
- Very specific: line 631
Use verify_rms_range for tolerance-based comparisons with floating-point noise
- Distinctive: line 633
Avoid flaky thresholds by using stable, reproducible test data
- Distinctive: line 644
Use single-digit iteration counts
- Distinctive: line 656
When fixing subtle bugs (e.g., stride miscalculation), add test reproducing original failure
- Distinctive: line 657
Keep golden ONNX/TF assets minimal (large models slow CI and obscure failures)
- Very specific: line 704
Check if earlier passes canonicalized shapes (simplify_reshapes)
- Distinctive: line 705
Verify broadcasts are explicit/implicit as expected
- Distinctive: line 706
Check if dynamic dims prevent grouping
- Distinctive: line 707
Ensure op type lists support new data types (fp8/bf16)
- Very specific: line 708
Use MIGRAPHX_TRACE_COMPILE=1 to confirm fusion pass execution
- Distinctive: line 769
No inheritance required - just implement the interface:
- Very specific: line 789
Add parse function in src/onnx/parse_<op>.cpp
- Very specific: line 793
Add test in test/onnx/parse/ (parsing only) and test/onnx/verify/ (execution)
- Distinctive: line 815
When adding C++ APIs, mirror in Python bindings following existing patterns.
- Very specific: line 871
Register custom passes: Add to src/driver/passes.cpp for driver access.
- Very specific: line 891
After adding an IR-rewrite path, grep the helpers it calls for assert( and check each one holds (e.g.
- Very specific: line 933
Missing GPU_TARGETS: Always specify via $(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*')
- Very specific: line 934
Dynamic shape errors: Ensure compute_shape() handles unknown dimensions
- Distinctive: line 937
Build failures: Check clang-tidy warnings - they're enforced in CI
- Very specific: line 969
Commits: Use imperative, concise subject lines (optionally add a scope), e.g., fix: correct hip stream handling.
- Very specific: line 974
Do not use $(nproc), run nproc first, then use the numeric value directly.
- Very specific: line 975
When running make -j$(nproc), first resolve $(nproc) to a literal number, e.g.
- Very specific: line 979
When creating *any* temporary/scratch/generated file, write it under tmp/ at the workspace root.
- Very specific: line 980
Use tmp/<task-slug>/<YYYYMMDD-HHMMSS>/ for groups of files.
- Very specific: line 981
Never place temp files outside tmp/ or inside source directories.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Repository Guidelines
This file provides guidance for agents and contributors working with code in this repository.
Project Overview
MIGraphX is AMD's graph inference engine that accelerates machine learning model inference on ROCm. It uses a three-level IR hierarchy:
- Programs (
migraphx::program): Top-level containers for entire neural networks - Modules (
migraphx::module): DAGs of instructions representing computational subgraphs - Instructions (
migraphx::instruction): Atomic operations wrapping anoperationwith inputs
The compilation flow transforms high-level models (ONNX, TensorFlow) through optimization passes into target-specific code (GPU kernels via HIP/MIOpen/rocBLAS, CPU ops, or reference implementations).
Dependencies
ROCm ecosystem: ROCm, MIOpen, rocBLAS, hipBLASLt, HIP (paths under /opt/rocm)
Core libraries: Protobuf (ONNX), Half (IEEE 754 fp16), pybind11 (Python), nlohmann/json, MessagePack, SQLite3
Build tools: CMake 3.15+, rbuild (optional but recommended), ccache (via rbuild develop)
Language level: C++17 (set in CMake)
Project Structure & Module Organization
src/include/migraphx/*.hpp- Public API headerssrc/op/- Operation implementationssrc/targets/gpu/- GPU backend (HIP, MIOpen, rocBLAS lowering)src/targets/cpu/- CPU backendsrc/targets/ref/- Reference implementationsrc/driver/- CLI tools (main.cpp, perf.cpp, verify.cpp, passes.cpp)src/py/- Python bindings (pybind11)test/- Unit and integration tests (mirrorssrc/structure)test/gpu/- GPU-specific tests (build astest_gpu_<topic>)test/verify/- End-to-end verification teststest/onnx/- ONNX model parsing/verificationtools/andcmake/- Build helpers, scripts, format scripts, CMake modulesexamples/- Usage examples (C++ API, Python, diffusion, transformers, vision)docs/- Sphinx/Doxygen documentation; build artifacts indocs/_build/build/- Default generated build output
Build Commands
Development build (recommended):
rbuild develop -DGPU_TARGETS=$(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*')Manual CMake build:
cmake -S . -B build -DGPU_TARGETS=$(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*')
make -C build -j$(nproc)Build with dependencies (rbuild):
rbuild build -d depend -B build -DGPU_TARGETS=$(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*')Run all tests:
make -C build checkInstall:
make -C build installBuild documentation:
make -C build doc
# Or manually in docs/:
pip3 install -r sphinx/requirements.txt
python3 -m sphinx -T -E -b html -d _build/doctrees -D language=en . _build/htmlFormat code:
python3 tools/format.py -i origin/developtools/format.py runs git-clang-format against the given base, so it formats only
the lines you changed (CI does the same). Do not run clang-format -i on a whole
file: lines committed under an older clang-format get reformatted and show up as
unrelated hunks in your diff. The script reads the git diff, so stage your work
first or it reports "No modified cpp files to format".
Incremental builds: trust make. It tracks kernel-header → embed → relink
(cmake/Embed.cmake), so a kernel-header edit followed by a ~5s make -C build driver
is a real rebuild, not a no-op. Never delete build artifacts to force a rebuild; to
confirm a header edit landed, grep the string in build/lib/libmigraphx_gpu.so.*.
Before running ctest: rebuild the test executables with make -C build -k tests.
Building only the library leaves the other test binaries stale against the fresh
libmigraphx.so, and they then fail in ways that look like real regressions.
Install git hooks:
./.githooks/installCoding Standards
Naming Conventions
- Files and symbols:
snake_case(e.g.,compute_shape,eliminate_concat.cpp) - Template parameters:
CamelCase - Macros:
UPPER_CASEwithMIGRAPHX_prefix - Public headers go under
src/include/migraphx/*.hpp - Prefer clear names over abbreviations
- Use
using type_alias = intnottypedef int type_alias - Use
nullptrnotNULLor0 - Use
orandandinstead of&&and||
Formatting (.clang-format)
- 4-space indent, no tabs
- 100 column limit
- Braces on new line for classes/functions/control structures
- Align consecutive assignments
Linting
.clang-tidyruns via CMake; treat warnings seriously (they are enforced in CI)cppcheckis enabled with custom rules (tools/cppcheck/migraphx.py, e.g.migraphx-RedundantCast,migraphx-RedundantStaticCastOp,migraphx-AvoidNestedValue)
Running the analyzers: use make -C build analyze (runs both cppcheck and
clang-tidy with the project's flags and suppressions) rather than hand-assembling a
cppcheck or clang-tidy command line. It is a whole-repo sweep that takes many minutes:
run it detached, never kill it mid-run (an interrupted run corrupts the
build/cppcheck-build cache and produces phantom Failed to execute addon errors), and
do not edit files while it runs. Judge the result by the exit code — analyze exits
non-zero on any finding. Greps under-report: cppcheck reports as style:, and
clang-tidy emits ANSI colour escapes between the location and the severity, so strip
them first (sed -e 's/\x1b\[[0-9;]*m//g'). For quick clang-tidy iteration on one file
use the per-file make tidy-target-... targets; there is no per-file cppcheck target.
Fixing lint findings:
readability-identifier-naming: rename to the correctlower_casename — neverNOLINT. Resolve a clash with a loop index by renaming the bound (out_c, notC), and rename code only, not theK/C/Nnotation instatic_assertstrings or math/hardware comments.performance-unnecessary-value-paramon an opaque cheap-to-copy type (e.g.__amdgpu_buffer_rsrc_t): append the type toperformance-unnecessary-value-param.AllowedTypesin the root.clang-tidyinstead of aNOLINT.- Never redefine a parent
.clang-tidykey in a child config: clang-tidy merges per key, so the child silently drops the parent's value. Child configs only add genuinely new options; everything else is fixed at the source withNOLINT(check-name)/NOLINTNEXTLINE/NOLINTBEGIN…NOLINTENDor a refactor. readability-function-sizeandcppcoreguidelines-pro-type-const-caston genuinely irreducible kernel code (fixed-arity WMMA unrolls, buffer-intrinsicconst_cast) may beNOLINTed.- For idioms cppcheck misreads, suppress rather than restructure: keep
static_cast<T&&>(x)moves in the kernel headers (// cppcheck-suppress migraphx-RedundantCast), and keep::valuereads in tests (migraphx-AvoidNestedValue; usecppcheck-suppress-begin/-endaround a block ofstatic_asserts). Insrcheaders apply the real fix (is_void<T>::value→is_void<T>{}). - When cppcheck's preprocessor cannot parse code (recursive macros), guard it with
#ifndef CPPCHECK— never// cppcheck-suppress syntaxError, which silently aborts analysis of everything after it. - cppcheck and clang-tidy can demand opposite things on one cast
(
RedundantStaticCastOpversusreadability-implicit-bool-conversion): cast the bool to a type that differs from the other operand, e.g.sizeof(T) * 8 - static_cast<int>(is_signed). #elifdef/#elifndefare C++23; use#elif defined(...).
Style Guidelines
Avoid raw loops - Prefer algorithms from
<migraphx/algorithm.hpp>and STL<algorithm>:// Good: Declarative, expresses intent clearly std::transform(in.begin(), in.end(), std::back_inserter(out), [](int i) { return i * 2; }); // Avoid: Raw loops are error-prone and less optimizable for (int i : in) { out.push_back(i * 2); } // Note: std::for_each should NOT be used as it doesn't encapsulate a raw loop // It's just a wrapper around a for loop - use std::transform or other algorithms insteadMIGraphX-specific algorithms (
#include <migraphx/algorithm.hpp>):transform_if(start, last, out, pred, f)- Transform with filteringtransform_accumulate(first, last, init, binop, unaryop)- Accumulate with projectiontransform_partial_sum(first, last, out, binop, unaryop)- Partial sum with projectiongroup_by(start, last, out, pred)- Group elements by predicategroup_unique(start, last, out, pred)- Group consecutive unique elementsgroup_find(start, last, pred, out)- Find and group matching rangesadjacent_remove_if(first, last, pred)- Remove based on adjacent pairsadjacent_for_each(first, last, f)- Iterate over adjacent pairsfor_each(first1, last1, first2, f)- Two-range iteration (like std::transform)
If no suitable algorithm exists: Add a new algorithm to
migraphx/algorithm.hpprather than using raw loops- Memory management - Use
std::make_unique/shared, avoid rawnew/delete - Non-memory resources - Use
MIGRAPHX_MANAGE_PTRfor C-style acquire/release APIs:using file_ptr = MIGRAPHX_MANAGE_PTR(FILE*, fclose); file_ptr f{fopen(filename, "r")}; // Automatically closed on scope exit - Type erasure over inheritance - MIGraphX uses type erasure extensively for
pass,operation,target- No need to inherit from base class - just implement required interface methods
- Generate boilerplate with
make generatewhen interfaces change - Enables value semantics with polymorphism, no virtual inheritance overhead
- Pass design principles:
- Keep passes idempotent (running twice = running once)
- Keep passes deterministic (same input → same output)
- Always handle dynamic shapes in
compute_shape(unknown dimensions)
- Generic programming - Write reusable, type-independent code using templates and STL algorithms
- Avoid Casts - Don't use a cast unless absolutely necessary. Declare correct types. Casts indicate a type mismatch that should be resolved at the source.
- Encapsulate Bit Manipulation - Put bit-twiddling and low-level operations behind well-named utility functions:
- Prefer
std::bitsetfor bit manipulation
- Prefer
- Use std::tie for Lexicographical Comparisons - Use
std::tieorstd::lexicographical_compareinstead of manually writing lexicographical comparisons. - Use shape class to compute offsets and indexing - The
migraphx::shapeclass provides methods for computing offsets, strides, and indexing. Use these instead of manual calculations with mod and division. This applies in kernel code too:constexpr auto gs = make_shape(index_ints<G, N>{}); auto m = gs.multi(i);rather thani / Nandi % N. - Structured bindings - Unpack pair/tuple returns with
auto [a, b] = f(...), notauto r = f(...); auto& a = r.first;. A C++17 lambda cannot capture a structured binding directly (-Wc++20-extensions); use an init-capture —[&, &x = x]— rather than avoiding the binding. - No locally-called named lambdas - A lambda that is only called from the enclosing function (
auto used_outside = [&](...) {...};) is a function in disguise with hidden captures. Make it astaticfree function with explicit parameters, or inline it at the single call site. Lambdas that are passed to an algorithm or callback are fine. - Helpers in headers - A non-template helper is declared in the header (with the target's export macro, e.g.
MIGRAPHX_GPU_EXPORT) and implemented in the.cpp. Only templates and trivial one-line accessors stay inline; an inline definition pulls its includes into every consumer. - Split big functions into a helper struct, not static helpers with long parameter lists - When
readability-function-sizeforces a split and the pieces would each take 5+ parameters, make a (nested) struct whose value members hold the shared state and whose methods add to it (seetuning_solutionsinsrc/targets/gpu/jit/reduce.cpp). Value members only, never reference members;context&and other external state are still passed to the methods that need them. - Static member helpers stay where they are - A
staticmember function that uses no member state is still scoped to the class it serves on purpose. Do not hoist it to a file-static free function during cleanup. - Comments - One to three lines stating the non-obvious why: the constraint, invariant, or workaround. Do not restate what the code shows or enumerate consequences.
- Narrow predicates do not go in core headers - Do not add a GPU- or conv-specific helper such as
is_nhwc(const shape&)to a widely included header likepermutation.hpp; write the stride check inline at each site with a comment. - JIT kernel source templates stay explicit - Keep one raw-string template per kernel rather than a shared
${...}shell; readability of the generated source beats de-duplicating the boilerplate. - Rewrites use insert + replace + DCE - Insert the new instruction,
replace_instruction(ins, new_ref), and letdead_code_eliminationremove the old one. Do not rewrite an instruction in place withreplace_instruction(ins, op, inputs)to avoid leaving dead code; if a later step of the same pass must not see the stale instruction, rundead_code_eliminationbetween the steps.
Code Quality
- Prefer correct, complete implementations over minimal ones.
- Use appropriate data structures and algorithms — don't brute-force what has a known better solution.
- When fixing a bug, fix the root cause, not the symptom.
- If something I asked for requires error handling or validation to work reliably, include it without asking.
Code Architecture
Core IR Components
Program (src/include/migraphx/program.hpp):
- Contains one or more modules
- Manages compilation via
program::compile(target) - Entry point:
get_main_module()returns main execution module
Module (src/include/migraphx/module.hpp):
- DAG of instructions
- Add instructions via
module::add_instruction(op, inputs...) - Add parameters via
module::add_parameter(name, shape)
Instruction (src/include/migraphx/instruction.hpp):
- References an operation and input instructions
- Mutable IR node/reference; passes may rewrite instructions in place via APIs such as
replace,set_normalized, andset_target_id
Operation (src/include/migraphx/operation.hpp):
- Defines computation semantics
- Must implement:
name(),compute_shape(inputs) - Optionally:
compute(ctx, output, inputs),finalize(ctx, shape, inputs)
Backends
A target provides get_passes, get_context, and memory ops; see GPU under src/targets/gpu/ and CPU under src/targets/cpu/.
Compilation Pipeline
- Parse model (ONNX/TF) → Generic IR
- Run optimization passes (
pass_manager.cpp) - Target lowering (
targets/{gpu,cpu}/) applies backend-specific transformations - Code generation (GPU: kernel fusion, HIP code objects; CPU: direct ops)
- Execution via
program::eval(params)orprogram::run(params)
Core Data Structures
Shape, Literal, Argument, and Tensor View
migraphx::shape - Describes tensor properties (one of the most important classes):
- Data type (e.g.,
float_type,int32_type) - Dimensions/lengths (e.g.,
{1, 3, 224, 224}) - Memory layout/strides
- States:
.packed(),.transposed(),.broadcasted(),.standard()
migraphx::literal - Immutable data buffer with a shape (compile-time constants like weights)
migraphx::argument - Mutable data buffer with a shape (runtime results and inputs)
migraphx::tensor_view<T> - Type-safe multi-dimensional view combining T* pointer with a shape
Visitor pattern for type-safe data access:
migraphx::literal l{migraphx::shape{migraphx::shape::float_type, {2, 2}}, {1.0f, 2.0f, 3.0f, 4.0f}};
l.visit([](auto view) {
// 'view' is a tensor_view<float>
std::cout << view(1, 1) << std::endl; // Prints 4.0
});Parameters and Evaluation
Add parameters to modules for runtime inputs:
migraphx::shape s{migraphx::shape::int32_type, {1}};
auto x = mm->add_parameter("x", s); // Creates placeholder instruction
// Later, evaluate with actual data:
migraphx::parameter_map params;
std::vector<int> data = {4};
params["x"] = migraphx::argument(s, data.data());
auto result = p.eval(params).back();Extension Patterns
Adding an Operation
- Create operation struct in
src/include/migraphx/op/<name>.hpp:
struct my_op {
std::string name() const { return "my_op"; }
shape compute_shape(std::vector<shape> inputs) const {
// Compute output shape from inputs
return inputs[0];
}
// Optional: for reference target
argument compute(context& ctx, const shape& output,
std::vector<argument> args) const {
// Compute implementation
}
};- Register operation (automatic via include pattern or explicit):
MIGRAPHX_REGISTER_OP(my_op)- Add tests in
test/op_shape_test.cpportest/my_op_test.cpp
Place headers in src/include/migraphx/op/ and implementation in src/ if not header-only. Reflect attributes if needed.
Adding an Optimization Pass
Simple implementation:
#include <migraphx/pass.hpp>
struct my_pass {
std::string name() const { return "my_pass"; }
void apply(module& m) const {
// Transform module instructions
}
// Or use module_pass_manager:
// void apply(module_pass_manager& mpm) const;
};Using matchers (preferred for pattern matching):
Create a matcher struct with matcher() and apply() methods:
namespace {
struct find_mul_by_one
{
auto matcher() const
{
// Match: mul(1, x) or mul(x, 1)
return match::name("mul")(match::either_arg(0, 1)(
match::is_constant(1.0f),
match::any().bind("x")));
}
void apply(module& m, const match::matcher_result& r) const
{
auto ins = r.result;
auto x = r.instructions["x"];
m.replace_instruction(ins, x);
}
};
} // namespace
void simplify_mul::apply(module& m) const
{
match::find_matches(m, find_mul_by_one{});
}Built-in matchers:
match::name("op_name")- Match operation by namematch::any()- Match any instructionmatch::is_constant(value)- Match literal with specific valuematch::args(m1, m2, ...)- Match positional argumentsmatch::arg(i)(m)- Match i-th argumentmatch::either_arg(0, 1)(m1, m2)- Match either argument order.bind("name")- Bind matched instruction to retrieve later
Custom matchers:
MIGRAPHX_PRED_MATCHER(broadcasted_shape, instruction_ref ins) {
return ins->get_shape().broadcasted();
}
// Usage: auto m = broadcasted_shape();Integration:
- Wire into target's
get_passes()method - For driver testing: register in
src/driver/passes.cpp - Add tests mirroring
test/eliminate_concat_test.cpp
Adding a Backend Target
- Implement target interface in
src/targets/mytarget/:
struct mytarget {
std::string name() const { return "mytarget"; }
std::vector<pass> get_passes(context& ctx,
const compile_options& options) const {
// Return optimization passes
}
context get_context() const {
// Return execution context
}
// Memory operations
argument allocate(const shape& s) const;
argument copy_to(const argument& arg) const;
argument copy_from(const argument& arg) const;
};- Register target:
MIGRAPHX_REGISTER_TARGET(mytarget)- Add target-specific tests in
test/mytarget/
Testing Guidelines
Framework
- CTest-driven executables generated from files in
test/(seetest/CMakeLists.txt) - Naming: prefer
*_test.cpp; CMake auto-creates an executabletest_<topic>and registers it with CTest - Colocate helpers under
test/include/when needed - Coverage: CI uploads via Codecov; keep unit tests focused and fast
Test Directory Organization
test/- Core unit tests (no specific backend), target-independenttest/gpu/- GPU-specific tests (requiresMIGRAPHX_ENABLE_GPU=Onand a ROCm device); build astest_gpu_<topic>test/ref/- Reference operator behavior teststest/verify/- Multi-target result comparison (GPU vs reference)test/onnx/parse/- ONNX model parsing tests (no execution)test/onnx/verify/- ONNX model execution + numerical comparisontest/tf/- TensorFlow parser teststest/api/- C/C++ API validationtest/py/- Python binding tests
Running Tests
# All tests
make -C build check
# Single test file
ctest -R '^test_<topic>$'
# Single test case with pattern
./bin/test_<topic> 'pattern*'
# GPU tests only
ctest -R '^test_gpu_'
# List all test cases in a file
./bin/test_<topic> --list
# Run test cases starting from a given name or glob (in registration order),
# skipping the ones before it
./bin/test_<topic> --start-from '<name-or-glob>'Unit Tests
Basic test structure:
#include "test.hpp" // From test/include/
TEST_CASE(my_feature) {
EXPECT(condition);
CHECK(other_condition);
}Module/Program test pattern:
#include <migraphx/program.hpp>
#include <migraphx/make_op.hpp>
#include "test.hpp"
// The only helper a test file should define - see "Keep tests self-contained" below
static void run_pass(migraphx::module& m)
{
migraphx::run_passes(m, {migraphx::my_pass{}, migraphx::dead_code_elimination{}});
}
TEST_CASE(test_my_pass)
{
migraphx::module m1;
{
auto input = m1.add_parameter("input", {migraphx::shape::float_type, {3, 3}});
auto result = m1.add_instruction(migraphx::make_op("relu"), input);
m1.add_return({result});
}
run_pass(m1);
migraphx::module m2;
{
auto input = m2.add_parameter("input", {migraphx::shape::float_type, {3, 3}});
auto result = m2.add_instruction(migraphx::make_op("relu"), input);
m2.add_return({result});
}
EXPECT(m1 == m2);
}Keep tests self-contained:
A unit test should be readable top to bottom without jumping to another function. Prefer duplication inside tests over indirection between them.
- Build graphs inline - call
add_instruction/insert_instructiondirectly in theTEST_CASE(or in the braced block for each module) so the test reads as the literal IR under test - No helpers for instruction creation, shape construction, parameter setup, or expected-module construction - write
{migraphx::shape::float_type, {3, 3}}at the use site rather than behind amake_shape()style factory - Bar for adding any test-local helper, both conditions must hold: it removes more than 5 instructions/lines of setup, and it is reused by at least two test cases. Anything cheaper than that gets inlined
- One sanctioned exception: a single file-level
run_pass(migraphx::module&)(orrun_pass(migraphx::program&)) that only forwards tomigraphx::run_passes, as shown above. Do not layer further helpers on top of it and do not add per-test variants; if a test needs a different pass list, callmigraphx::run_passesinline - Shared utilities already in
test/include/(test.hpp,basic_ops.hpp,pointwise.hpp,reduce.hpp,verify_rms_range) remain fine to use; this rule governs helpers you would write inside a test file
// Good: the graph and its shapes are visible in the test
TEST_CASE(test_relu)
{
migraphx::module m;
auto x = m.add_parameter("x", {migraphx::shape::float_type, {3, 3}});
auto y = m.add_instruction(migraphx::make_op("relu"), x);
m.add_return({y});
run_pass(m);
// ...
}
// Avoid: wrappers hide the graph and the shapes from the reader
static migraphx::shape make_shape() { return {migraphx::shape::float_type, {3, 3}}; }
static migraphx::instruction_ref add_relu(migraphx::module& m, migraphx::instruction_ref x)
{
return m.add_instruction(migraphx::make_op("relu"), x);
}
TEST_CASE(test_relu)
{
migraphx::module m;
auto x = m.add_parameter("x", make_shape());
m.add_return({add_relu(m, x)});
run_pass(m);
// ...
}Tests over several types: write the test as a function template and register each instantiation, so a failure names the type and each one is listable:
template <class T>
static void match_foo()
{
// ...
}
TEST_CASE_REGISTER(match_foo<std::uint32_t>);
TEST_CASE_REGISTER(match_foo<std::int64_t>);Do not write an expect_* helper template called from one plain TEST_CASE. Kernel
tests under test/gpu/kernels use template <class T> TEST_CASE_TEMPLATE(name) plus
TEST_CASE_REGISTER(name<T>) instead, and never take the test manager as an explicit
parameter.
Testing Passes
When adding a pass that rewrites structure:
- Build a small graph with the pattern to optimize
- Run only the new pass (plus prerequisites and dead_code_elimination for cleanup)
- Create a new module with the expected final structure and assert that it is the same
This is the only form a pass unit test takes: build the exact expected module by hand
and EXPECT(m1 == m2). Do not write property-based or randomized sweeps as the pass
test; numerical checks of a transformed program go in test/verify/ with fixed inputs.
When the pass must leave a module unchanged, build it once, copy it, and compare instead of constructing it twice:
migraphx::module m2 = m1;
run_pass(m1);
EXPECT(m1 == m2);Use driver for inspection: migraphx-driver compile --text --apply-pass your_pass
(-p is the short form; both append, so repeat them to run several passes in order.
There is no --passes option.)
Which tests to run
Run only the tests that exercise the code you changed; grepping for a keyword and
running everything that matches is slower and no more informative. A change to an op's
point_op() codegen string does not need the ONNX parser tests; a kernel-header change
does not need the host-side shape tests unless shape logic moved.
Do not modify a failing test to match the new output. The tests express the contract; fix the implementation, and if the contract itself must change, say so explicitly.
Numerical Verification
- Use
verify_rms_rangefor tolerance-based comparisons with floating-point noise - Provide deterministic inputs (explicit literals or seeded random generators)
- Avoid flaky thresholds by using stable, reproducible test data
Verify tests
- Tests under
test/verifywill verify the results using the ref target - Each verify class should go into a separate .cpp file
Test Best Practices
Speed principles:
- Favor shapes < 64 elements for scalar algorithms
- Use single-digit iteration counts
- Reuse shapes across tests to exploit compiler cache hits
- To skip large-buffer tests, configure
-DMIGRAPHX_DISABLE_LARGE_BUFFER_TESTS=On
Edge cases to cover:
- Zero-length dimensions
- Dynamic shapes at extreme min/max
- Mixed type promotion (int + float → float)
- Broadcasting asymmetries
- Reduction axes ordering
Regression strategy:
- When fixing subtle bugs (e.g., stride miscalculation), add test reproducing original failure
- Keep golden ONNX/TF assets minimal (large models slow CI and obscure failures)
- For new operator parsing, a model containing just that op suffices
Test utilities: test/include/test.hpp, basic_ops.hpp, pointwise.hpp, reduce.hpp
GPU Backend Details
The GPU backend (src/targets/gpu/) implements:
- Lowering (
lowering.cpp): Transform generic ops to GPU-specific ops - Fusion (
fuse_ops.cpp,fuse_ck.cpp; shared pointwise fusion insrc/fuse_pointwise.cpp): Kernel fusion optimizations - Code generation (
compile_hip*.cpp,compile_miopen.cpp): HIP/MIOpen/rocBLAS dispatch - Scheduling (
schedule_model.cpp): Select optimal implementation - Performance DB (
perfdb.cpp,problem_cache.cpp): Cache tuning results
Key subdirectories:
device/- Device-side utilities and kernelskernels/- Custom kernel implementationsinclude/migraphx/gpu/- GPU-specific headers
Kernel header notes:
- Kernel headers are recompiled by the JIT at runtime, so template instantiation count is a real cost.
if constexprspecial cases that look redundant with the generic path (e.g.arg_cforN == 0) exist to cut instantiations; keep them and say so in a one-line comment. - The kernel functional library (
kernels/functional.hppand friends) is a general-purpose toolkit: a helper is kept even when a change removes its last caller. - Inline asm must reference operands as
%[name]; never hardcode register numbers, which conflict with the compiler's allocator. Large hand-tuned asm kernels with explicit register allocation (MIOpen's Winograd kernels) cannot be embedded as inline asm and need a standalone code object. - Wavefront size is a target property: RDNA parts such as gfx1201 are wave32, not wave64.
Fusion Strategies
MIGraphX employs multiple fusion stages to reduce kernel overhead and improve memory locality:
1. Prefusion (GPU-specific):
- Replace recognizable operator clusters with custom placeholders before lowering
- Passes:
prefuse_ops,fuse_attention - Detects high-level patterns like transformer attention (Q*K^T scaling, softmax, V multiplication)
2. Pointwise/Reduce Fusion:
fuse_pointwise: Groups pure elementwise chains into submodules for single-kernel code generationfuse_reduce/fuse_pointwise_reduce: Combines reductions sharing compatible axes- Flags:
enable_rewrite_reshapes,enable_rewrite_broadcasts,enable_multi_output - Critical for layernorm and attention patterns
3. Backend Post-Lowering Fusion:
fuse_ops: Combine lowered ops with adjacent pointwise or layout adjustmentsfuse_ck: Leverage Composable Kernel library for GEMM + epilogues (bias, activation)fuse_mlir: Groups pointwise + reshape sequences around GEMM/Conv for MLIR pipeline
Troubleshooting missing fusion:
- Check if earlier passes canonicalized shapes (
simplify_reshapes) - Verify broadcasts are explicit/implicit as expected
- Check if dynamic dims prevent grouping
- Ensure op type lists support new data types (fp8/bf16)
- Use
MIGRAPHX_TRACE_COMPILE=1to confirm fusion pass execution
Target Compilation Flow
Target object provides:
get_passes(context&, compile_options&)- Ordered backend-specific pass list- Allocation model and context types
- Lowering rules mapping high-level ops to target intrinsics/library calls
Compile options control:
fast_math- Relaxed math transformationsexhaustive_tune- Full search for fastest GEMM/convolution implementation- Quantization toggles (fp16/int8/fp8) for op substitution
Memory planning:
memory_coloring- Assign buffers to reuse memory where lifetimes don't overlapadjust_allocation/replace_allocate- Coordinate with target allocation modelsschedule- Order instructions for optimal overlap/locality (disable:MIGRAPHX_DISABLE_SCHEDULE_PASS=1)
Library integration:
- GEMM/DOT: HIPBLASLt or Composable Kernel (
MIGRAPHX_SET_GEMM_PROVIDER) - Convolution: MIOpen if available (
MIGRAPHX_USE_MIOPEN), else custom kernels - Precision handling:
propagate_precision,rewrite_low_precisionfor mixed precision (fp16/fp8/bf16)
Type Erasure System
MIGraphX uses type erasure extensively to create extensible systems without traditional inheritance. This pattern is used for pass, operation, target, instruction, and other core abstractions. It enables value semantics with polymorphic behavior, no virtual inheritance overhead.
Creating New Type-Erased Interfaces
1. Define interface template in tools/include/<name>.hpp:
<%
interface('pass',
virtual('name', returns='std::string', const=True),
virtual('apply', returns='void', mpm='module_pass_manager &', const=True,
default='migraphx::detail::module_pass_manager_apply'),
virtual('apply', returns='void', p='program &', const=True,
default='migraphx::nop')
)
%>Syntax explanation:
interface('pass', ...)- Defines type-erased interface namedpassvirtual(...)- Defines one method in the interface- First arg: method name (e.g.,
'name') returns='...'- C++ return typeconst=True- Method is const- Method arguments as keyword args:
p='program &' default='...'- Optional default implementation fallback
- First arg: method name (e.g.,
2. Generate boilerplate:
make generate # Runs tools/te.py on templates in tools/include/3. Generated header placed in src/include/migraphx/<name>.hpp contains wrapper class with concept/model pattern for virtual dispatch.
Using Type-Erased Objects
No inheritance required - just implement the interface:
struct my_custom_pass {
std::string name() const { return "my_custom_pass"; }
void apply(module& m) const { /* transformation logic */ }
};
// Use directly:
migraphx::pass p = my_custom_pass{}; // Value semantics!Frontend Parsers
ONNX Parser (src/onnx/)
- Parses ONNX models into MIGraphX IR
- Located in
src/onnx/parse_*.cppfiles - Each ONNX operator has a corresponding parse function
Adding a new ONNX operator:
- Add parse function in
src/onnx/parse_<op>.cpp - Register in ONNX parser operator map
- Map ONNX attributes to MIGraphX operation parameters
- Handle shape inference and broadcasting
- Add test in
test/onnx/parse/(parsing only) andtest/onnx/verify/(execution)
TensorFlow Parser follows similar pattern in src/tf/.
Python Bindings
Located in src/py/migraphx_py.cpp using pybind11.
Basic pattern:
import migraphx
# Parse model
model = migraphx.parse_onnx("model.onnx")
# Compile for target
model.compile(migraphx.get_target("gpu"))
# Run inference
results = model.run({"input": input_data})When adding C++ APIs, mirror in Python bindings following existing patterns.
migraphx-driver CLI Tool
The driver (./bin/migraphx-driver) is essential for development, debugging, and testing without writing C++ code.
Commands
| Command | Description |
op -l/--list | Print all available MIGraphX operators |
params | Print input/output parameter shapes |
read | Load and print input graph |
compile | Compile and print input graph |
run | Compile, allocate parameters, evaluate, and print graph |
verify | Run on reference and GPU, check output consistency |
perf | Compile and run, then print performance report |
Key Options
| Option | Description |
--onnx | Load file as ONNX graph |
--tf | Load file as TensorFlow graph |
--migraphx | Load file as MIGraphX graph |
--gpu / --cpu / --ref | Target backend for compilation |
--fp16 / --int8 | Quantization mode |
--text / --json / --binary | Output format |
--batch N | Set batch size for model |
--input-dim @input 1 3 224 224 | Set static dimensions |
-n N / --iterations N | Number of iterations for perf |
--fill0 / --fill1 | Fill parameters with 0s or 1s |
-o FILE / --output FILE | Write output to file |
Usage Examples
Compile and inspect IR:
migraphx-driver compile model.onnx --gpu --textApply a driver pass:
migraphx-driver compile model.onnx --apply-pass dead_code_elimination --textPerformance benchmarking:
migraphx-driver perf model.onnx --gpu -n 100Verify numerical correctness:
migraphx-driver verify model.onnx --atol 1e-5 --rtol 1e-5Register custom passes: Add to src/driver/passes.cpp for driver access.
Debugging & Tracing
Environment variables:
MIGRAPHX_TRACE_COMPILE=1- Trace compilation passesMIGRAPHX_TRACE_EVAL=1- Trace evaluationMIGRAPHX_DISABLE_SCHEDULE_PASS=1- Disable scheduling for debuggingMIGRAPHX_DISABLE_PASSES=a,b- Skip passes by name; bisect which pass corrupts the IRMIGRAPHX_GPU_DUMP_ASM=1- Dump the ISA of the C++ kernels; first configure CMake with-DMIGRAPHX_USE_HIPRTC=OFFand rebuild. MIGraphX JIT-compiles itself (not rocMLIR'smlir_*kernels). A pure rename inside a kernel must leave the dump byte-identical.MIGRAPHX_GPU_DEBUG=1- Enable kernel asserts at runtime (works on a Release build)
Env vars are read once per process and memoized. Never add an env read that bypasses the
MIGRAPHX_<NAME>{} object pattern, and never use an env var to override per-shape tuning.
Asserts and the local build: a Release build defines NDEBUG, so assert() never
fires locally and a test can pass while violating an assert that the CI debug build trips.
After adding an IR-rewrite path, grep the helpers it calls for assert( and check each
one holds (e.g. module_with_inputs::replace asserts matching lens).
Performance reporting:
program.perf_report(std::ostream&, iterations, parameter_map)- Get timing statsmigraphx-driver perf --onnx model.onnx --gpu -n 50- Per-operator times for a whole model; the numbers include kernel-launch overheadmigraphx-driver time- One isolated kernel without launch overhead; use it for the clean number onceperfhas told you which op to look atmigraphx-driver time -b Nrotates N parameter maps so inputs are cold; size N past the last-level cache when measuring nontemporal or streaming loads
Benchmarking two variants: absolute kernel times drift with GPU clocks and thermal
state, so only the ratio of two variants measured in the same window is trustworthy.
Alternate A and B back-to-back in the same loop, with no exhaustive-tune or other heavy
GPU work between them, and report the median per-iteration ratio over at least five
rounds. Never compare a number from one sweep against a number from another, and never
benchmark while make analyze or another all-core CPU job runs — it skews the
compile-time solution benchmarking as well, so the tuner picks the wrong solution.
IR inspection:
program.debug_print()- Print IR to stdout- GraphViz output via
graphviz.cpphelpers - ROCm profiling markers via
marker_roctx.*
Key CMake Options
MIGRAPHX_ENABLE_GPU- Enable GPU backend (default: ON if HIP available)MIGRAPHX_ENABLE_CPU- Enable CPU backend (default: OFF)MIGRAPHX_ENABLE_PYTHON- Build Python bindings (default: ON)MIGRAPHX_USE_MIOPEN- Use MIOpen library (default: ON)MIGRAPHX_USE_ROCBLAS- Use rocBLAS library (default: ON)MIGRAPHX_USE_HIPBLASLT- Use hipBLASLt (default: ON, requires rocBLAS)MIGRAPHX_USE_COMPOSABLEKERNEL- Use composable kernel JIT (default: ON)MIGRAPHX_DISABLE_LARGE_BUFFER_TESTS- Skip large-buffer testsBUILD_DEV- Development mode with extra checksGPU_TARGETS- Target GPU architectures (use detection snippet)
Example: cmake -S . -B build -DMIGRAPHX_ENABLE_GPU=On -DGPU_TARGETS=$(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*')
Common Issues
- Missing GPU_TARGETS: Always specify via
$(/opt/rocm/bin/rocminfo | grep -o -m1 'gfx.*') - Dynamic shape errors: Ensure
compute_shape()handles unknown dimensions - Pass ordering: Passes have dependencies - check existing target pipelines
- Kernel packaging: New GPU kernels must be included in CMake install targets
- Build failures: Check clang-tidy warnings - they're enforced in CI
reshapecopies at runtime:op::reshapeandflattenallocate and copy incompute; they are not views and have nooutput_alias. The runtime view op isreshape_lazy. A pass that inserts a generated view chain (e.g. fromshape_transform_descriptor::generate()) into a lowered GPU graph must convertreshapetoreshape_lazyand reject chains that cannot alias (seeinvert_alias_transformsinsrc/replace_allocate.cpp); violating this is a GPU VM fault in the kernel writing through the "view". Separately, do not insertcontiguousbeforereshape— reshape already handles non-standard input layouts.squeeze/unsqueezeare not inverses on scalars:unsqueezereturns a rank-1{1}input unchanged, and squeezing all axes clamps to rank-1{1}. To invert a squeeze on an input whose lens are{1}, usemultibroadcastto the reduce lens instead ofunsqueeze.- CI runner "shutdown signal" / exit 143 mid-compile: the runner was OOM-killed compiling a matcher. A matcher that embeds the same sub-matcher more than once grows the nested closure type exponentially; local clang copes, but gcc and MSVC-target clang (where
MIGRAPHX_USE_TYPE_ERASED_OPAQUE_MATCHERis 1) do not. Wrap each intermediate level inmatch::opaque(...)assrc/fuse_attention.cppdoes. Reproduce with g++ 13 using the flags frombuild/compile_commands.jsonunderulimit -v. - Target knobs: expose a new target setting through
compile_options::backend_options(the reflected struct insrc/targets/gpu/target.cpp), not a dedicatedMIGRAPHX_*env var;MIGRAPHX_GPU_OPTIONSalready provides environment control as a JSON object merged into the backend options. Tests setoptions.backend_options["name"] = value— a test that callssetenvleaks into every later test in the process.
Reference Materials
When implementing new features, search for analogous implementations:
- Operations:
grep -r "similar_op" src/op/ - Passes:
grep -r "similar_pass" src/ - GPU lowering: Check
src/targets/gpu/lowering.cpp - Tests: Mirror structure from
test/<similar_feature>_test.cpp - Matchers: See existing patterns in
src/matcher.cpp - ONNX ops: Look at similar operators in
src/onnx/parse_*.cpp
Key header files to understand:
src/include/migraphx/program.hpp- Top-level containersrc/include/migraphx/module.hpp- DAG of instructionssrc/include/migraphx/instruction.hpp- Graph nodessrc/include/migraphx/operation.hpp- Operation abstractionsrc/include/migraphx/shape.hpp- Tensor shape/layoutsrc/include/migraphx/pass.hpp- Pass interfacesrc/include/migraphx/matcher.hpp- Pattern matching DSL
Documentation:
- Official docs:
docs/directory (Sphinx + Doxygen) - Developer guides:
docs/directory (detailed contributor documentation) - Build documentation:
make -C build doc
Commit & Pull Request Guidelines
- Commits: Use imperative, concise subject lines (optionally add a scope), e.g.,
fix: correct hip stream handling. Reference issues/PRs (Fixes #123). - PRs: Include problem statement, approach, and risk notes; link issues. Show test output for affected areas and add tests for new behavior. Ensure formatting/linters pass and CI is green.
Running Commands
- Do not use
$(nproc), runnprocfirst, then use the numeric value directly. - When running
make -j$(nproc), first resolve$(nproc)to a literal number, e.g. runnprocfirst, then use the numeric value directly in the make command (i.e.,make -j32ifnprocreturns32).
Workspace Edit Rules
- When creating any temporary/scratch/generated file, write it under
tmp/at the workspace root. - Use
tmp/<task-slug>/<YYYYMMDD-HHMMSS>/for groups of files. - Never place temp files outside
tmp/or inside source directories.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- .cursor/rules/split-sym-dim-pass-tests.mdcCursor rule
- migraphx-code-reviewSkill
- migraphx-simplifySkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

