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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,20 @@ jobs:
- uses: actions/checkout@v4

# The package config carries its own copy of the target list so it can
# reject a typo at configure time. That copy has to keep up with main.c,
# and nothing else would notice if it did not.
# reject a typo at configure time. That copy has to keep up with the
# backends, which is where target flags are declared now, and nothing
# else would notice if it did not.
- name: Compare target lists
run: |
grep -oE '"--(gfx[0-9]+[a-z]?|xe[0-9a-z-]*)"' src/main.c \
grep -ohE '"--(gfx[0-9]+[a-z]?|xe[0-9a-z-]*)"' src/*/*_be.c \
| tr -d '"' | sed 's/^--//' | sort -u > /tmp/kath_arches
sed -n '/set(_arches/,/)/p' cmake/BoothConfig.cmake.in \
| sed -e 's/set(_arches//' -e 's/)//' | tr -s ' \t' '\n' \
| grep -E '^(gfx|xe)' | sort -u > /tmp/cmake_arches
if ! diff -u /tmp/kath_arches /tmp/cmake_arches; then
echo
echo "The target list in cmake/BoothConfig.cmake.in has drifted from"
echo "the flags main.c accepts. Lines marked - are in the compiler but"
echo "the flags the backends declare. Lines marked - are in the compiler but"
echo "not the package config; + is the other way round."
exit 1
fi
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ Booth — Changelog
- #137: support bare convergent warp and lane intrinsics
(Maou, 2026-07-27)

### Architecture

- one target per run. Several backends at once all wrote to the same
`-o` path, so you got whichever came last in the registry under the
name you asked for, and a zero exit

- backend contract (`be_desc_t`) with static registration; every
existing backend sits behind the same shape and the driver iterates
`be_list` instead of the copy-pasted if-chain. A backend also owns its
own command line now, so adding a target means one file and one line in
the list rather than editing a shared config struct and the driver's
argument loop. Skeleton in `src/backend/skeleton/` and
`docs/backends.md` for anyone adding a target
(Zane Hambly, 2026-08-02)

### Backends

- seed atomic RMW as divergent in the AMD divergence analysis, so a GEP off
Expand Down
29 changes: 20 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CFLAGS = -std=c99 -MMD -MP -Wall -Wextra -pedantic -O2 \
-Wdouble-promotion -Wswitch-enum -Wwrite-strings \
-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE $(CF_PROT) \
$(GCC_ONLY) \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/backend -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime
LDFLAGS = -pie
LIBS = -lm
# Linux/ELF only: -Wl,-z,relro,-z,now -Wl,-z,noexecstack
Expand Down Expand Up @@ -60,12 +60,13 @@ SOURCES = src/main.c \
src/fe/bc_err.c src/fe/bc_render.c src/fe/preproc.c src/fe/lexer.c src/fe/parser.c src/fe/sema.c \
src/ir/bir.c src/ir/bir_print.c src/ir/bir_lower.c src/ir/bir_mem2reg.c src/ir/bir_cfold.c src/ir/bir_dce.c src/ir/bir_struct.c src/ir/bir_insert.c src/ir/bir_sroa.c src/ir/bir_inline.c \
src/tdf/tdf.c src/tdf/tdf_lower.c src/tdf/tdf_fission.c src/tdf/tdf_place.c src/tdf/tdf_noc.c \
src/amdgpu/amd_rplan.c src/amdgpu/isel.c src/amdgpu/emit.c src/amdgpu/ra_ssa.c src/amdgpu/encode.c src/amdgpu/enc_tab.c src/amdgpu/sched.c src/amdgpu/verify.c \
src/backend/backends.c \
src/amdgpu/amd_rplan.c src/amdgpu/isel.c src/amdgpu/emit.c src/amdgpu/ra_ssa.c src/amdgpu/encode.c src/amdgpu/enc_tab.c src/amdgpu/sched.c src/amdgpu/verify.c src/amdgpu/amd_be.c \
src/tensix/isel.c src/tensix/emit.c src/tensix/coarsen.c src/tensix/datamov.c src/tensix/noc.c \
src/tensix/rv_enc.c src/tensix/rv_buf.c src/tensix/rv_elf.c src/tensix/rv_isel.c src/cpu/cpu_emit.c src/cpu/cpu_elf.c src/cpu/rv64_emit.c src/cpu/rv64_elf.c \
src/nvidia/isel.c src/nvidia/emit.c \
src/metal/emit.c \
src/intel/emit.c \
src/tensix/rv_enc.c src/tensix/rv_buf.c src/tensix/rv_elf.c src/tensix/rv_isel.c src/tensix/tensix_be.c src/cpu/cpu_emit.c src/cpu/cpu_elf.c src/cpu/rv64_emit.c src/cpu/rv64_elf.c src/cpu/cpu_be.c \
src/nvidia/isel.c src/nvidia/emit.c src/nvidia/nv_be.c \
src/metal/emit.c src/metal/metal_be.c \
src/intel/emit.c src/intel/intel_be.c \
src/triton/lex.c src/triton/parse.c src/triton/sema.c src/triton/lower.c
OBJECTS = $(SOURCES:%.c=$(OBJDIR)/%.o)
TARGET = kath
Expand All @@ -81,7 +82,7 @@ $(OBJDIR)/%.o: %.c

# ---- Test Suite ----
TCFLAGS = -std=c99 -MMD -MP -D_POSIX_C_SOURCE=200809L -Wall -Wextra -O0 -g \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime \
-Isrc -Isrc/fe -Isrc/ir -Isrc/tdf -Isrc/backend -Isrc/amdgpu -Isrc/tensix -Isrc/nvidia -Isrc/metal -Isrc/intel -Isrc/triton -Isrc/cpu -Isrc/runtime \
-Iruntime
TSRC = tests/tmain.c tests/tsmoke.c tests/tcomp.c tests/tenc.c \
tests/ttabs.c tests/ttypes.c tests/terrs.c tests/tphase.c \
Expand All @@ -102,7 +103,8 @@ TSRC = tests/tmain.c tests/tsmoke.c tests/tcomp.c tests/tenc.c \
tests/trv_enc.c tests/trv_buf.c tests/trv_elf.c tests/trv_isel.c \
tests/tcbsync.c \
tests/tsoft_fp.c \
tests/tsysprint.c
tests/tsysprint.c \
tests/tbackend.c

TOBJS = $(TSRC:%.c=$(OBJDIR)/%.o)
COBJS = $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir_print.o $(OBJDIR)/src/ir/bir_lower.o $(OBJDIR)/src/ir/bir_mem2reg.o $(OBJDIR)/src/ir/bir_cfold.o $(OBJDIR)/src/ir/bir_dce.o $(OBJDIR)/src/ir/bir_struct.o $(OBJDIR)/src/ir/bir_insert.o $(OBJDIR)/src/ir/bir_sroa.o $(OBJDIR)/src/ir/bir_inline.o \
Expand All @@ -111,7 +113,16 @@ COBJS = $(OBJDIR)/src/ir/bir.o $(OBJDIR)/src/ir/bir_print.o $(OBJDIR)/src/ir/b
$(OBJDIR)/runtime/soft_fp.o $(OBJDIR)/runtime/sysprint.o \
$(OBJDIR)/src/amdgpu/amd_rplan.o $(OBJDIR)/src/amdgpu/encode.o $(OBJDIR)/src/amdgpu/enc_tab.o $(OBJDIR)/src/amdgpu/isel.o $(OBJDIR)/src/amdgpu/emit.o $(OBJDIR)/src/amdgpu/ra_ssa.o $(OBJDIR)/src/amdgpu/sched.o $(OBJDIR)/src/amdgpu/verify.o \
$(OBJDIR)/src/fe/bc_err.o $(OBJDIR)/src/fe/lexer.o $(OBJDIR)/src/fe/parser.o $(OBJDIR)/src/fe/preproc.o $(OBJDIR)/src/fe/sema.o \
$(OBJDIR)/src/runtime/bc_abend.o $(HOSTRT)
$(OBJDIR)/src/runtime/bc_abend.o $(HOSTRT) \
$(OBJDIR)/src/backend/backends.o \
$(OBJDIR)/src/amdgpu/amd_be.o $(OBJDIR)/src/nvidia/nv_be.o \
$(OBJDIR)/src/tensix/tensix_be.o $(OBJDIR)/src/cpu/cpu_be.o \
$(OBJDIR)/src/metal/metal_be.o $(OBJDIR)/src/intel/intel_be.o \
$(OBJDIR)/src/nvidia/isel.o $(OBJDIR)/src/nvidia/emit.o \
$(OBJDIR)/src/cpu/cpu_emit.o $(OBJDIR)/src/cpu/cpu_elf.o \
$(OBJDIR)/src/cpu/rv64_emit.o $(OBJDIR)/src/cpu/rv64_elf.o \
$(OBJDIR)/src/tensix/isel.o $(OBJDIR)/src/tensix/coarsen.o $(OBJDIR)/src/tensix/datamov.o \
$(OBJDIR)/src/metal/emit.o $(OBJDIR)/src/intel/emit.o

test: $(TARGET) trunner
./trunner --all
Expand Down
133 changes: 133 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Adding a backend

[← back to README](../README.md)

If you want to add a target, or take one of the stubs and make it real,
this is the file for it. Everything a backend has to provide is in
`src/backend/backend.h`, and there is a skeleton in
`src/backend/skeleton/` you can copy and start filling in.

The reason this document exists at all is that the AMD backend is about
420 KB of source, and for a long time the only way to work out what a
backend actually had to do was to read it. That is a miserable way to
start. So the shape got pulled out into one struct with one page of
explanation, and now the AMD backend is just an example of the shape
rather than the definition of it.

## The pipeline

```
BIR -> isel -> [sched] -> [regalloc] -> [verify] -> emit -> file(s)
```

Only `isel` and `emit` are required. The bracketed phases are optional
and a NULL pointer means the driver skips that step, so a text-format
target like PTX can go straight from selection to emission without
pretending it has a register allocator.

## The contract

```c
typedef struct be_desc {
const char *name;
const char *triple;
uint32_t feats;
uint32_t opts_size;

const char *const *flags;
int (*parse) (const char *arg, const char *next, void *opts);

int (*is_on) (const void *opts);
uint32_t (*warp_size)(const void *opts);
int (*isel) (const struct bir_module *, const struct be_cfg *,
const void *opts, void **out_mmod);
int (*sched) (void *);
int (*regalc) (void *);
int (*verify) (const void *, int phase);
int (*emit) (const void *, const struct be_cfg *, const void *opts,
const char *path);
void (*mfree) (void *);
} be_desc_t;
```

A backend owns its own command line, which is the part I would most
like you to notice. `flags` is the NULL-terminated list of options you
answer for, `parse` gets called with each one and returns how many
extra argv entries it swallowed, and `opts` is a fixed slot the driver
holds on to and hands back to every other op. That means nothing about
your target needs to go anywhere near `be_cfg_t`, which now carries
only the handful of settings every backend genuinely shares, and you
should not need to touch `src/main.c` at all.

`is_on` should be cheap and free of side effects. Usually it just
returns a flag your own `parse` set. `warp_size` is optional and
answers 32 if you leave it out. The IR asks for it while lowering
`warpSize`, long before your backend has built anything, and having it
answer a plain number is what keeps target-specific enums from leaking
back into the frontend.

Return codes are `be_ret_t`. `BE_OK` for success, something negative
for failure, and `BE_UNSUP` when an op deliberately does nothing yet.
The driver treats `BE_UNSUP` as a real error rather than quiet success,
which is on purpose. A half-finished backend that silently writes an
empty file is worse than one that says it cannot do this yet.

## Feature bits

```
BE_F_SIMT BE_F_SCALAR BE_F_ATOMIC BE_F_SHARED
BE_F_WARP BE_F_MFMA BE_F_BARRIER BE_F_DIV
BE_F_SCRATCH BE_F_TRANSC BE_F_F16 BE_F_F64
BE_F_BF16 BE_F_MULTIOUT BE_F_NOCALL
```

Only claim what you have actually implemented. These are not
aspirations, they are what the conformance suite will key its tests off
once it lands, so a backend that claims `BE_F_MFMA` without emitting
`mfma` will simply fail.

Two of them do something today rather than just describing you.
`BE_F_NOCALL` says your emitted code has no calling convention for
`__device__` functions, so the driver inlines them before selection
ever runs. `BE_F_MULTIOUT` says you write more than one file, which
Tensix does.

## Roughly how it goes

1. `cp -r src/backend/skeleton src/backend/mygpu`
2. Rename `skel_` to your own prefix.
3. Add the extern and the list entry in `src/backend/backends.c`.
4. Add your file to `SOURCES` in the Makefile.
5. Put your flags in the descriptor and set them in `parse`. The driver
routes them to you.
6. Document those flags in `usage()`. The test suite checks that every
flag a backend declares turns up in `--help`, so this one is not
optional.
7. Get `isel` working, then `emit`, and add feature bits as things
start actually working rather than up front.

## Existing backends, by shape

Whichever of these is closest to your target is probably the best thing
to read first.

- `src/nvidia/nv_be.c` is the simplest complete one, isel and emit and
nothing else.
- `src/amdgpu/amd_be.c` is the full pipeline with scheduling, register
allocation and two verify passes.
- `src/tensix/tensix_be.c` emits several files from one path stem.
- `src/cpu/cpu_be.c` is the scalar, stack-everything shape.
- `src/intel/intel_be.c` is what a backend looks like while it is still
being built.

## Things the driver assumes

- Every registered backend has a `name`, `is_on`, `isel` and `emit`.
- `is_on` has no side effects.
- `isel` allocates the module and `mfree` frees it, or `mfree` is NULL
because you never allocated anything.
- An op that is not implemented returns `BE_UNSUP` rather than
pretending it worked.

The usual coding rules in [CONTRIBUTING.md](../CONTRIBUTING.md) apply
here too.
Loading
Loading