Add Apple GPU backend for Triton - #48
Conversation
|
In combination with this work I was able to compile Qwen 3.5 with fla through torch inductor |
…support - Add utlx_async_commit_group custom op (#47) accepting variadic async tokens for proper token threading in async_load_commit_group - Add utlx_async_wait_group custom op (#48) accepting pendings + variadic async tokens for proper token threading in async_load_wait_group - Pass async token to utlx_local_load in SMEM path (C++ already supported optional token via operands[2])
|
@imperatormk Are you able to include the metal-ir-pipeline code without a submodule? I think we want to be able to review everything involved in the stack. |
|
I also wonder if we couldn't start with a much simpler PR for an initial Metal Hello World backend? I have an example of a really tiny backend here (does enough codeine for the vector-add kernel on amdgpu): https://github.com/plotfi/triton/tree/plotfi-nano-backend/third_party/nano |
ps. I had small issues when trying to build the upstream Triton on macOS, will quickly list them here because I am not sure they could have been avoided in another way without making cmake changes:
Let me know what you think! |
|
@plotfi I got an idea; any chance this code can be brought to a separate branch so someone can still use it and report potential issues before deciding to merge into main? I can track the main branch regularly given this repo is under active development. Let me know! |
|
@imperatormk @CRobeck @abrown What do yall think about making a dir or a branch for Darco here? I'd be for it, but I don't feel very easy with any external dependencies / git sub modules myself. Assuming everything is incorporated in one branch or directory thats fine by me. |
|
No submodule/ext dependencies as per the latest code. However the LLVM metal-ir-pipeline part bloats up the project size significantly, you can decide to rehost it separately on your end in the future |
|
I think you could refer to the code structure under triton-lang/triton third_party/nvidia and adjust your file organization accordingly. current pseudocode i'm also an macbook user 😊 |
|
@enjustli Sure I'll attempt together with next changes related to tracking the latest code from main. ps: Not sure about what M series you have at hand but help needed to test all this on M3+ because I fear there might be subtle differences of what their GPU ISA expects compared to the M1/M2 models that I have tested on! |
|
i use M2 apple silicon |
|
Maybe it could also be worth considering contributing this Apple backend implementation to the Triton ecosystem under the Maintaining it as an independent repository might make collaboration easier, especially since there seems to be growing interest in running LLM workloads on Apple hardware. I think there could be developers in the community who would be interested in contributing as well. The process would probably not be easy, but I still feel the attempt itself could be meaningful. With the current progress of coding agents and LLM-assisted development, the barrier to contributing to compiler infrastructure is also becoming lower than before. |
|
Thank you @enjustli yes that's definitely a cleaner deal however the maintainers here had an idea to exercise this repo by appending a whole backend here and work on shaping it better. I think it IS possible to maintain it like that as some kind of subsystem, similar to how torch has inductor |
|
That said, I’m still a bit concerned about the current pace of progress in this repository. Since the LLM ecosystem is evolving very quickly right now, I feel that getting feedback and iteration cycles earlier and more frequently could also be quite important. |
|
I found an issue discussing this problem already. From the issue history, it looks like three different people each made their own implementation. |
|
I think we should consider proposing an independent Triton-Metal backend under the triton-lang organization. Regardless of whether Apple eventually open-sources their own implementation, simply waiting probably isn't very meaningful. Having a community-driven repository could help consolidate development efforts, avoid duplicated work, and make collaboration much easier. Right now, one of the biggest difficulties is that information and experiments are scattered across many separate projects, which makes the ecosystem harder to move forward efficiently. |
|
@enjustli See triton-lang/triton#9701 - they were open to making a separate repo so I suppose well can return to that option if they still like (this would be my preference at this point). There is triton-cpu already so this is a good adjacent to it. BTW I'd like to see this integrated to torch inductor eventually (see above that I managed to pull off a good POC for that) so I don't know which approach would facilitate it best |
|
OK |
|
If it becomes a separate repository, one possible approach could be to package Triton-Metal as a standalone wheel and distribute it through pip. From the user side, the experience could remain very simple — after installation, users would just use This is similar to how the current triton-shared project works, where the entire backend is packaged and distributed as a complete wheel. https://github.com/facebookincubator/triton-shared/blob/b32f76945570b198279226cf9e89f9ee7be58da7/README.md?plain=1#L27-L41 |
|
My English is bad. so I mostly rely on AI-assisted translation. Please excuse me if some of my wording or phrasing sounds unnatural. |
|
Sounds like a good plan, let me try to get ahold of the devs here. And I'd trade my English for your Chinese any day of the week 💯 |
|
@imperatormk, ok, I'm back looking at this PR! Apologies for not reviewing this sooner but I've been pulled in a few other directions lately and I had put this PR in a mental bucket of "nothing needed from me." I did reiterate your request for access to the Triton Slack workspace; if we get that going, that will be a good way to have discussions on this kind of thing. Thanks for this contribution and being patient! So, a high-level review:
My sense at the moment is that if we talk through the testing and maintenance questions, this PR should be good to upstream. I'm no Apple GPU expert, though, so @plotfi may have some more review comments re: that interface and it looks like @enjustli had some thoughts about the directory layout. We can probably work those out, though, even in separate PRs, if we figure out a solid plan for the maintenance and testing angles. |
|
@abrown No problem thank you for the response! Just to mention that in the meanwhile just today a new repo has been created for this (currently empty). Let me push a clean version there now and we can try to coin out a CI pipeline (so we get reliable testing scheme as you mentioned) + other specifics without interfering with this codebase for the time being, while trying to keep it faithful so it can be brought in here at some point as per a decision of yours. Regarding maintenance I have no problem being around for anything that pops up and will be happy to help, however I meant to ask earlier too what is the vision for that angle for this repo in general, if a fault in one extension can affect the status of the whole collection or if say a change is made in the build system to accommodate a new extension and needs to be ensured it won't affect the existing ones, that sounds somewhat challenging. Does this confirm these 23k additions should be indeed kept in the new separate repo for the time being so as not to overload the development of triton-ext? PS. the 'minimal' backend PR was made according to @plotfi 's request to start slow but from that point I'd have no idea how to continue building upon it given the current state. Hope this addresses your points for now, eager to hear your opinion! |
…moteWideAccumulator - AtomicOpConversion/MetalDeviceLoadsVolatile: device fences + coherent rewrite + sem->order map make the CAS spinlock correct; i64 CAS now a clean diagnostic (no 64-bit device atomics on AGX). - PointeeTypeMap: type a phi feeding air.atomic.* by the intrinsic element type, fixing the materializeAll/Invalid-record crash (test_atomic_min_max_neg_zero). - Remove DemoteWideAccumulator (IPSCCP no longer needs it; simpler IR). - kTGResidentBudgetBytes 30720->32768.
…ic writer metadata Emit the canonical 8-arg air.atomic.global.cmpxchg.weak.i32 for scalar spinlock CAS so AGX honours the success/failure memory orders strongly, closing the cold-cache flake on acquire/release sems. The writer now emits the metal::_atomic arg metadata (air.arg_type_name + air.struct_type_info) the canonical form needs, else PSO creation crashes in TypeFinder; and stamps air.version 2.9 / MSL 4.1 when an 8-arg cmpxchg is present (the form is only honoured at that level). Gated to macOS 26+ (clean reject below).
…wering Copy the operand-offset/base structured bindings into plain locals before the tile-loader lambdas; capturing a structured binding is a C++20 extension under -std=gnu++17. No behavior change.
…AS rewrite The lock-guarded foreign buffer in a spinlock is now rewritten to air.load.device_coherent (was plain load); the lock word stays volatile.
…osed/strided) Resolve MMA operand row/col strides directly from the address IR via extractAffineMmaPtrInfo instead of reconstructing them from pointer differences. Covers row-major, transposed (x @ W.T, the LLM hot path) and simple-strided views; masked / non-affine / modulo / both-or-neither-unit fall back to the pointer-diff path. Also consolidates the per-operand routing flags into an OperandRouting struct. Dense GEMM device-direct loads now build addressing from IR-extracted strides (no ptrtoint/simd_shuffle reconstruction); gemm_bench err=0, dot tests pass.
- Dynamic TG: rebase @global_smem onto a dispatch-sized ptr addrspace(3) kernel param (staticThreadgroupMemoryLength -> 0); declare via the air.thread_group_bound fn attr so the AIR FromGenericGPU pass keeps the param dynamic instead of promoting it to a static global. - Writer pointee fixes (both twins): aggregate load/store from null, AS3 atomicrmw on a null/inline-constant base, multi-typed GEP on one AS3 pointer -> all kept objdump-clean instead of Invalid record. - Remove the unproven resident-A blocked-dot experiment (no maxThreads lift). - driver.py: drop the native-dispatch path; metal_utils binds the TG arena. - Move fragment-oracle dev tool out of the tree.
Remove three permanently-disabled (return-false / commented-out) code paths and the unused ScalarizeShuffleOperands pass, plus their callers and now-dead locals: - scalarizeVec1Users (MetalPrepare) - scalarizeMixedWidthTGVecStores (MetalPrepare) - scalarizeVectorSelects (BitcodeEmitter) - MetalScalarizeShuffleOperands pass (.cpp/.h, CMake, Metal.h decls, target-machine init+addPass)
abrown
left a comment
There was a problem hiding this comment.
I am only a fraction of the way through looking at this but I thought I would leave a few comments (I got bogged down in llvm-metal-target--how necessary is that?). One high-level comment is the testing story; is there any way to emulate this?
| @@ -0,0 +1,4 @@ | |||
| name = "applegpu_backend" | |||
| status = "experimental" | |||
…inear GEMM
Column-major B (B stride [1,K], the nn.Linear weight layout) bailed
resolveSmemOperand at shOrder[0]!=1 and fell to the slow TG scatter, while
A reached the packed SG-load -- the A-fast/B-slow asymmetry was a ~1.78x gap
vs MPSGraph on transposed-B GEMM (DistilBert vocab/FFN). resolveSmemOperand
now accepts col-major B (pitch=shape[0]); emitSGLoadT does a transposed TG
simdgroup load (stride {pitch,1}); loadBTile dispatches on bSmemColMajor.
Gated METAL_SMEM_BPACK (default on). err=0; vocab addmm 345->298ms,
3072 mm 33->25ms; gemm_bench square unchanged.
… + f64-bitcast narrow Frontend (DotOpAppleMmaConversion): gated tl.dot->matmul2d cooperative_tensor lowering (env METAL_COOP_MMA, default off, Metal4/Apple7+; 8x8 path unchanged when off). AIR bitcode writer hardening (both twins): preserve function/global section attr + irsymtab (STRTAB/SYMTAB) for air.externally_defined; poison/undef ptr operand encoding (CST_CODE_POISON); named-struct-ptr descriptor + tensor- runtime arg typing; ValueAsMetadata typeIdx; shared-functype + lifetime-pointee inference; metadata cycle guard; strip lifetime intrinsics+decls; vector-into- aggregate store split; GEP base-type fixups. MetalDemoteF64: narrow i64->double bit-pattern to float bits for bitcast i64->double->fptrunc (was invalid 64->32 bitcast -> llc abort). ScalarBufferPacking: skip prebaked air.kernel-md kernels + rebuild param attrs after renumber. Removed agx_tti opt-in + the MetalDeadAIRIntrinsicDCE pass (parked as tasks). All gated/additive; 8x8 GEMM err=0, fla 12/12, agx-crash-trunk green, inductor MPS suite clean.
… ArithmeticIntensity CMake to upstream
Yes the LLVM part is very needed, it is positioned as a counterpart of NVPTX for CUDA and then some, besides the common things like say BC serialization/AIR metadata attachment it also does lots of work to reconcile the IR emitted by the LLVM version that Triton uses to the version that the older LLVM that Xcode tooling uses will accept. Illustration:
needs to become roughly
needs to become At scale this becomes far more delicate: pointers-to-i1 crash the JIT -> remap i8; MMA needs each device pointer pinned to its matrix element type otherwise the JIT rejects it -> keep int8/half/bf16 pointers at their element type.... the list goes on and I hope you see the problem here. I'm now working on that code to: 1. reorder everything so it's more traceable and obvious 2. strip away/simplify what is done excessively (fix in one place makes other parts moot). It doesn't help that when the AGX JIT rejects the BC it will give a short message at best and obscure 'invalid bitcode' at worst. So then it's a cycle of llvm-reduce -> fix the code up (after trying to figure out how to by lowering a .metal equivalent to AIR and see how native tooling does it) -> rerun a large test suite to make sure no side effects were introduced. I was hoping that if this manages to land in the 'raw' but best-effort comprehensive version as is now, it will become more sophisticated as it gets more users/contributors. If you want to try some of that in isolation I can prepare some concrete runner or anything else that might be needed (needs GPU) |
…uthority PointeeRules, delete redundant passes, exhaustive type-registration + fail-loud guard, gather matmul2d glue) - Split BitcodeEmitter.cpp (2027->~828) into concern files: LowerPointerVectors, NormalizeGEPs, IntegerLegalize, AggregateScalarize, PointerPointeeRepair, PointeeRules, ConstantExprLower, CoopTensorLowering + PointerRepairUtil.h. - PointeeRules: single-authority typing (mmaElemFromName incl i8, requiredPhiPointee, requiredSelectPointee, requiredTensorArgPointee, reconcileGEPBaseType); collapse the analysis-vs-repair rule duplication. - Delete fixGEPBaseTypeMismatch pass (root-caused: scalarizeAggregateStores now emits typed GEPs); eliminate the Phase6->Phase8 MMA-collapse read-after-write. - ValueEnumerator: register all ptr-producing instruction kinds (inttoptr/addrspacecast/ exhaustive) + fail-loud guard on unregistered pointee past the frozen type table. - LowerPointerVectors: lower vector GEPs (per-lane i64) instead of bailing; fail-loud on unhandled <N x ptr> producer. - AggregateScalarize: symmetric per-element vector stores (no spurious scalar split). - Gather matmul2d/cooperative_tensor glue into CoopTensorLowering; share kExternallyDefinedSection; centralize event_t; route FunctionWriter MMA-float via PTM. - disjoint-strip -> IntegerLegalize; drop dead AsyncCopy tileShape param. Trunk 36/0, inductor MPS 1228/1228, build clean.
…asm-path disjoint lit tests)
Rename IntegerLegalize -> MetalLegalizeUnsupportedIR (honest name: strip/lower
newer-LLVM constructs the AIR v1 bitcode era can't encode) and run it as a
codegen-prepare legacy pass instead of inline in the writer driver. The six
strips (lifetime, wide-int, freeze, nneg, disjoint, cmp) are PTM-free pure IR
rewrites; running them in addCodeGenPrepare makes them visible on the
-filetype=asm path, restoring the normalize-allocas{,-strip-disjoint}.ll lit
tests. Obj-path behavior unchanged (trunk 36/0, lit green). + f64-demote TODO.
…, abs path, stage-2 planning language)
|
@abrown If you've reviewed these changes, I have no problem with your approval. Sorry for being unavailable for review these days. |
I believe the macos-xlarge runners allow using the Apple GPU. |
|
@imperatormk out of interest, will this backend work for gluon? |
|
@robtaylor Should! No expected blockers; I meant to work on this but then focused on the core part because I wasn't sure it would be interesting to anyone initially. Now I'm working on a playground that will support multiple producers (I figure it can be useful for someone not necessarily wanting to go through a local build just to try it out a bit) and can try to add in Gluon there, will keep you posted! |
|
thanks man, thats really exciting!
…On Mon, Jul 6, 2026 at 10:13 AM Darko ***@***.***> wrote:
*imperatormk* left a comment (triton-lang/triton-ext#48)
<#48 (comment)>
@robtaylor <https://github.com/robtaylor> Should! No expected blockers; I
meant to work on this but then focused on the core part because I wasn't
sure it would be interesting to anyone initially. Now I'm working on a
playground that will support multiple producers (I figure it can be useful
for someone not necessarily wanting to go through a local build just to try
it out a bit) and can try to add in Gluon there, will keep you posted!
—
Reply to this email directly, view it on GitHub
<#48?email_source=notifications&email_token=AAB4O4GO23P3GLDCG7MZGOD5DNUUPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIOBZGEYDKNZXGYYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4891057760>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAB4O4F5IMMLRL6A4J4SXJD5DNUUPAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMJQGEZTQMBQGY5US43TOVSTWNBQHA2TINJSHAYDNILWAI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Add gluon_to_ttgir stage (skip TTIR, run dialect-generic Gluon passes), warp_size/default_dot_input_precision/get_target_name on MPSOptions, and Language.GLUON branch in add_stages. Extend ConvertLayoutOpConversion to lower a blocked->dot_op convert when no tt.dot consumes it (materialized dot-operand tensor via LinearLayout), instead of unconditionally peeling.
|
No problem, it worked out almost right off the shelf; available to test in the playground now (live url listed in https://github.com/imperatormk/applegpu-playground). Let me know how it goes! |

Out-of-tree Apple GPU (Metal) backend with:
C++ MLIR passes (loaded via TRITON_PASS_PLUGIN_PATH):
Python backend (pip installable, entry_points discovery):
metal-ir-pipeline (git submodule):