diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index b738127..0b3f00c 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -63,11 +63,12 @@ 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' \ @@ -75,7 +76,7 @@ jobs: 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 095b46f..8173151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 30994a1..c9b9482 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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 \ @@ -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 \ @@ -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 diff --git a/docs/backends.md b/docs/backends.md new file mode 100644 index 0000000..c251519 --- /dev/null +++ b/docs/backends.md @@ -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. diff --git a/src/amdgpu/amd_be.c b/src/amdgpu/amd_be.c new file mode 100644 index 0000000..05e1134 --- /dev/null +++ b/src/amdgpu/amd_be.c @@ -0,0 +1,195 @@ +/* amd_be.c -- AMDGPU as a be_desc_t. Full pipeline: isel, sched, + * regalloc, verify twice, emit. What a mature backend looks like at + * the descriptor layer; the actual work is in the rest of src/amdgpu/. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "amdgpu.h" +#include "sched.h" +#include "verify.h" +#include "barracuda.h" +#include +#include +#include + +typedef struct { + int on; /* --amdgpu, assembly to stdout or -o */ + int bin; /* --amdgpu-bin, .hsaco */ + int snap; + int set; /* a --gfx* was given, so defaults are out */ + amd_target_t target; + uint32_t elfm; + const char *chip; +} amd_opts_t; + +/* The part list, one row per chip rather than nineteen else-ifs. elfm is + * EF_AMDGPU_MACH for the exact part; target is the ISA family the rest of + * the backend switches on, so several chips share one. */ +static const struct { + const char *flag; + amd_target_t target; + uint32_t elfm; + const char *chip; +} amd_parts[] = { + { "--gfx90a", AMD_TARGET_GFX90A, 0x3F, "gfx90a" }, /* CDNA 2 */ + { "--gfx942", AMD_TARGET_GFX942, 0x54C, "gfx942" }, /* CDNA 3 */ + { "--gfx1030", AMD_TARGET_GFX1030, 0x36, "gfx1030" }, /* RDNA 2 */ + { "--gfx1031", AMD_TARGET_GFX1030, 0x37, "gfx1031" }, + { "--gfx1032", AMD_TARGET_GFX1030, 0x38, "gfx1032" }, + { "--gfx1033", AMD_TARGET_GFX1030, 0x39, "gfx1033" }, + { "--gfx1034", AMD_TARGET_GFX1030, 0x3e, "gfx1034" }, + { "--gfx1035", AMD_TARGET_GFX1030, 0x3d, "gfx1035" }, + { "--gfx1036", AMD_TARGET_GFX1030, 0x45, "gfx1036" }, + { "--gfx1100", AMD_TARGET_GFX1100, 0x41, "gfx1100" }, /* RDNA 3 */ + { "--gfx1101", AMD_TARGET_GFX1100, 0x46, "gfx1101" }, + { "--gfx1102", AMD_TARGET_GFX1100, 0x47, "gfx1102" }, + { "--gfx1103", AMD_TARGET_GFX1100, 0x44, "gfx1103" }, + { "--gfx1150", AMD_TARGET_GFX1100, 0x43, "gfx1150" }, /* RDNA 3.5 */ + { "--gfx1151", AMD_TARGET_GFX1100, 0x4a, "gfx1151" }, + { "--gfx1152", AMD_TARGET_GFX1100, 0x55, "gfx1152" }, + { "--gfx1153", AMD_TARGET_GFX1100, 0x58, "gfx1153" }, + { "--gfx1200", AMD_TARGET_GFX1200, 0x48, "gfx1200" }, /* RDNA 4 */ + { "--gfx1201", AMD_TARGET_GFX1200, 0x4e, "gfx1201" }, +}; + +#define AMD_NPARTS (sizeof amd_parts / sizeof amd_parts[0]) + +static const char *const amd_flags[] = { + "--amdgpu", "--amdgpu-bin", "--snap", + "--no-graphcolor", "--ssa-ra", "--max-vgprs", + "--gfx90a", "--gfx942", + "--gfx1030", "--gfx1031", "--gfx1032", "--gfx1033", + "--gfx1034", "--gfx1035", "--gfx1036", + "--gfx1100", "--gfx1101", "--gfx1102", "--gfx1103", + "--gfx1150", "--gfx1151", "--gfx1152", "--gfx1153", + "--gfx1200", "--gfx1201", + NULL +}; + +static int amd_parse(const char *arg, const char *next, void *o) +{ + amd_opts_t *p = (amd_opts_t *)o; + + if (strcmp(arg, "--amdgpu") == 0) { p->on = 1; return 0; } + if (strcmp(arg, "--amdgpu-bin") == 0) { p->bin = 1; return 0; } + if (strcmp(arg, "--snap") == 0) { p->snap = 1; return 0; } + + /* Register allocator knobs live as globals in the allocator itself, + * so this is the backend setting its own state rather than main + * reaching across for it. */ + if (strcmp(arg, "--no-graphcolor") == 0) { amd_ra_lin = 1; return 0; } + if (strcmp(arg, "--ssa-ra") == 0) { amd_ra_ssa = 1; return 0; } + if (strcmp(arg, "--max-vgprs") == 0) { + if (next == NULL) { + fprintf(stderr, "error: --max-vgprs wants a count\n"); + return -1; + } + amd_max_vgpr = atoi(next); + return 1; + } + + for (uint32_t i = 0; i < AMD_NPARTS; i++) { + if (strcmp(arg, amd_parts[i].flag) != 0) continue; + p->target = amd_parts[i].target; + p->elfm = amd_parts[i].elfm; + p->chip = amd_parts[i].chip; + p->set = 1; + return 0; + } + return 0; +} + +static int amd_on(const void *o) +{ + const amd_opts_t *p = (const amd_opts_t *)o; + return p->on || p->bin; +} + +static uint32_t amd_warp(const void *o) +{ + const amd_opts_t *p = (const amd_opts_t *)o; + return (uint32_t)amd_get_wave_size(p->set ? p->target : AMD_TARGET_GFX1100); +} + +static int amd_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + const amd_opts_t *p = (const amd_opts_t *)o; + (void)cfg; + amd_module_t *amd = calloc(1, sizeof(*amd)); + if (amd == NULL) return BE_ENOMEM; + + /* RDNA 3 unless a part was named. */ + amd->target = p->set ? p->target : AMD_TARGET_GFX1100; + amd->elf_mach = p->set ? p->elfm : 0x41u; + amd->snap_mode = (uint8_t)p->snap; + snprintf(amd->chip_name, sizeof(amd->chip_name), "%s", + p->set && p->chip ? p->chip : "gfx1100"); + + if (amdgpu_compile((const bir_module_t *)M, amd) != BC_OK) { + free(amd); + return BE_EISEL; + } + *out_mmod = amd; + return BE_OK; +} + +static int amd_sched(void *mmod) { amdgpu_sched(mmod); return BE_OK; } +static int amd_regalc(void *mmod) { amdgpu_regalloc(mmod); return BE_OK; } + +static int amd_verify(const void *mmod, int phase) +{ + int p = (phase == BE_VFY_ISEL) ? VFY_ISEL : VFY_RA; + vfy_res_t v = bc_vfy(mmod, p); + if (v.errs == 0) return BE_OK; + fprintf(stderr, "verify: %u error(s) after %s\n", + v.errs, p == VFY_ISEL ? "isel" : "regalloc"); + return BE_EVFY; +} + +static int amd_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + const amd_opts_t *p = (const amd_opts_t *)o; + (void)cfg; + + /* --amdgpu-bin writes a .hsaco; bare --amdgpu writes assembly, to the + * file if one was named and stdout otherwise. */ + if (p->bin) { + int rc = amdgpu_emit_elf((amd_module_t *)mmod, out ? out : "a.hsaco"); + return rc == 0 ? BE_OK : BE_EEMIT; + } + + if (out != NULL) { + FILE *af = fopen(out, "w"); + if (af == NULL) { + fprintf(stderr, "error: cannot open %s for writing\n", out); + return BE_EIO; + } + amdgpu_emit_asm(mmod, af); + fclose(af); + return BE_OK; + } + + amdgpu_emit_asm(mmod, stdout); + return BE_OK; +} + +const be_desc_t be_amd = { + .name = "amdgpu", + .triple = "amdgcn--", + .feats = BE_F_SIMT | BE_F_ATOMIC | BE_F_SHARED | BE_F_WARP + | BE_F_BARRIER | BE_F_DIV | BE_F_SCRATCH | BE_F_TRANSC + | BE_F_F16 | BE_F_F64 | BE_F_MFMA | BE_F_NOCALL, + .opts_size = sizeof(amd_opts_t), + .flags = amd_flags, + .parse = amd_parse, + .is_on = amd_on, + .warp_size = amd_warp, + .isel = amd_isel, + .sched = amd_sched, + .regalc = amd_regalc, + .verify = amd_verify, + .emit = amd_emit, + .mfree = free +}; diff --git a/src/backend/backend.h b/src/backend/backend.h new file mode 100644 index 0000000..ac9d901 --- /dev/null +++ b/src/backend/backend.h @@ -0,0 +1,138 @@ +/* backend.h -- the shape every Booth backend implements. + * Seven function pointers, one struct, no dlopen, no frameworks. + * See docs/backends.md for the walkthrough. */ + +#ifndef BOOTH_BE_H +#define BOOTH_BE_H + +#include + +struct bir_module; +struct be_cfg; + +/* ---- Return codes ---- */ + +typedef enum { + BE_OK = 0, + BE_UNSUP = -1, /* op deliberately not implemented */ + BE_EISEL = -2, + BE_ESCHED = -3, + BE_ERA = -4, + BE_EVFY = -5, + BE_EEMIT = -6, + BE_EIO = -7, + BE_EINPUT = -8, + BE_ENOMEM = -9 +} be_ret_t; + +/* ---- Verify phase ---- + * Only the two the driver actually calls. Phases for post-sched and + * post-emit were declared here first, but nothing invoked them and the + * AMD adapter folds every non-ISEL phase onto its post-regalloc check, + * so a backend implementing them would have been checking physical + * registers on a module that still had virtual ones. Add them back with + * the call sites, not before. */ + +typedef enum { + BE_VFY_ISEL, + BE_VFY_RA +} be_vfy_t; + +/* ---- Feature flags ---- + * A backend must not claim a feature it does not implement. The + * conformance suite (not yet landed) keys tests off these bits. */ + +#define BE_F_SIMT (1u << 0) +#define BE_F_SCALAR (1u << 1) +#define BE_F_ATOMIC (1u << 2) +#define BE_F_SHARED (1u << 3) +#define BE_F_WARP (1u << 4) +#define BE_F_MFMA (1u << 5) +#define BE_F_BARRIER (1u << 6) +#define BE_F_DIV (1u << 7) +#define BE_F_SCRATCH (1u << 8) +#define BE_F_TRANSC (1u << 9) +#define BE_F_F16 (1u << 10) +#define BE_F_F64 (1u << 11) +#define BE_F_BF16 (1u << 12) +#define BE_F_MULTIOUT (1u << 13) /* emits more than one file (Tensix) */ +/* No real calls in emitted code, so device functions have to be inlined + * before isel. The vector targets want this; CPU, RV64 and Metal emit + * calls and are left alone. */ +#define BE_F_NOCALL (1u << 14) + +/* ---- Options storage ---- + * Each backend gets one fixed slot for whatever its flags set. Sized at + * compile time and union-aligned, so no malloc and no alignment guessing + * when a backend casts the slot to its own struct. A descriptor asking + * for more than BE_OPTS_MAX is refused at startup rather than trusted. */ + +#define BE_OPTS_MAX 128 + +typedef union { + unsigned char raw[BE_OPTS_MAX]; + void *p; + long long l; + double d; +} be_opts_t; + +/* ---- Backend descriptor ---- + * NULL op means "not applicable, driver skips". isel and emit are + * required; a registered backend without them is a bug. cfg is opaque + * here so the contract does not couple to driver internals; + * implementations cast to be_cfg_t. opts is the backend's own slot, + * which only it knows the shape of. */ + +typedef struct be_desc { + const char *name; + const char *triple; + uint32_t feats; + uint32_t opts_size; /* bytes wanted, <= BE_OPTS_MAX */ + + /* Flags this backend owns, NULL-terminated. Declared rather than + * discovered, so two backends claiming one flag is a static error + * the suite catches instead of a first-past-the-post race. */ + const char *const *flags; + + /* Called for an argv entry matching one of `flags`. `next` is the + * following entry or NULL at the end. Returns how many extra entries + * were consumed (0 or 1), or negative if the value was bad. */ + int (*parse) (const char *arg, const char *next, void *opts); + + int (*is_on) (const void *opts); + + /* Lanes that move together on this target. The IR needs it while + * lowering warpSize, long before a backend module exists, and asking + * for a number here is what keeps AMD types out of the frontend. + * NULL means the 32 the driver assumes. */ + uint32_t (*warp_size)(const void *opts); + int (*isel) (const struct bir_module *M, const struct be_cfg *cfg, + const void *opts, void **out_mmod); + int (*sched) (void *mmod); + int (*regalc) (void *mmod); + int (*verify) (const void *mmod, int phase); + int (*emit) (const void *mmod, const struct be_cfg *cfg, + const void *opts, const char *out_path); + void (*mfree) (void *mmod); +} be_desc_t; + +/* ---- Registration ---- */ + +extern const be_desc_t * const be_list[]; /* NULL-terminated */ + +const be_desc_t *be_find(const char *name); + +/* Offer one argv entry to the registry. Returns 1 if a backend took it, + * 0 if no backend owns it, negative on a bad value. On 1, *used_next says + * whether the following argv entry was consumed as a value. */ +int be_parse_flag(const char *arg, const char *next, int *used_next); + +/* How many backends the flags turned on, and the first of them. The + * driver asks rather than keeping its own copy of every mode flag. */ +uint32_t be_num_on(void); +uint32_t be_warp_size(void); +const be_desc_t *be_active(void); + +int be_run(const struct bir_module *M, const struct be_cfg *cfg); + +#endif diff --git a/src/backend/backend_cfg.h b/src/backend/backend_cfg.h new file mode 100644 index 0000000..6999a15 --- /dev/null +++ b/src/backend/backend_cfg.h @@ -0,0 +1,15 @@ +/* backend_cfg.h -- the settings every backend shares, and nothing else. + * Target selection and per-target knobs live in the backend that owns + * them, reached through be_desc_t.parse, so this no longer needs to + * include a single backend header. */ + +#ifndef BOOTH_BE_CFG_H +#define BOOTH_BE_CFG_H + +typedef struct be_cfg { + int no_mem2reg, no_cfold, no_dce, no_sched, no_sroa; + int mode_ir, mode_tdf, mode_tdf_fission; + const char *output_file; +} be_cfg_t; + +#endif diff --git a/src/backend/backends.c b/src/backend/backends.c new file mode 100644 index 0000000..84271ed --- /dev/null +++ b/src/backend/backends.c @@ -0,0 +1,191 @@ +/* backends.c -- registration list and dispatch loop. + * Adding a target is one line in be_list and one _be.c file. + * Static, ordered, auditable. Boring in the way infra should be. */ + +#include "backend.h" +#include "backend_cfg.h" +#include +#include + +/* Bounded so a stray zero terminator can't wander off the list. */ +#define BE_MAX 64 + +extern const be_desc_t be_amd; +extern const be_desc_t be_ptx; +extern const be_desc_t be_tsx; +extern const be_desc_t be_rvcore; +extern const be_desc_t be_x86; +extern const be_desc_t be_rv64; +extern const be_desc_t be_metal; +extern const be_desc_t be_intel; + +#ifdef BOOTH_INCLUDE_SKELETON +extern const be_desc_t be_skel; +#endif + +const be_desc_t * const be_list[] = { + &be_amd, + &be_ptx, + &be_tsx, + &be_rvcore, + &be_metal, + &be_intel, + &be_x86, + &be_rv64, +#ifdef BOOTH_INCLUDE_SKELETON + &be_skel, +#endif + NULL +}; + +/* One slot per registered backend, indexed the same as be_list. Static + * because a compiler run has exactly one of each and JPL style keeps + * malloc out of the driver. */ +static be_opts_t be_opts_store[BE_MAX]; + +int be_parse_flag(const char *arg, const char *next, int *used_next) +{ + if (arg == NULL || used_next == NULL) return 0; + *used_next = 0; + + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) { + const be_desc_t *b = be_list[i]; + if (b->flags == NULL || b->parse == NULL) continue; + + for (uint32_t f = 0; b->flags[f] != NULL; f++) { + if (strcmp(arg, b->flags[f]) != 0) continue; + + /* A descriptor wanting more room than the slot holds would + * scribble over its neighbour, so refuse rather than trust it. */ + if (b->opts_size > BE_OPTS_MAX) { + fprintf(stderr, "backend %s: opts_size %u exceeds %u\n", + b->name, b->opts_size, (uint32_t)BE_OPTS_MAX); + return -1; + } + + int n = b->parse(arg, next, be_opts_store[i].raw); + if (n < 0) return -1; + *used_next = n; + return 1; + } + } + return 0; +} + +uint32_t be_num_on(void) +{ + uint32_t n = 0; + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) + if (be_list[i]->is_on != NULL && be_list[i]->is_on(be_opts_store[i].raw)) + n++; + return n; +} + +const be_desc_t *be_active(void) +{ + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) + if (be_list[i]->is_on != NULL && be_list[i]->is_on(be_opts_store[i].raw)) + return be_list[i]; + return NULL; +} + +uint32_t be_warp_size(void) +{ + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) { + const be_desc_t *b = be_list[i]; + if (b->is_on == NULL || !b->is_on(be_opts_store[i].raw)) continue; + if (b->warp_size == NULL) break; + return b->warp_size(be_opts_store[i].raw); + } + return 32u; +} + +const be_desc_t *be_find(const char *name) +{ + if (name == NULL) return NULL; + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) { + if (be_list[i]->name != NULL && + strcmp(be_list[i]->name, name) == 0) { + return be_list[i]; + } + } + return NULL; +} + +int be_run(const struct bir_module *M, const be_cfg_t *cfg) +{ + if (M == NULL || cfg == NULL) return BE_EINPUT; + + /* One target per run. They all emit to cfg->output_file, so asking for + * several used to write each over the last and leave whichever sorted + * last in be_list, under the name you picked, with a zero exit. Run + * kath once per target instead. */ + uint32_t on = be_num_on(); + + if (on > 1) { + fprintf(stderr, "error: %u backends selected, pick one:", on); + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) + if (be_list[i]->is_on != NULL && + be_list[i]->is_on(be_opts_store[i].raw)) + fprintf(stderr, " %s", be_list[i]->name); + fprintf(stderr, "\n"); + return BE_EINPUT; + } + + int first = BE_OK; + + for (uint32_t i = 0; i < BE_MAX && be_list[i] != NULL; i++) { + const be_desc_t *b = be_list[i]; + const void *opts = be_opts_store[i].raw; + + if (b->is_on == NULL || !b->is_on(opts)) continue; + + /* A registered backend without isel or emit is a descriptor + * bug, not a runtime condition; complain and skip. */ + if (b->isel == NULL || b->emit == NULL) { + fprintf(stderr, "backend %s: missing required op\n", b->name); + if (first == BE_OK) first = BE_UNSUP; + continue; + } + + void *mmod = NULL; + int rc = b->isel(M, cfg, opts, &mmod); + if (rc != BE_OK) { + if (first == BE_OK) first = rc; + if (mmod != NULL && b->mfree != NULL) b->mfree(mmod); + continue; + } + + if (b->verify != NULL) { + rc = b->verify(mmod, BE_VFY_ISEL); + if (rc != BE_OK) goto fail; + } + + if (b->sched != NULL && !cfg->no_sched) { + rc = b->sched(mmod); + if (rc != BE_OK) goto fail; + } + + if (b->regalc != NULL) { + rc = b->regalc(mmod); + if (rc != BE_OK) goto fail; + } + + if (b->verify != NULL) { + rc = b->verify(mmod, BE_VFY_RA); + if (rc != BE_OK) goto fail; + } + + rc = b->emit(mmod, cfg, opts, cfg->output_file); + if (rc != BE_OK && first == BE_OK) first = rc; + + if (b->mfree != NULL) b->mfree(mmod); + continue; + +fail: + if (first == BE_OK) first = rc; + if (b->mfree != NULL) b->mfree(mmod); + } + + return first; +} diff --git a/src/backend/skeleton/skeleton.c b/src/backend/skeleton/skeleton.c new file mode 100644 index 0000000..4f67e6d --- /dev/null +++ b/src/backend/skeleton/skeleton.c @@ -0,0 +1,63 @@ +/* skeleton.c -- copy-paste starting point for a new target. + * Every op returns BE_UNSUP; the driver bails cleanly until you + * implement one. Rename skel_ to your prefix, register in + * backends.c, fill in ops. Godspeed. Compiled only under + * -DBOOTH_INCLUDE_SKELETON so release builds don't ship it. */ + +#ifdef BOOTH_INCLUDE_SKELETON + +#include "backend.h" +#include "backend_cfg.h" +#include + +/* Whatever your flags set. The driver keeps one of these per backend + * and hands it back to every op, so nothing target-specific has to go + * anywhere near be_cfg_t. */ +typedef struct { + int on; +} skel_opts_t; + +/* The flags you answer for. The suite checks no two backends claim the + * same one, and that each appears in usage(). */ +static const char *const skel_flags[] = { "--skeleton", NULL }; + +static int skel_parse(const char *arg, const char *next, void *o) +{ + (void)next; /* return 1 instead if you consume the following entry */ + if (strcmp(arg, "--skeleton") == 0) { ((skel_opts_t *)o)->on = 1; } + return 0; +} + +static int skel_on(const void *o) { return ((const skel_opts_t *)o)->on; } + +static int skel_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)M; (void)cfg; (void)o; (void)out_mmod; + return BE_UNSUP; +} + +static int skel_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out_path) +{ + (void)mmod; (void)cfg; (void)o; (void)out_path; + return BE_UNSUP; +} + +const be_desc_t be_skel = { + .name = "skeleton", + .triple = NULL, + .feats = 0, + .opts_size = sizeof(skel_opts_t), + .flags = skel_flags, + .parse = skel_parse, + .is_on = skel_on, + .isel = skel_isel, + .sched = NULL, + .regalc = NULL, + .verify = NULL, + .emit = skel_emit, + .mfree = NULL +}; + +#endif diff --git a/src/cpu/cpu_be.c b/src/cpu/cpu_be.c new file mode 100644 index 0000000..b93122c --- /dev/null +++ b/src/cpu/cpu_be.c @@ -0,0 +1,127 @@ +/* cpu_be.c -- x86-64 and RV64 as be_desc_t descriptors. + * The reason a Triton kernel runs on a laptop with no GPU in it. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "cpu.h" +#include "rv64.h" +#include "barracuda.h" +#include +#include +#include + +/* ---- x86-64 ---- */ + +typedef struct { int on; } cpu_opts_t; + +static const char *const x86_flags[] = { "--cpu", NULL }; + +static int x86_parse(const char *arg, const char *next, void *o) +{ + (void)next; + if (strcmp(arg, "--cpu") == 0) { ((cpu_opts_t *)o)->on = 1; } + return 0; +} + +static int x86_on(const void *o) { return ((const cpu_opts_t *)o)->on; } + +static int x86_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)cfg; (void)o; + const bir_module_t *bir = (const bir_module_t *)M; + if (bir->num_funcs == 0u) { + fprintf(stderr, "error: no functions\n"); + return BE_EINPUT; + } + cpu_mod_t *cm = calloc(1, sizeof(*cm)); + if (cm == NULL) return BE_ENOMEM; + cpu_init(cm, bir); + if (cpu_emit(cm) != 0) { free(cm); return BE_EISEL; } + *out_mmod = cm; + return BE_OK; +} + +static int x86_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + const cpu_mod_t *cm = mmod; + const char *p = out ? out : "a.o"; + if (cpu_elf(cm, p) != 0) { + fprintf(stderr, "cpu: elf write failed\n"); + return BE_EIO; + } + fprintf(stderr, "wrote %s (%u bytes x86-64)\n", p, cm->codelen); + return BE_OK; +} + +const be_desc_t be_x86 = { + .name = "cpu-x86-64", + .triple = "x86_64-unknown-none", + .feats = BE_F_SCALAR | BE_F_TRANSC | BE_F_F64, + .opts_size = sizeof(cpu_opts_t), + .flags = x86_flags, + .parse = x86_parse, + .is_on = x86_on, + .isel = x86_isel, + .emit = x86_emit, + .mfree = free +}; + +/* ---- RV64 ---- */ + +static const char *const rv_flags[] = { "--rv64", NULL }; + +static int rv_parse(const char *arg, const char *next, void *o) +{ + (void)next; + if (strcmp(arg, "--rv64") == 0) { ((cpu_opts_t *)o)->on = 1; } + return 0; +} + +static int rv_on(const void *o) { return ((const cpu_opts_t *)o)->on; } + +static int rv_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)cfg; (void)o; + const bir_module_t *bir = (const bir_module_t *)M; + if (bir->num_funcs == 0u) { + fprintf(stderr, "error: no functions\n"); + return BE_EINPUT; + } + rv64_mod_t *vm = calloc(1, sizeof(*vm)); + if (vm == NULL) return BE_ENOMEM; + rv64_init(vm, bir); + if (rv64_emit(vm) != 0) { free(vm); return BE_EISEL; } + *out_mmod = vm; + return BE_OK; +} + +static int rv_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + const rv64_mod_t *vm = mmod; + const char *p = out ? out : "a.o"; + if (rv64_elf(vm, p) != 0) { + fprintf(stderr, "rv64: elf write failed\n"); + return BE_EIO; + } + fprintf(stderr, "wrote %s (%u bytes RV64)\n", p, vm->codelen); + return BE_OK; +} + +const be_desc_t be_rv64 = { + .name = "cpu-rv64", + .triple = "riscv64-unknown-none", + .feats = BE_F_SCALAR | BE_F_TRANSC | BE_F_F64, + .opts_size = sizeof(cpu_opts_t), + .flags = rv_flags, + .parse = rv_parse, + .is_on = rv_on, + .isel = rv_isel, + .emit = rv_emit, + .mfree = free +}; diff --git a/src/fe/sema.c b/src/fe/sema.c index aa218ec..baed33f 100644 --- a/src/fe/sema.c +++ b/src/fe/sema.c @@ -1,5 +1,4 @@ #include "sema.h" -#include "../amd_target_defs.h" #include #include #include @@ -1590,13 +1589,13 @@ static void check_func_def(sema_ctx_t *S, uint32_t node) /* ---- Initialization ---- */ -void sema_init(sema_ctx_t *S, const parser_t *P, uint32_t root, int amd_target) +void sema_init(sema_ctx_t *S, const parser_t *P, uint32_t root, int warp_size) { (void)root; memset(S, 0, sizeof(*S)); S->P = P; S->src = P->src; - S->amd_target = amd_target; + S->warp_size = warp_size; st_void(S); /* 0 */ st_bool(S); /* 1 */ diff --git a/src/fe/sema.h b/src/fe/sema.h index 65beaab..227dbe2 100644 --- a/src/fe/sema.h +++ b/src/fe/sema.h @@ -120,7 +120,7 @@ typedef struct { uint32_t cur_ret_type; /* Target setting for wavefront size */ - int amd_target; + int warp_size; /* lanes per warp on the chosen target */ /* Errors */ bc_error_t errors[SEMA_MAX_ERRORS]; @@ -129,7 +129,7 @@ typedef struct { /* ---- Public API ---- */ -void sema_init(sema_ctx_t *S, const parser_t *P, uint32_t root, int amd_target); +void sema_init(sema_ctx_t *S, const parser_t *P, uint32_t root, int warp_size); int sema_check(sema_ctx_t *S, uint32_t root); void sema_dump(const sema_ctx_t *S, uint32_t root); diff --git a/src/intel/intel_be.c b/src/intel/intel_be.c new file mode 100644 index 0000000..53dcd7a --- /dev/null +++ b/src/intel/intel_be.c @@ -0,0 +1,72 @@ +/* intel_be.c -- Intel Arc SPIR-V as a be_desc_t. + * Even more stub than Metal; intel_compile prints an honest "not yet + * a working compiler" so nobody thinks they got a real .spv. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "intel.h" +#include "barracuda.h" +#include +#include +#include + +typedef struct { + int on; + intel_target_t target; +} in_opts_t; + +static const char *const in_flags[] = { + "--intel-spirv", "--xe-lpg", "--xe-hpg", "--xe-hpc", "--xe2", NULL +}; + +static int in_parse(const char *arg, const char *next, void *o) +{ + in_opts_t *p = (in_opts_t *)o; + (void)next; + if (strcmp(arg, "--intel-spirv") == 0) { p->on = 1; return 0; } + if (strcmp(arg, "--xe-lpg") == 0) { p->target = INTEL_TARGET_XE_LPG; return 0; } + if (strcmp(arg, "--xe-hpg") == 0) { p->target = INTEL_TARGET_XE_HPG; return 0; } + if (strcmp(arg, "--xe-hpc") == 0) { p->target = INTEL_TARGET_XE_HPC; return 0; } + if (strcmp(arg, "--xe2") == 0) { p->target = INTEL_TARGET_XE2; return 0; } + return 0; +} + +static int in_on(const void *o) { return ((const in_opts_t *)o)->on; } + +static int in_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + const in_opts_t *p = (const in_opts_t *)o; + (void)cfg; + intel_module_t *im = calloc(1, sizeof(*im)); + if (im == NULL) return BE_ENOMEM; + if (intel_compile((const bir_module_t *)M, im, p->target) != BC_OK) { + fprintf(stderr, "error: Intel SPIR-V backend not yet a working compiler\n"); + free(im); + return BE_EISEL; + } + *out_mmod = im; + return BE_OK; +} + +static int in_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + return intel_emit_spirv((const intel_module_t *)mmod, + out ? out : "a.spv") == BC_OK + ? BE_OK : BE_EEMIT; +} + +const be_desc_t be_intel = { + .name = "intel-spirv", + .triple = NULL, + .feats = BE_F_SIMT | BE_F_SHARED | BE_F_BARRIER, + .opts_size = sizeof(in_opts_t), + .flags = in_flags, + .parse = in_parse, + .is_on = in_on, + .isel = in_isel, + .emit = in_emit, + .mfree = free +}; diff --git a/src/ir/bir_lower.c b/src/ir/bir_lower.c index 8e8ba51..fba9b64 100644 --- a/src/ir/bir_lower.c +++ b/src/ir/bir_lower.c @@ -1,5 +1,4 @@ #include "bir_lower.h" -#include "../amd_target_defs.h" #include #include #include @@ -789,7 +788,7 @@ static uint32_t lower_expr(lower_t *L, uint32_t node) /* Builtin constant: warpSize (HIP) */ if (strcmp(name, "warpSize") == 0 && L->sema) { - int wave_size = amd_get_wave_size(L->sema->amd_target); + int wave_size = L->sema->warp_size; uint32_t t = bir_type_int(L->M, 32); return BIR_MAKE_CONST(bir_const_int(L->M, t, wave_size)); } diff --git a/src/main.c b/src/main.c index 70a5f3b..31dab4d 100644 --- a/src/main.c +++ b/src/main.c @@ -23,6 +23,8 @@ #include "rv_isel.h" #include "cpu.h" #include "rv64.h" +#include "backend.h" +#include "backend_cfg.h" #include static char source_buf[BC_MAX_SOURCE]; @@ -38,19 +40,11 @@ static bir_module_t *bir_module; /* heap-allocated (~11 MB) */ * path can both call it without duplicating two hundred lines of * backend wiring. */ -typedef struct { - int no_mem2reg, no_cfold, no_dce, no_sched, no_sroa; - int mode_ir, mode_tdf, mode_tdf_fission; - int mode_amdgpu, mode_amdgpu_bin; - int mode_tensix, mode_nvidia, nv_bkhit; - int mode_metal, mode_intel, mode_rv_elf, mode_cpu, mode_rv64; - amd_target_t amd_target; - uint32_t amd_elfm; - const char *amd_chip; - int snap_mode; - intel_target_t intel_target; - const char *output_file; -} backend_cfg_t; +/* be_cfg_t definition lives in src/backend/backend_cfg.h so every + * backend descriptor sees the same field layout. Alias kept for the + * few local uses below that predate the move; new code should use + * be_cfg_t directly. */ +typedef be_cfg_t backend_cfg_t; /* TDF module and lowering scratch live in BSS, not on the stack. * The struct is ~20 KB and trips -Wstack-usage hard if you put it @@ -66,8 +60,9 @@ static int target_for_cfg(const backend_cfg_t *cfg) * for the dump label and for when Tensix fission starts doing * something interesting on its own branch. */ if (cfg->mode_tdf_fission) return TD_TGT_TENSIX; - if (cfg->mode_tensix) return TD_TGT_TENSIX; - if (cfg->mode_nvidia) return TD_TGT_NVIDIA; + const be_desc_t *b = be_active(); + if (b != NULL && strcmp(b->name, "tensix") == 0) return TD_TGT_TENSIX; + if (b != NULL && strcmp(b->name, "nvptx") == 0) return TD_TGT_NVIDIA; return TD_TGT_AMD; } @@ -101,10 +96,12 @@ static int run_bir_backends(bir_module_t *bir, const backend_cfg_t *cfg) * so splice the callee bodies in before anything else runs. mem2reg then * cleans up the inlined parameter stores as if they were always local. * CPU, RV64 and Metal emit real calls and are left untouched. */ - if (cfg->mode_amdgpu || cfg->mode_amdgpu_bin || - cfg->mode_nvidia || cfg->mode_tensix) { - int irc = bir_inline_device(bir); - if (irc != BC_OK) return irc; + { + const be_desc_t *b = be_active(); + if (b != NULL && (b->feats & BE_F_NOCALL)) { + int irc = bir_inline_device(bir); + if (irc != BC_OK) return irc; + } } /* Optimisation passes: same shape regardless of frontend. */ @@ -164,224 +161,8 @@ static int run_bir_backends(bir_module_t *bir, const backend_cfg_t *cfg) bir->num_funcs, bir->num_globals, bir->num_insts); } - if (cfg->mode_amdgpu || cfg->mode_amdgpu_bin) { - amd_module_t *amd = (amd_module_t *)malloc(sizeof(amd_module_t)); - if (!amd) { - fprintf(stderr, "error: failed to allocate AMD module\n"); - return BC_ERR_IO; - } - amd->target = cfg->amd_target; - amd->elf_mach = cfg->amd_elfm; - amd->snap_mode = (uint8_t)cfg->snap_mode; - snprintf(amd->chip_name, sizeof(amd->chip_name), "%s", cfg->amd_chip); - int arc = amdgpu_compile(bir, amd); - if (arc == BC_OK) { - vfy_res_t v1 = bc_vfy(amd, VFY_ISEL); - if (v1.errs) { - fprintf(stderr, "verify: %u error(s) after isel\n", v1.errs); - arc = BC_ERR_VERIFY; - } - } - if (arc == BC_OK) { - if (!cfg->no_sched) amdgpu_sched(amd); - amdgpu_regalloc(amd); - vfy_res_t v2 = bc_vfy(amd, VFY_RA); - if (v2.errs) { - fprintf(stderr, "verify: %u error(s) after regalloc\n", v2.errs); - arc = BC_ERR_VERIFY; - } - } - if (arc == BC_OK) { - if (cfg->mode_amdgpu_bin) - amdgpu_emit_elf(amd, - cfg->output_file ? cfg->output_file : "a.hsaco"); - else if (cfg->output_file) { - /* -o used to be silently ignored here, so a build system had - * no way to ask for the assembly other than capturing stdout. - * No -o still means stdout, which is what anyone piping it - * today already relies on. */ - FILE *af = fopen(cfg->output_file, "w"); - if (!af) { - fprintf(stderr, "error: cannot open %s for writing\n", - cfg->output_file); - rc = BC_ERR_IO; - } else { - amdgpu_emit_asm(amd, af); - fclose(af); - } - } else - amdgpu_emit_asm(amd, stdout); - } else { - if (arc != BC_ERR_VERIFY) - fprintf(stderr, "error: AMDGPU compilation failed\n"); - rc = arc; - } - free(amd); - } - - if (cfg->mode_nvidia) { - nv_module_t *nvm = (nv_module_t *)malloc(sizeof(nv_module_t)); - if (!nvm) { - fprintf(stderr, "error: failed to allocate NVIDIA module\n"); - return BC_ERR_IO; - } - int nrc = nv_compile(bir, nvm); - if (nrc == BC_OK) { - nvm->bkhit = (uint8_t)cfg->nv_bkhit; - nv_emit_ptx(nvm, cfg->output_file ? cfg->output_file : "a.ptx"); - } else { - fprintf(stderr, "error: NVIDIA PTX compilation failed\n"); - rc = nrc; - } - free(nvm); - } - - if (cfg->mode_metal) { - metal_module_t *mm = (metal_module_t *)malloc(sizeof(metal_module_t)); - if (!mm) { - fprintf(stderr, "error: failed to allocate Metal module\n"); - return BC_ERR_IO; - } - int mrc = metal_compile(bir, mm); - if (mrc == BC_OK) { - metal_emit_msl(mm, cfg->output_file ? cfg->output_file : "a.metal"); - } else { - fprintf(stderr, "error: Metal backend compilation failed\n"); - rc = mrc; - } - free(mm); - } - - if (cfg->mode_intel) { - intel_module_t *im = (intel_module_t *)malloc(sizeof(intel_module_t)); - if (!im) { - fprintf(stderr, "error: failed to allocate Intel module\n"); - return BC_ERR_IO; - } - int irc = intel_compile(bir, im, cfg->intel_target); - if (irc == BC_OK) { - intel_emit_spirv(im, cfg->output_file ? cfg->output_file : "a.spv"); - } else { - fprintf(stderr, - "error: Intel SPIR-V backend not yet a working compiler\n"); - rc = irc; - } - free(im); - } - - /* Native RV32IM emission for the baby cores. Picks the first - * function in the BIR module (which is the first CUDA kernel - * defined in the source) and runs it through the bring-up isel - * into an ELF that the tt-metal host loader can drop onto a - * baby core. Soft-float not yet linked in; integer kernels - * only for now. */ - if (cfg->mode_cpu) { - static cpu_mod_t cm; - if (bir->num_funcs == 0u) { fprintf(stderr,"error: no functions\n"); return BC_ERR_TDF; } - cpu_init(&cm, bir); - cpu_emit(&cm); - const char *path = cfg->output_file ? cfg->output_file : "a.o"; - if (cpu_elf(&cm, path) != 0) { fprintf(stderr,"cpu: elf write failed\n"); return BC_ERR_IO; } - fprintf(stderr, "wrote %s (%u bytes x86-64)\n", path, cm.codelen); - } - - if (cfg->mode_rv64) { - static rv64_mod_t vm; - if (bir->num_funcs == 0u) { fprintf(stderr,"error: no functions\n"); return BC_ERR_TDF; } - rv64_init(&vm, bir); - rv64_emit(&vm); - const char *path = cfg->output_file ? cfg->output_file : "a.o"; - if (rv64_elf(&vm, path) != 0) { fprintf(stderr,"rv64: elf write failed\n"); return BC_ERR_IO; } - fprintf(stderr, "wrote %s (%u bytes RV64)\n", path, vm.codelen); - } - - if (cfg->mode_rv_elf) { - static rv_buf_t rv_code; - rv_buf_init(&rv_code); - if (bir->num_funcs == 0u) { - fprintf(stderr, "error: BIR module has no functions\n"); - return BC_ERR_TDF; - } - /* Module, not just function 0: rv_isel_func records call patches but - * only rv_isel_module resolves them, so a BIR_CALL through this path - * would otherwise keep its placeholder and trap as an illegal insn. */ - int irc = rv_isel_module(bir, &rv_code); - if (irc != BC_OK) return irc; - const char *path = cfg->output_file ? cfg->output_file : "a.elf"; - int erc = rv_elf_write(&rv_code, path); - if (erc != BC_OK) return erc; - fprintf(stderr, "wrote %s (%u bytes code, %u instructions)\n", - path, rv_buf_nbytes(&rv_code), - rv_buf_n_words(&rv_code)); - } - - /* Tensix needs additional reader/writer/host emission besides - * the compute kernel, which is why it lives slightly off the - * shared shape. */ - if (cfg->mode_tensix) { - tt_module_t *ttm = (tt_module_t *)malloc(sizeof(tt_module_t)); - if (!ttm) { - fprintf(stderr, "error: failed to allocate Tensix module\n"); - return BC_ERR_IO; - } - int trc = tensix_compile(bir, ttm); - if (trc == BC_OK) { - tensix_coarsen(ttm); - tensix_regalloc(ttm); - const char *compute_path = - cfg->output_file ? cfg->output_file : "a_compute.cpp"; - tensix_analyze_datamov(bir, ttm, &ttm->dmov); - tensix_emit_metalium(ttm, compute_path); - /* Raw Tensix machine code alongside the Metalium C++: the encoded - * 32-bit word stream, decodable by ttas/Kahu. */ - { - char bin_path[BC_MAX_PATH]; - const char *st2 = strstr(compute_path, "_compute"); - int bp = st2 ? (int)(st2 - compute_path) - : (int)strlen(compute_path); - snprintf(bin_path, sizeof(bin_path), "%.*s_compute.bin", - bp, compute_path); - tensix_emit_binary(ttm, bin_path); - /* The math core's RISC-V .ttinsn stream that issues them. */ - snprintf(bin_path, sizeof(bin_path), "%.*s_compute.ttinsn", - bp, compute_path); - tensix_emit_ttinsn(ttm, bin_path); - } - char host_path[BC_MAX_PATH]; - char reader_path[BC_MAX_PATH]; - char writer_path[BC_MAX_PATH]; - const char *stem = strstr(compute_path, "_compute"); - int pfx; - if (stem) pfx = (int)(stem - compute_path); - else { - const char *dot = strrchr(compute_path, '.'); - pfx = dot ? (int)(dot - compute_path) - : (int)strlen(compute_path); - } - snprintf(host_path, sizeof(host_path), - "%.*s_host.cpp", pfx, compute_path); - snprintf(reader_path, sizeof(reader_path), - "%.*s_reader.cpp", pfx, compute_path); - snprintf(writer_path, sizeof(writer_path), - "%.*s_writer.cpp", pfx, compute_path); - tensix_emit_reader(ttm, &ttm->dmov, reader_path); - tensix_emit_writer(ttm, &ttm->dmov, writer_path); - tensix_emit_host_full(ttm, &ttm->dmov, host_path, - reader_path, compute_path, writer_path); - /* The same three cores as baby-core machine code, one shared L1 - * address map. The Metalium C++ above is the dev path; these ELFs - * are the toolchain-free path. */ - { - char elf_stem[BC_MAX_PATH]; - snprintf(elf_stem, sizeof(elf_stem), "%.*s", pfx, compute_path); - tensix_emit_kernel_elves(ttm, &ttm->dmov, elf_stem); - } - } else { - fprintf(stderr, "error: Tensix compilation failed\n"); - rc = trc; - } - free(ttm); - } + int brc = be_run((struct bir_module *)bir, (struct be_cfg *)cfg); + if (brc != BE_OK && rc == BC_OK) rc = BC_ERR_VERIFY; return rc; } @@ -452,10 +233,14 @@ static void usage(const char *prog) " --amdgpu-bin Compile to AMDGPU ELF code object (.hsaco)\n" " --tt-chip C Tenstorrent part: wormhole or blackhole " "(default blackhole)\n" - " --gfx90a Target CDNA 2 (gfx90a, MI250)\n" - " --gfx942 Target CDNA 3 (gfx942, MI300X)\n" - " --gfx1030 Target RDNA 2 (gfx1030)\n" - " --gfx1200 Target RDNA 4 (gfx1200)\n" + " AMD targets (default --gfx1100):\n" + " CDNA 2 --gfx90a (MI250)\n" + " CDNA 3 --gfx942 (MI300X)\n" + " RDNA 2 --gfx1030 --gfx1031 --gfx1032 --gfx1033 --gfx1034\n" + " --gfx1035 --gfx1036\n" + " RDNA 3 --gfx1100 --gfx1101 --gfx1102 --gfx1103\n" + " RDNA 3.5 --gfx1150 --gfx1151 --gfx1152 --gfx1153\n" + " RDNA 4 --gfx1200 --gfx1201\n" " --no-graphcolor Force linear scan register allocation\n" " --ssa-ra Divergence-aware SSA register allocation\n" " --max-vgprs N Cap VGPR count for regalloc (forces spills)\n" @@ -475,6 +260,10 @@ static void usage(const char *prog) " --xe2 Target Xe2 (Lunar Lake, next-gen Arc)\n" " -o Output file (for --amdgpu, --amdgpu-bin, --tensix, --nvidia-ptx,\n" " --metal, --intel-spirv). --amdgpu writes to stdout without it.\n" + " --snap AMD: write each kernel parameter's register value into a\n" + " host-visible buffer on entry, for the ABEND dump to read back\n" + " --bkhit NVIDIA: add a __bkhit counter parameter each block atomically\n" + " increments, so you can see which blocks actually ran\n" " --lang Load translated error messages\n" " --version Print version and exit\n" " --help Show this message\n" @@ -493,30 +282,15 @@ int main(int argc, char *argv[]) int mode_ir = 0; int mode_tdf = 0; int mode_tdf_fission = 0; - int mode_amdgpu = 0; - int mode_amdgpu_bin = 0; - int mode_cpu = 0; - int mode_rv64 = 0; - int mode_tensix = 0; - int mode_nvidia = 0; - int mode_metal = 0; - int mode_intel = 0; - int mode_rv_elf = 0; int mode_hip = 0; /* HIP frontend: see HIP NOTES below */ int mode_triton = 0; /* Triton frontend: see TRITON NOTES below */ - intel_target_t intel_target = INTEL_TARGET_XE_HPG; - int nv_bkhit = 0; int no_mem2reg = 0; int no_cfold = 0; int no_dce = 0; int no_sroa = 0; int no_sched = 0; int no_pp = 0; - int snap_mode = 0; td_chip_t tt_chip = TD_CHIP_BH; - amd_target_t amd_target = AMD_TARGET_GFX1100; - uint32_t amd_elfm = 0x41; /* EF_AMDGPU_MACH for exact chip */ - const char *amd_chip = "gfx1100"; /* chip string for ELF metadata */ /* Collect -I and -D options for preprocessor */ const char *include_paths[PP_MAX_INCLUDE_PATHS]; @@ -542,86 +316,20 @@ int main(int argc, char *argv[]) mode_tdf = 1; else if (strcmp(argv[i], "--tdf-fission") == 0) mode_tdf_fission = 1; - else if (strcmp(argv[i], "--rv-elf") == 0) - mode_rv_elf = 1; - else if (strcmp(argv[i], "--cpu") == 0) - mode_cpu = 1; - else if (strcmp(argv[i], "--rv64") == 0) - mode_rv64 = 1; else if (strcmp(argv[i], "--pp") == 0) mode_pp = 1; else if (strcmp(argv[i], "--no-pp") == 0) no_pp = 1; - else if (strcmp(argv[i], "--amdgpu") == 0) - mode_amdgpu = 1; - else if (strcmp(argv[i], "--amdgpu-bin") == 0) - mode_amdgpu_bin = 1; /* CDNA 2 (GFX9) */ - else if (strcmp(argv[i], "--gfx90a") == 0) - { amd_target = AMD_TARGET_GFX90A; amd_elfm = 0x3F; amd_chip = "gfx90a"; } /* CDNA 3 (GFX9.4.2) */ - else if (strcmp(argv[i], "--gfx942") == 0) - { amd_target = AMD_TARGET_GFX942; amd_elfm = 0x54C; amd_chip = "gfx942"; } /* xnack=off sramecc=off */ /* RDNA 2 (GFX10.3) */ - else if (strcmp(argv[i], "--gfx1030") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x36; amd_chip = "gfx1030"; } - else if (strcmp(argv[i], "--gfx1031") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x37; amd_chip = "gfx1031"; } - else if (strcmp(argv[i], "--gfx1032") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x38; amd_chip = "gfx1032"; } - else if (strcmp(argv[i], "--gfx1033") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x39; amd_chip = "gfx1033"; } - else if (strcmp(argv[i], "--gfx1034") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x3e; amd_chip = "gfx1034"; } - else if (strcmp(argv[i], "--gfx1035") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x3d; amd_chip = "gfx1035"; } - else if (strcmp(argv[i], "--gfx1036") == 0) - { amd_target = AMD_TARGET_GFX1030; amd_elfm = 0x45; amd_chip = "gfx1036"; } /* RDNA 3 (GFX11) */ - else if (strcmp(argv[i], "--gfx1100") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x41; amd_chip = "gfx1100"; } - else if (strcmp(argv[i], "--gfx1101") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x46; amd_chip = "gfx1101"; } - else if (strcmp(argv[i], "--gfx1102") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x47; amd_chip = "gfx1102"; } - else if (strcmp(argv[i], "--gfx1103") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x44; amd_chip = "gfx1103"; } /* RDNA 3.5 (GFX11.5) */ - else if (strcmp(argv[i], "--gfx1150") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x43; amd_chip = "gfx1150"; } - else if (strcmp(argv[i], "--gfx1151") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x4a; amd_chip = "gfx1151"; } - else if (strcmp(argv[i], "--gfx1152") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x55; amd_chip = "gfx1152"; } - else if (strcmp(argv[i], "--gfx1153") == 0) - { amd_target = AMD_TARGET_GFX1100; amd_elfm = 0x58; amd_chip = "gfx1153"; } /* RDNA 4 (GFX12) */ - else if (strcmp(argv[i], "--gfx1200") == 0) - { amd_target = AMD_TARGET_GFX1200; amd_elfm = 0x48; amd_chip = "gfx1200"; } - else if (strcmp(argv[i], "--gfx1201") == 0) - { amd_target = AMD_TARGET_GFX1200; amd_elfm = 0x4e; amd_chip = "gfx1201"; } - else if (strcmp(argv[i], "--tensix") == 0) - mode_tensix = 1; - else if (strcmp(argv[i], "--nvidia-ptx") == 0) - mode_nvidia = 1; - else if (strcmp(argv[i], "--metal") == 0) - mode_metal = 1; - else if (strcmp(argv[i], "--intel-spirv") == 0) - mode_intel = 1; - else if (strcmp(argv[i], "--xe-lpg") == 0) - intel_target = INTEL_TARGET_XE_LPG; - else if (strcmp(argv[i], "--xe-hpg") == 0) - intel_target = INTEL_TARGET_XE_HPG; - else if (strcmp(argv[i], "--xe-hpc") == 0) - intel_target = INTEL_TARGET_XE_HPC; - else if (strcmp(argv[i], "--xe2") == 0) - intel_target = INTEL_TARGET_XE2; else if (strcmp(argv[i], "--hip") == 0) mode_hip = 1; else if (strcmp(argv[i], "--triton") == 0) mode_triton = 1; - else if (strcmp(argv[i], "--bkhit") == 0) - nv_bkhit = 1; else if (strcmp(argv[i], "--tt-chip") == 0 && i + 1 < argc) { if (td_pchip(argv[++i], &tt_chip) != BC_OK) { fprintf(stderr, "unknown Tenstorrent chip: %s " @@ -655,20 +363,23 @@ int main(int argc, char *argv[]) no_sroa = 1; else if (strcmp(argv[i], "--no-sched") == 0) no_sched = 1; - else if (strcmp(argv[i], "--no-graphcolor") == 0) - amd_ra_lin = 1; - else if (strcmp(argv[i], "--ssa-ra") == 0) - amd_ra_ssa = 1; - else if (strcmp(argv[i], "--max-vgprs") == 0 && i + 1 < argc) - amd_max_vgpr = atoi(argv[++i]); - else if (strcmp(argv[i], "--snap") == 0) - snap_mode = 1; else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { usage(argv[0]); return 0; } else if (argv[i][0] != '-') file = argv[i]; else { + /* Not one of the driver's, so offer it round the backend + * registry before calling it unknown. Target selection and + * every target-specific knob lives in the backend that owns + * it, which is why main no longer has a --gfx list. */ + int used_next = 0; + int taken = be_parse_flag(argv[i], + (i + 1 < argc) ? argv[i + 1] : NULL, + &used_next); + if (taken < 0) return 1; + if (taken > 0) { i += used_next; continue; } + fprintf(stderr, "unknown option: %s\n", argv[i]); usage(argv[0]); return 1; @@ -687,9 +398,7 @@ int main(int argc, char *argv[]) * --tdf reached some gates and not others. Triton keeps its own list below; * it gates a different frontend and does not accept --rv-elf. */ int want_bir = mode_ir || mode_tdf || mode_tdf_fission || - mode_amdgpu || mode_amdgpu_bin || mode_tensix || - mode_nvidia || mode_metal || mode_intel || - mode_rv_elf || mode_cpu || mode_rv64; + be_num_on() > 0u; int want_sema = mode_sema || want_bir; if (!mode_pp && !mode_lex && !mode_parse && !want_sema) @@ -759,9 +468,7 @@ int main(int argc, char *argv[]) } /* One list, used by both the gate and the sema/lower decision below. * It was two, they disagreed, and --cpu fell down the gap. */ - int want_backend = mode_amdgpu || mode_amdgpu_bin || - mode_tensix || mode_nvidia || - mode_metal || mode_intel || mode_cpu || mode_rv64; + int want_backend = be_num_on() > 0u; if (mode_parse || mode_sema || mode_ir || want_backend) { tn_parse_t *tnp = (tn_parse_t *)malloc(sizeof(tn_parse_t)); if (!tnp) { @@ -809,19 +516,6 @@ int main(int argc, char *argv[]) cfg.mode_ir = mode_ir; cfg.mode_tdf = mode_tdf; cfg.mode_tdf_fission = mode_tdf_fission; - cfg.mode_rv_elf = mode_rv_elf; cfg.mode_cpu = mode_cpu; cfg.mode_rv64 = mode_rv64; - cfg.mode_amdgpu = mode_amdgpu; - cfg.mode_amdgpu_bin = mode_amdgpu_bin; - cfg.mode_tensix = mode_tensix; - cfg.mode_nvidia = mode_nvidia; - cfg.nv_bkhit = nv_bkhit; - cfg.mode_metal = mode_metal; - cfg.mode_intel = mode_intel; - cfg.amd_target = amd_target; - cfg.amd_elfm = amd_elfm; - cfg.amd_chip = amd_chip; - cfg.snap_mode = snap_mode; - cfg.intel_target = intel_target; cfg.output_file = output_file; brc = run_bir_backends(bir_module, &cfg); } @@ -884,7 +578,11 @@ int main(int argc, char *argv[]) if (mode_hip) { pp_define(pp, "__HIPCC__", "1"); pp_define(pp, "__HIP_DEVICE_COMPILE__", "1"); - if (mode_nvidia) + /* Which platform HIP thinks it is compiling for follows the + * selected target, so ask the registry rather than keep a + * copy of the mode flag here. */ + const be_desc_t *hb = be_active(); + if (hb != NULL && strcmp(hb->name, "nvptx") == 0) pp_define(pp, "__HIP_PLATFORM_NVIDIA__", "1"); else pp_define(pp, "__HIP_PLATFORM_AMD__", "1"); @@ -955,7 +653,7 @@ int main(int argc, char *argv[]) fprintf(stderr, "error: failed to allocate sema context\n"); return 1; } - sema_init(sema_ctx, &P, root, (int)amd_target); + sema_init(sema_ctx, &P, root, (int)be_warp_size()); sema_check(sema_ctx, root); bc_diag(file, lex_src, sema_ctx->errors, sema_ctx->num_errors); @@ -996,19 +694,6 @@ int main(int argc, char *argv[]) cfg.mode_ir = mode_ir; cfg.mode_tdf = mode_tdf; cfg.mode_tdf_fission = mode_tdf_fission; - cfg.mode_rv_elf = mode_rv_elf; cfg.mode_cpu = mode_cpu; cfg.mode_rv64 = mode_rv64; - cfg.mode_amdgpu = mode_amdgpu; - cfg.mode_amdgpu_bin = mode_amdgpu_bin; - cfg.mode_tensix = mode_tensix; - cfg.mode_nvidia = mode_nvidia; - cfg.nv_bkhit = nv_bkhit; - cfg.mode_metal = mode_metal; - cfg.mode_intel = mode_intel; - cfg.amd_target = amd_target; - cfg.amd_elfm = amd_elfm; - cfg.amd_chip = amd_chip; - cfg.snap_mode = snap_mode; - cfg.intel_target = intel_target; cfg.output_file = output_file; int brc = run_bir_backends(bir_module, &cfg); if (brc != BC_OK) rc = brc; diff --git a/src/metal/metal_be.c b/src/metal/metal_be.c new file mode 100644 index 0000000..097c5ef --- /dev/null +++ b/src/metal/metal_be.c @@ -0,0 +1,59 @@ +/* metal_be.c -- Apple Metal as a be_desc_t. + * Stub-level until someone with an M-series and an afternoon hardware + * validates it. Who tf makes Fortran run on a Mac? Us, eventually. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "metal.h" +#include "barracuda.h" +#include +#include + +typedef struct { int on; } mt_opts_t; + +static const char *const mt_flags[] = { "--metal", NULL }; + +static int mt_parse(const char *arg, const char *next, void *o) +{ + (void)next; + if (strcmp(arg, "--metal") == 0) { ((mt_opts_t *)o)->on = 1; } + return 0; +} + +static int mt_on(const void *o) { return ((const mt_opts_t *)o)->on; } + +static int mt_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)cfg; (void)o; + metal_module_t *mm = calloc(1, sizeof(*mm)); + if (mm == NULL) return BE_ENOMEM; + if (metal_compile((const bir_module_t *)M, mm) != BC_OK) { + free(mm); + return BE_EISEL; + } + *out_mmod = mm; + return BE_OK; +} + +static int mt_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + return metal_emit_msl((metal_module_t *)mmod, out ? out : "a.metal") == BC_OK + ? BE_OK : BE_EEMIT; +} + +const be_desc_t be_metal = { + .name = "metal", + .triple = NULL, + .feats = BE_F_SIMT | BE_F_SHARED | BE_F_BARRIER | BE_F_TRANSC + | BE_F_F16 | BE_F_F64, + .opts_size = sizeof(mt_opts_t), + .flags = mt_flags, + .parse = mt_parse, + .is_on = mt_on, + .isel = mt_isel, + .emit = mt_emit, + .mfree = free +}; diff --git a/src/nvidia/nv_be.c b/src/nvidia/nv_be.c new file mode 100644 index 0000000..3a0beb4 --- /dev/null +++ b/src/nvidia/nv_be.c @@ -0,0 +1,69 @@ +/* nv_be.c -- NVIDIA PTX as a be_desc_t. + * Smallest of the lot: PTX is a text format so we jump isel to emit + * and skip the fine-grained pipeline. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "nvidia.h" +#include "barracuda.h" +#include +#include + +typedef struct { + int on; + int bkhit; +} nv_opts_t; + +static const char *const nv_flags[] = { + "--nvidia-ptx", "--bkhit", NULL +}; + +static int nv_parse(const char *arg, const char *next, void *o) +{ + nv_opts_t *p = (nv_opts_t *)o; + (void)next; + if (strcmp(arg, "--nvidia-ptx") == 0) { p->on = 1; return 0; } + if (strcmp(arg, "--bkhit") == 0) { p->bkhit = 1; return 0; } + return 0; +} + +static int nv_on(const void *o) { return ((const nv_opts_t *)o)->on; } + +static int nv_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + const nv_opts_t *p = (const nv_opts_t *)o; + (void)cfg; + nv_module_t *nvm = calloc(1, sizeof(*nvm)); + if (nvm == NULL) return BE_ENOMEM; + if (nv_compile((const bir_module_t *)M, nvm) != BC_OK) { + free(nvm); + return BE_EISEL; + } + nvm->bkhit = (uint8_t)p->bkhit; + *out_mmod = nvm; + return BE_OK; +} + +static int nv_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + return nv_emit_ptx((nv_module_t *)mmod, out ? out : "a.ptx") == BC_OK + ? BE_OK : BE_EEMIT; +} + +const be_desc_t be_ptx = { + .name = "nvptx", + .triple = "nvptx64--", + .feats = BE_F_SIMT | BE_F_ATOMIC | BE_F_SHARED | BE_F_WARP + | BE_F_BARRIER | BE_F_DIV | BE_F_SCRATCH | BE_F_TRANSC + | BE_F_F16 | BE_F_F64 | BE_F_BF16 | BE_F_NOCALL, + .opts_size = sizeof(nv_opts_t), + .flags = nv_flags, + .parse = nv_parse, + .is_on = nv_on, + .isel = nv_isel, + .emit = nv_emit, + .mfree = free +}; diff --git a/src/tensix/tensix_be.c b/src/tensix/tensix_be.c new file mode 100644 index 0000000..0b91512 --- /dev/null +++ b/src/tensix/tensix_be.c @@ -0,0 +1,179 @@ +/* tensix_be.c -- Tensix Metalium (be_tsx) and RV32 baby-core + * (be_rvcore) as be_desc_t descriptors. + * + * Metalium emits five files off one path stem: compute, reader, + * writer, host, plus the .bin and .ttinsn drops. It's ceremony but + * it's the ceremony Tensix wants. Blackhole's in the room and I'm + * hoping it gets less silly over time. */ + +#include "backend.h" +#include "backend_cfg.h" +#include "tensix.h" +#include "rv_isel.h" +#include "rv_elf.h" +#include "rv_buf.h" +#include "barracuda.h" +#include +#include +#include + +/* ---- Metalium ---- + * tt_wrap keeps the BIR pointer alive through emit so datamov + * analysis can run inside isel where BIR is still in scope. */ + +typedef struct { + tt_module_t *tt; + const bir_module_t *bir; +} tt_wrap_t; + +typedef struct { int on; } tt_opts_t; + +static const char *const tt_flags[] = { "--tensix", NULL }; + +static int tt_parse(const char *arg, const char *next, void *o) +{ + (void)next; + if (strcmp(arg, "--tensix") == 0) { ((tt_opts_t *)o)->on = 1; } + return 0; +} + +static int tt_on(const void *o) { return ((const tt_opts_t *)o)->on; } + +static int tt_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)cfg; (void)o; + tt_wrap_t *w = calloc(1, sizeof(*w)); + if (w == NULL) return BE_ENOMEM; + w->tt = calloc(1, sizeof(*w->tt)); + if (w->tt == NULL) { free(w); return BE_ENOMEM; } + w->bir = (const bir_module_t *)M; + + if (tensix_compile(w->bir, w->tt) != BC_OK) { + fprintf(stderr, "error: Tensix compilation failed\n"); + free(w->tt); free(w); + return BE_EISEL; + } + tensix_coarsen(w->tt); + tensix_regalloc(w->tt); + tensix_analyze_datamov(w->bir, w->tt, &w->tt->dmov); + *out_mmod = w; + return BE_OK; +} + +/* Metalium emits five siblings off a compute-path stem; the file + * juggling isn't obvious but matches what main.c did before. */ +static int tt_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + const tt_wrap_t *w = mmod; + tt_module_t *ttm = w->tt; + const char *compute = out ? out : "a_compute.cpp"; + + tensix_emit_metalium(ttm, compute); + + char path[BC_MAX_PATH]; + const char *st2 = strstr(compute, "_compute"); + int bp = st2 ? (int)(st2 - compute) : (int)strlen(compute); + snprintf(path, sizeof(path), "%.*s_compute.bin", bp, compute); + tensix_emit_binary(ttm, path); + snprintf(path, sizeof(path), "%.*s_compute.ttinsn", bp, compute); + tensix_emit_ttinsn(ttm, path); + + int pfx; + if (st2) pfx = (int)(st2 - compute); + else { + const char *dot = strrchr(compute, '.'); + pfx = dot ? (int)(dot - compute) : (int)strlen(compute); + } + char host[BC_MAX_PATH], rd[BC_MAX_PATH], wr[BC_MAX_PATH]; + snprintf(host, sizeof(host), "%.*s_host.cpp", pfx, compute); + snprintf(rd, sizeof(rd), "%.*s_reader.cpp", pfx, compute); + snprintf(wr, sizeof(wr), "%.*s_writer.cpp", pfx, compute); + tensix_emit_reader(ttm, &ttm->dmov, rd); + tensix_emit_writer(ttm, &ttm->dmov, wr); + tensix_emit_host_full(ttm, &ttm->dmov, host, rd, compute, wr); + + char stem[BC_MAX_PATH]; + snprintf(stem, sizeof(stem), "%.*s", pfx, compute); + tensix_emit_kernel_elves(ttm, &ttm->dmov, stem); + return BE_OK; +} + +static void tt_free(void *mmod) +{ + tt_wrap_t *w = mmod; + if (w == NULL) return; + free(w->tt); + free(w); +} + +const be_desc_t be_tsx = { + .name = "tensix", + .triple = NULL, + .feats = BE_F_SIMT | BE_F_SHARED | BE_F_BARRIER | BE_F_MFMA | BE_F_NOCALL + | BE_F_MULTIOUT | BE_F_F16 | BE_F_BF16, + .opts_size = sizeof(tt_opts_t), + .flags = tt_flags, + .parse = tt_parse, + .is_on = tt_on, + .isel = tt_isel, + .emit = tt_emit, + .mfree = tt_free +}; + +/* ---- Baby-core RV32IM ---- */ + +static const char *const rvc_flags[] = { "--rv-elf", NULL }; + +static int rvc_parse(const char *arg, const char *next, void *o) +{ + (void)next; + if (strcmp(arg, "--rv-elf") == 0) { ((tt_opts_t *)o)->on = 1; } + return 0; +} + +static int rvc_on(const void *o) { return ((const tt_opts_t *)o)->on; } + +static int rvc_isel(const struct bir_module *M, const be_cfg_t *cfg, + const void *o, void **out_mmod) +{ + (void)cfg; (void)o; (void)o; + const bir_module_t *bir = (const bir_module_t *)M; + if (bir->num_funcs == 0u) { + fprintf(stderr, "error: BIR module has no functions\n"); + return BE_EINPUT; + } + rv_buf_t *code = calloc(1, sizeof(*code)); + if (code == NULL) return BE_ENOMEM; + rv_buf_init(code); + if (rv_isel_module(bir, code) != BC_OK) { free(code); return BE_EISEL; } + *out_mmod = code; + return BE_OK; +} + +static int rvc_emit(const void *mmod, const be_cfg_t *cfg, const void *o, + const char *out) +{ + (void)cfg; (void)o; + const rv_buf_t *code = mmod; + const char *p = out ? out : "a.elf"; + if (rv_elf_write(code, p) != BC_OK) return BE_EIO; + fprintf(stderr, "wrote %s (%u bytes code, %u instructions)\n", + p, rv_buf_nbytes(code), rv_buf_n_words(code)); + return BE_OK; +} + +const be_desc_t be_rvcore = { + .name = "tensix-rv32", + .triple = "riscv32-unknown-none", + .feats = BE_F_SCALAR, + .opts_size = sizeof(tt_opts_t), + .flags = rvc_flags, + .parse = rvc_parse, + .is_on = rvc_on, + .isel = rvc_isel, + .emit = rvc_emit, + .mfree = free +}; diff --git a/tests/tbackend.c b/tests/tbackend.c new file mode 100644 index 0000000..e60ae18 --- /dev/null +++ b/tests/tbackend.c @@ -0,0 +1,158 @@ +/* tbackend.c -- backend descriptor contract checks. Broken + * descriptor at test time beats broken kernel at runtime. */ + +#include "tharns.h" +#include "backend.h" +#include + +static void be_list_ok(void) +{ + uint32_t n = 0; + for (uint32_t i = 0; be_list[i] != NULL && i < 64; i++) { + const be_desc_t *b = be_list[i]; + CHECK(b->name != NULL && b->name[0] != '\0'); + CHECK(b->is_on != NULL); + CHECK(b->isel != NULL); + CHECK(b->emit != NULL); + n++; + } + CHECK(n >= 8); + PASS(); +} +TH_REG("backend", be_list_ok) + +static void be_find_hits(void) +{ + static const char * const known[] = { + "amdgpu", "nvptx", "tensix", "tensix-rv32", + "metal", "intel-spirv", "cpu-x86-64", "cpu-rv64" + }; + for (uint32_t i = 0; i < sizeof(known)/sizeof(known[0]); i++) { + const be_desc_t *b = be_find(known[i]); + CHECK(b != NULL); + CHSTR(b->name, known[i]); + } + PASS(); +} +TH_REG("backend", be_find_hits) + +static void be_find_miss(void) +{ + CHECK(be_find(NULL) == NULL); + CHECK(be_find("") == NULL); + CHECK(be_find("this-is-not-a-real-backend") == NULL); + PASS(); +} +TH_REG("backend", be_find_miss) + +static void be_feats_sane(void) +{ + const uint32_t all = + 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; + for (uint32_t i = 0; be_list[i] != NULL && i < 64; i++) { + CHEQ(be_list[i]->feats & ~all, 0u); + } + PASS(); +} +TH_REG("backend", be_feats_sane) + +/* ---- one target per run ---- + * Every backend emits to cfg->output_file, so several at once used to + * write over each other and leave whichever sorted last in be_list. */ + +static char be_obuf[TH_BUFSZ]; + +static void be_one_target(void) +{ + int rc = th_run(BC_BIN " --amdgpu-bin --nvidia-ptx --metal " + "tests/vector_add.cu -o be_multi.out", + be_obuf, TH_BUFSZ); + CHNE(rc, 0); + CHECK(strstr(be_obuf, "pick one") != NULL); + /* and nothing written, so a build system cannot pick up the wrong one */ + FILE *f = fopen("be_multi.out", "r"); + CHECK(f == NULL); + if (f) fclose(f); + remove("be_multi.out"); + PASS(); +} +TH_REG("backend", be_one_target) + +static void be_one_target_ok(void) +{ + int rc = th_run(BC_BIN " --nvidia-ptx tests/vector_add.cu -o be_single.ptx", + be_obuf, TH_BUFSZ); + CHEQ(rc, 0); + remove("be_single.ptx"); + PASS(); +} +TH_REG("backend", be_one_target_ok) + +/* ---- no two backends claim the same flag ---- + * Flags are declared in the descriptor rather than discovered, so a + * collision is a static error we can catch here instead of a + * first-past-the-post race decided by be_list order. */ + +static void be_flags_unique(void) +{ + for (uint32_t i = 0; be_list[i] != NULL && i < 64; i++) { + if (be_list[i]->flags == NULL) continue; + for (uint32_t a = 0; be_list[i]->flags[a] != NULL; a++) { + for (uint32_t j = i + 1; be_list[j] != NULL && j < 64; j++) { + if (be_list[j]->flags == NULL) continue; + for (uint32_t b = 0; be_list[j]->flags[b] != NULL; b++) { + CHECK(strcmp(be_list[i]->flags[a], + be_list[j]->flags[b]) != 0); + } + } + } + } + PASS(); +} +TH_REG("backend", be_flags_unique) + +/* ---- a declared flag is a parsed flag ---- + * The list and parse() could drift apart, which would leave a flag + * routed to a backend that then ignores it. Offering each one back + * through the registry proves the pairing still holds. */ + +static void be_flags_parse(void) +{ + for (uint32_t i = 0; be_list[i] != NULL && i < 64; i++) { + if (be_list[i]->flags == NULL) continue; + CHECK(be_list[i]->parse != NULL); + CHECK(be_list[i]->opts_size <= BE_OPTS_MAX); + for (uint32_t a = 0; be_list[i]->flags[a] != NULL; a++) { + int used = 0; + /* "1" as the value so a flag wanting one is satisfied */ + CHEQ(be_parse_flag(be_list[i]->flags[a], "1", &used), 1); + } + } + PASS(); +} +TH_REG("backend", be_flags_parse) + +/* ---- every backend flag is documented ---- + * Backends own their flags now, so --help can fall behind without + * anything in the driver noticing. */ + +static char be_hbuf[TH_BUFSZ]; + +static void be_flags_documented(void) +{ + th_run(BC_BIN " --help", be_hbuf, TH_BUFSZ); + for (uint32_t i = 0; be_list[i] != NULL && i < 64; i++) { + if (be_list[i]->flags == NULL) continue; + for (uint32_t a = 0; be_list[i]->flags[a] != NULL; a++) { + if (strstr(be_hbuf, be_list[i]->flags[a]) == NULL) { + printf("\n undocumented: %s\n", be_list[i]->flags[a]); + CHECK(0); + } + } + } + PASS(); +} +TH_REG("backend", be_flags_documented)