d4r: official NVIDIA DLSS on AMD GPUs under Linux and Proton

This commit is contained in:
ayo committed 2026-09-27 15:26:56 -04:00
commit 118173ecd7
128 files changed
+31333

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,772 @@
diff --git a/ptx/src/pass/mod.rs b/ptx/src/pass/mod.rs
index 15dd9ba..cff04dd 100644
--- a/ptx/src/pass/mod.rs
+++ b/ptx/src/pass/mod.rs
@@ -61,6 +61,42 @@ pub struct Attributes {
pub clock_rate: u32,
}
+/// Work-group size bound applied to kernels without `.maxntid`/`.reqntid`
+/// (D4R_ZLUDA_IMPLICIT_MAX_BLOCK, 1..=1024; unset keeps LLVM's 1024 default).
+///
+/// Without a bound, LLVM must assume 1024-thread work-groups, which on gfx11
+/// in CU mode caps each wave at 96 VGPRs and makes register-heavy kernels
+/// (e.g. DLSS's tensor-core convolutions) spill kilobytes to scratch. CUDA's
+/// own compiler gives unbounded kernels up to 255 registers, which likewise
+/// limits them to 256-thread blocks; a launch exceeding the bound fails
+/// (as it would on NVIDIA) instead of misbehaving.
+/// Optional AMDGPU machine-scheduler strategy for kernels
+/// (D4R_ZLUDA_SCHED_STRATEGY: max-ilp, max-memory-clause, iterative-ilp,
+/// iterative-minreg or iterative-maxocc); unset keeps LLVM's default.
+pub fn sched_strategy() -> Option<&'static str> {
+ static VALUE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
+ VALUE
+ .get_or_init(|| {
+ std::env::var("D4R_ZLUDA_SCHED_STRATEGY").ok().filter(|value| {
+ matches!(
+ value.as_str(),
+ "max-ilp" | "max-memory-clause" | "iterative-ilp" | "iterative-minreg" | "iterative-maxocc"
+ )
+ })
+ })
+ .as_deref()
+}
+
+pub fn implicit_max_block() -> Option<u32> {
+ static VALUE: std::sync::OnceLock<Option<u32>> = std::sync::OnceLock::new();
+ *VALUE.get_or_init(|| {
+ std::env::var("D4R_ZLUDA_IMPLICIT_MAX_BLOCK")
+ .ok()
+ .and_then(|value| value.trim().parse::<u32>().ok())
+ .filter(|value| (1..=1024).contains(value))
+ })
+}
+
pub fn to_llvm_module<'input>(
ast: ast::Module<'input>,
attributes: Attributes,
diff --git a/ptx/src/lib.rs b/ptx/src/lib.rs
index 8daa637..79ed812 100644
--- a/ptx/src/lib.rs
+++ b/ptx/src/lib.rs
@@ -4,4 +4,6 @@ mod test;
pub use pass::to_llvm_module;
pub use pass::Attributes;
+pub use pass::implicit_max_block;
+pub use pass::sched_strategy;
pub use pass::TranslateError;
diff --git a/ptx/src/pass/llvm/emit.rs b/ptx/src/pass/llvm/emit.rs
index 5ab8985..e891c34 100644
--- a/ptx/src/pass/llvm/emit.rs
+++ b/ptx/src/pass/llvm/emit.rs
@@ -147,6 +147,23 @@ impl<'a, 'input> ModuleEmitContext<'a, 'input> {
}
self.emit_target_features(fn_);
self.emit_tuning(fn_, &method.tuning);
+ if method.is_kernel()
+ && !method.tuning.iter().any(|tuning| {
+ matches!(
+ tuning,
+ ptx_parser::TuningDirective::MaxNtid(..) | ptx_parser::TuningDirective::ReqNtid(..)
+ )
+ })
+ {
+ if let Some(size) = crate::pass::implicit_max_block() {
+ self.emit_fn_attribute_string(fn_, "amdgpu-flat-work-group-size", &format!("1,{size}"));
+ }
+ }
+ if method.is_kernel() {
+ if let Some(strategy) = crate::pass::sched_strategy() {
+ self.emit_fn_attribute_string(fn_, "amdgpu-sched-strategy", strategy);
+ }
+ }
if let Some(kernel_attrs) = &method.kernel_attributes {
self.emit_fn_attribute_string(
fn_,
diff --git a/zluda/src/impl/module.rs b/zluda/src/impl/module.rs
index a6adeef..0fbc7f7 100644
--- a/zluda/src/impl/module.rs
+++ b/zluda/src/impl/module.rs
@@ -260,6 +260,9 @@ fn get_best_ptx_and_compile(
let attributes = ExtraCacheAttributes {
clock_rate: hip_properties.clockRate as u32,
is_debug: cfg!(debug_assertions),
+ implicit_max_block: ptx::implicit_max_block(),
+ sched_strategy: ptx::sched_strategy(),
+ wmma: llvm_zluda::compile::wmma_rewrite_requested(),
};
let mut cache_with_key = match (text, global_state.cache_path.as_ref()) {
(Some(text), Some(p)) => (|| {
@@ -296,6 +299,9 @@ pub(crate) fn load_hip_module(
struct ExtraCacheAttributes {
is_debug: bool,
clock_rate: u32,
+ implicit_max_block: Option<u32>,
+ sched_strategy: Option<&'static str>,
+ wmma: bool,
}
fn get_hip_properties<'a>() -> Result<hipDeviceProp_tR0600, CUerror> {
diff --git a/llvm_zluda/src/compile.rs b/llvm_zluda/src/compile.rs
index b80262b..1284cbc 100644
--- a/llvm_zluda/src/compile.rs
+++ b/llvm_zluda/src/compile.rs
@@ -87,6 +87,18 @@ fn create_oclc_constants(ctx: &Context, gcn_arch: &str) -> Result<Module, String
Ok(module)
}
+/// D4R_ZLUDA_WMMA=1 lowers f16 m16n8k16 MMAs to gfx11 WMMA (see
+/// LLVMZludaRewriteMmaF16Wmma); anything else keeps the dot-product fallback.
+pub fn wmma_rewrite_requested() -> bool {
+ static VALUE: OnceLock<bool> = OnceLock::new();
+ *VALUE.get_or_init(|| std::env::var("D4R_ZLUDA_WMMA").map_or(false, |value| value.trim() == "1"))
+}
+
+fn wmma_rewrite_enabled(gcn_arch: &str) -> bool {
+ // The helpers assume RDNA3's wave32 WMMA register layout.
+ wmma_rewrite_requested() && gcn_arch.starts_with("gfx11")
+}
+
pub fn compile(
ctx: &Context,
gcn_arch: &str,
@@ -106,6 +118,9 @@ pub fn compile(
let oclc_constants = create_oclc_constants(ctx, gcn_arch)?;
+ if wmma_rewrite_enabled(gcn_arch) {
+ unsafe { crate::ffi::LLVMZludaRewriteMmaF16Wmma(main.get()) };
+ }
linked.link(main)?;
linked.link(attributes)?;
linked.link(oclc_constants)?;
diff --git a/llvm_zluda/src/ffi.rs b/llvm_zluda/src/ffi.rs
index 7ca4289..6bf0d30 100644
--- a/llvm_zluda/src/ffi.rs
+++ b/llvm_zluda/src/ffi.rs
@@ -42,6 +42,9 @@ pub const LLVMZludaFastMathAll: ::std::ffi::c_uint = LLVMZludaFastMathAllowReass
pub type LLVMZludaFastMathFlags = std::ffi::c_uint;
extern "C" {
+ /// See LLVMZludaRewriteMmaF16Wmma in lib.cpp.
+ pub fn LLVMZludaRewriteMmaF16Wmma(M: LLVMModuleRef) -> u32;
+
pub fn LLVMZludaBuildAlloca(
B: LLVMBuilderRef,
Ty: LLVMTypeRef,
diff --git a/llvm_zluda/src/lib.cpp b/llvm_zluda/src/lib.cpp
index 14b4f40..30d5490 100644
--- a/llvm_zluda/src/lib.cpp
+++ b/llvm_zluda/src/lib.cpp
@@ -9,6 +9,11 @@
#include <llvm/Support/raw_ostream.h>
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"
+#include <llvm/ADT/DenseMap.h>
+#include <llvm/ADT/DenseSet.h>
+#include <llvm/ADT/SmallVector.h>
+#include <llvm/IR/Module.h>
+#include <llvm/IR/InstIterator.h>
#pragma GCC diagnostic pop
#include <mutex>
@@ -255,6 +260,367 @@ int LLVMZludaLinkWithLLD(uint32_t inputs, const char **input_paths, const char *
return 0;
}
+// gfx11 WMMA lowering of mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16,
+// run on ZLUDA's emitted module before linking, while PTX registers are still
+// allocas. Each MMA becomes
+// d = d4r_wmma_t_to_nv(d4r_wmma_t_mma_f16(a_from_b(B), b_from_a(A), C'))
+// (see zluda_ptx_impl.cpp for the layouts). Register pairs used as MMA
+// accumulators get a shadow alloca holding the pair in WMMA layout: an MMA
+// whose C is a plain load of such a pair reads the shadow, an MMA whose D is
+// stored to it writes the shadow, and any other store to either register
+// refreshes the shadow with d4r_wmma_t_from_nv. The invariant "shadow ==
+// WMMA layout of the pair" thus holds at every program point, and once
+// mem2reg promotes the allocas the layout conversions between chained MMAs
+// become dead code. Returns the number of MMAs rewritten.
+namespace
+{
+constexpr const char *kMmaF16 = "__zluda_ptx_impl_mma_sync_aligned_m16n8k16_row_col_f16_f16_f16_f16";
+
+struct RegisterPair
+{
+ AllocaInst *Low = nullptr;
+ AllocaInst *High = nullptr;
+ bool operator==(const RegisterPair &Other) const { return Low == Other.Low && High == Other.High; }
+};
+
+// Only allocas used purely as the address of loads and stores are tracked.
+static bool isPlainRegister(AllocaInst *Alloca)
+{
+ for (User *U : Alloca->users())
+ {
+ if (isa<LoadInst>(U))
+ continue;
+ auto *Store = dyn_cast<StoreInst>(U);
+ if (Store == nullptr || Store->getPointerOperand() != Alloca)
+ return false;
+ }
+ return true;
+}
+
+// Matches insertelement(insertelement(undef, load Low, 0), load High, 1) where
+// the loads sit in the call's block with no store to either register between
+// them and the call.
+static bool matchLoadedPair(Value *Vector, CallInst *Call, RegisterPair &Pair)
+{
+ auto *Second = dyn_cast<InsertElementInst>(Vector);
+ if (Second == nullptr)
+ return false;
+ auto *First = dyn_cast<InsertElementInst>(Second->getOperand(0));
+ auto *SecondIndex = dyn_cast<ConstantInt>(Second->getOperand(2));
+ if (First == nullptr || SecondIndex == nullptr || SecondIndex->getZExtValue() != 1)
+ return false;
+ auto *FirstIndex = dyn_cast<ConstantInt>(First->getOperand(2));
+ if (FirstIndex == nullptr || FirstIndex->getZExtValue() != 0 || !isa<UndefValue>(First->getOperand(0)))
+ return false;
+ auto *LowLoad = dyn_cast<LoadInst>(First->getOperand(1));
+ auto *HighLoad = dyn_cast<LoadInst>(Second->getOperand(1));
+ if (LowLoad == nullptr || HighLoad == nullptr || LowLoad->getParent() != Call->getParent() ||
+ HighLoad->getParent() != Call->getParent())
+ return false;
+ Pair.Low = dyn_cast<AllocaInst>(LowLoad->getPointerOperand());
+ Pair.High = dyn_cast<AllocaInst>(HighLoad->getPointerOperand());
+ if (Pair.Low == nullptr || Pair.High == nullptr || Pair.Low == Pair.High ||
+ !Pair.Low->getAllocatedType()->isIntegerTy(32) || !Pair.High->getAllocatedType()->isIntegerTy(32) ||
+ !isPlainRegister(Pair.Low) || !isPlainRegister(Pair.High))
+ return false;
+ Instruction *Earliest = LowLoad->comesBefore(HighLoad) ? LowLoad : HighLoad;
+ for (Instruction *I = Earliest->getNextNode(); I != Call; I = I->getNextNode())
+ if (auto *Store = dyn_cast<StoreInst>(I))
+ if (Store->getPointerOperand() == Pair.Low || Store->getPointerOperand() == Pair.High)
+ return false;
+ return true;
+}
+
+// Finds the stores of both lanes of an MMA result into a register pair.
+static bool matchStoredPair(CallInst *Call, RegisterPair &Pair, StoreInst *Stores[2])
+{
+ Stores[0] = Stores[1] = nullptr;
+ for (User *U : Call->users())
+ {
+ auto *Extract = dyn_cast<ExtractElementInst>(U);
+ auto *Index = Extract != nullptr ? dyn_cast<ConstantInt>(Extract->getIndexOperand()) : nullptr;
+ if (Index == nullptr || Index->getZExtValue() > 1 || !Extract->hasOneUse())
+ return false;
+ auto *Store = dyn_cast<StoreInst>(Extract->user_back());
+ if (Store == nullptr || Store->getValueOperand() != Extract || Stores[Index->getZExtValue()] != nullptr)
+ return false;
+ Stores[Index->getZExtValue()] = Store;
+ }
+ if (Stores[0] == nullptr || Stores[1] == nullptr)
+ return false;
+ Pair.Low = dyn_cast<AllocaInst>(Stores[0]->getPointerOperand());
+ Pair.High = dyn_cast<AllocaInst>(Stores[1]->getPointerOperand());
+ return Pair.Low != nullptr && Pair.High != nullptr && Pair.Low != Pair.High;
+}
+
+// Matches an N-element vector built as a chain of insertelements (indices
+// 0..N-1 into undef) of loads from plain i32 registers; returns the registers.
+static bool matchLoadedVector(Value *Vector, unsigned Elements, SmallVectorImpl<AllocaInst *> &Registers)
+{
+ Registers.assign(Elements, nullptr);
+ for (unsigned Remaining = Elements; Remaining > 0; --Remaining)
+ {
+ auto *Insert = dyn_cast<InsertElementInst>(Vector);
+ auto *Index = Insert != nullptr ? dyn_cast<ConstantInt>(Insert->getOperand(2)) : nullptr;
+ if (Index == nullptr || Index->getZExtValue() != Remaining - 1)
+ return false;
+ auto *Load = dyn_cast<LoadInst>(Insert->getOperand(1));
+ auto *Register = Load != nullptr ? dyn_cast<AllocaInst>(Load->getPointerOperand()) : nullptr;
+ if (Register == nullptr || !Register->getAllocatedType()->isIntegerTy(32) || !isPlainRegister(Register))
+ return false;
+ Registers[Remaining - 1] = Register;
+ Vector = Insert->getOperand(0);
+ }
+ return isa<UndefValue>(Vector);
+}
+
+// The registers are plain allocas (only loaded and stored directly), so only
+// explicit stores can change them.
+static bool storesBetween(Instruction *From, Instruction *To, ArrayRef<AllocaInst *> Registers)
+{
+ for (Instruction *I = From->getNextNode(); I != nullptr && I != To; I = I->getNextNode())
+ if (auto *Store = dyn_cast<StoreInst>(I))
+ if (is_contained(Registers, Store->getPointerOperand()))
+ return true;
+ return false;
+}
+
+struct Helpers
+{
+ FunctionCallee AFromB, BFromA, Mma, FromNv, ToNv, AFromBPair, MmaPair;
+};
+
+static Value *loadPair(IRBuilder<> &Builder, const RegisterPair &Pair)
+{
+ Type *I32 = Builder.getInt32Ty();
+ Value *Vector = UndefValue::get(FixedVectorType::get(I32, 2));
+ Vector = Builder.CreateInsertElement(Vector, Builder.CreateLoad(I32, Pair.Low), Builder.getInt8(0));
+ return Builder.CreateInsertElement(Vector, Builder.CreateLoad(I32, Pair.High), Builder.getInt8(1));
+}
+
+static unsigned rewriteFunction(Function &F, Function *Mma, const Helpers &H)
+{
+ SmallVector<CallInst *, 64> Calls;
+ for (Instruction &I : instructions(F))
+ if (auto *Call = dyn_cast<CallInst>(&I))
+ if (Call->getCalledFunction() == Mma && Call->arg_size() == 3)
+ Calls.push_back(Call);
+ if (Calls.empty())
+ return 0;
+
+ struct Site
+ {
+ CallInst *Call;
+ bool HasC;
+ RegisterPair C;
+ bool HasD;
+ RegisterPair D;
+ StoreInst *DStores[2];
+ };
+ SmallVector<Site, 64> Sites;
+ for (CallInst *Call : Calls)
+ {
+ Site S{Call, false, {}, false, {}, {nullptr, nullptr}};
+ S.HasC = matchLoadedPair(Call->getArgOperand(2), Call, S.C);
+ S.HasD = matchStoredPair(Call, S.D, S.DStores);
+ Sites.push_back(S);
+ }
+
+ // Accumulator pairs whose registers belong to no other pair are replaced
+ // outright by a shadow in WMMA layout.
+ SmallVector<RegisterPair, 32> Shadowed;
+ DenseMap<AllocaInst *, int> Owner; // register -> shadow index, -1 if ambiguous
+ auto consider = [&](const RegisterPair &Pair) {
+ for (const RegisterPair &Known : Shadowed)
+ if (Known == Pair)
+ return;
+ int Index = static_cast<int>(Shadowed.size());
+ for (AllocaInst *Member : {Pair.Low, Pair.High})
+ {
+ auto Found = Owner.find(Member);
+ if (Found != Owner.end())
+ Found->second = -1;
+ else
+ Owner[Member] = Index;
+ }
+ Shadowed.push_back(Pair);
+ };
+ for (const Site &S : Sites)
+ if (S.HasC)
+ consider(S.C);
+ auto shadowOf = [&](const RegisterPair &Pair) -> int {
+ auto Low = Owner.find(Pair.Low);
+ auto High = Owner.find(Pair.High);
+ if (Low == Owner.end() || High == Owner.end() || Low->second < 0 || Low->second != High->second)
+ return -1;
+ return Shadowed[Low->second] == Pair ? Low->second : -1;
+ };
+
+ Type *Pair32 = FixedVectorType::get(Type::getInt32Ty(F.getContext()), 2);
+ SmallVector<AllocaInst *, 32> Shadows;
+ {
+ IRBuilder<> Builder(&*F.getEntryBlock().getFirstInsertionPt());
+ for (size_t Index = 0; Index < Shadowed.size(); ++Index)
+ Shadows.push_back(
+ shadowOf(Shadowed[Index]) == static_cast<int>(Index)
+ ? Builder.CreateAlloca(Pair32, Shadowed[Index].Low->getAddressSpace(), nullptr, "wmma.shadow")
+ : nullptr);
+ }
+ auto shadowFor = [&](const RegisterPair &Pair) -> AllocaInst * {
+ int Index = shadowOf(Pair);
+ return Index >= 0 ? Shadows[Index] : nullptr;
+ };
+
+ // Snapshot every access to shadowed registers before rewriting anything.
+ SmallVector<LoadInst *, 64> Loads;
+ SmallVector<StoreInst *, 64> Stores;
+ for (auto &[Register, Index] : Owner)
+ {
+ if (Index < 0 || Shadows[Index] == nullptr)
+ continue;
+ for (User *U : Register->users())
+ {
+ if (auto *Load = dyn_cast<LoadInst>(U))
+ Loads.push_back(Load);
+ else
+ Stores.push_back(cast<StoreInst>(U));
+ }
+ }
+ DenseSet<StoreInst *> ResultStores; // MMA result stores replaced by shadow stores
+
+ auto accumulator = [&](const Site &S, IRBuilder<> &Builder) -> Value * {
+ if (S.HasC)
+ if (AllocaInst *Shadow = shadowFor(S.C))
+ return Builder.CreateLoad(Pair32, Shadow);
+ return Builder.CreateCall(H.FromNv, {S.Call->getArgOperand(2)});
+ };
+ auto finish = [&](const Site &S, Value *D, IRBuilder<> &Builder) {
+ if (S.HasD)
+ if (AllocaInst *Shadow = shadowFor(S.D))
+ {
+ // The result goes to the shadow where its register stores were.
+ StoreInst *Last = S.DStores[0]->comesBefore(S.DStores[1]) ? S.DStores[1] : S.DStores[0];
+ IRBuilder<> At(Last);
+ At.CreateStore(D, Shadow);
+ ResultStores.insert(S.DStores[0]);
+ ResultStores.insert(S.DStores[1]);
+ }
+ return Builder.CreateCall(H.ToNv, {D});
+ };
+ // Two MMAs that share A (same registers, no store in between to anything
+ // the second one reads) run as one 16x16x16 WMMA placed at the first.
+ auto pairable = [&](const Site &First, const Site &Second) {
+ SmallVector<AllocaInst *, 4> AFirst, ASecond, BSecond;
+ if (First.Call->getParent() != Second.Call->getParent() || !Second.HasC ||
+ shadowFor(Second.C) == nullptr || !First.Call->comesBefore(Second.Call) ||
+ !matchLoadedVector(First.Call->getArgOperand(0), 4, AFirst) ||
+ !matchLoadedVector(Second.Call->getArgOperand(0), 4, ASecond) || AFirst != ASecond ||
+ !matchLoadedVector(Second.Call->getArgOperand(1), 2, BSecond))
+ return false;
+ // The second B is reloaded at the first MMA, so it must not live in a
+ // shadow (those registers are no longer kept up to date).
+ for (AllocaInst *Register : BSecond)
+ {
+ auto Found = Owner.find(Register);
+ if (Found != Owner.end() && Found->second >= 0 && Shadows[Found->second] != nullptr)
+ return false;
+ }
+ SmallVector<AllocaInst *, 8> Read(AFirst.begin(), AFirst.end());
+ Read.append(BSecond.begin(), BSecond.end());
+ Read.push_back(Second.C.Low);
+ Read.push_back(Second.C.High);
+ return !storesBetween(First.Call, Second.Call, Read);
+ };
+ for (size_t Index = 0; Index < Sites.size(); ++Index)
+ {
+ const Site &S = Sites[Index];
+ CallInst *Call = S.Call;
+ IRBuilder<> Builder(Call);
+ Value *BTransposed = Builder.CreateCall(H.BFromA, {Call->getArgOperand(0)});
+ if (Index + 1 < Sites.size() && pairable(S, Sites[Index + 1]))
+ {
+ const Site &Next = Sites[Index + 1];
+ SmallVector<AllocaInst *, 2> BNext;
+ matchLoadedVector(Next.Call->getArgOperand(1), 2, BNext);
+ Value *B1 = loadPair(Builder, RegisterPair{BNext[0], BNext[1]});
+ Value *ATransposed = Builder.CreateCall(H.AFromBPair, {Call->getArgOperand(1), B1});
+ Value *C0 = accumulator(S, Builder);
+ Value *C1 = accumulator(Next, Builder);
+ Value *D = Builder.CreateCall(H.MmaPair, {ATransposed, BTransposed, C0, C1});
+ Value *D0 = Builder.CreateShuffleVector(D, ArrayRef<int>{0, 1});
+ Value *D1 = Builder.CreateShuffleVector(D, ArrayRef<int>{2, 3});
+ Call->replaceAllUsesWith(finish(S, D0, Builder));
+ Next.Call->replaceAllUsesWith(finish(Next, D1, Builder));
+ Call->eraseFromParent();
+ Next.Call->eraseFromParent();
+ ++Index;
+ continue;
+ }
+ Value *ATransposed = Builder.CreateCall(H.AFromB, {Call->getArgOperand(1)});
+ Value *D = Builder.CreateCall(H.Mma, {ATransposed, BTransposed, accumulator(S, Builder)});
+ Call->replaceAllUsesWith(finish(S, D, Builder));
+ Call->eraseFromParent();
+ }
+
+ // Every other access now goes through the shadow.
+ for (LoadInst *Load : Loads)
+ {
+ auto *Register = cast<AllocaInst>(Load->getPointerOperand());
+ const RegisterPair &Pair = Shadowed[Owner[Register]];
+ IRBuilder<> Builder(Load);
+ Value *Nv = Builder.CreateCall(H.ToNv, {Builder.CreateLoad(Pair32, shadowFor(Pair))});
+ Load->replaceAllUsesWith(Builder.CreateExtractElement(Nv, Builder.getInt8(Register == Pair.High)));
+ Load->eraseFromParent();
+ }
+ for (StoreInst *Store : Stores)
+ {
+ if (!ResultStores.contains(Store))
+ {
+ auto *Register = cast<AllocaInst>(Store->getPointerOperand());
+ const RegisterPair &Pair = Shadowed[Owner[Register]];
+ AllocaInst *Shadow = shadowFor(Pair);
+ IRBuilder<> Builder(Store);
+ Value *Nv = Builder.CreateCall(H.ToNv, {Builder.CreateLoad(Pair32, Shadow)});
+ Nv = Builder.CreateInsertElement(Nv, Store->getValueOperand(), Builder.getInt8(Register == Pair.High));
+ Builder.CreateStore(Builder.CreateCall(H.FromNv, {Nv}), Shadow);
+ }
+ auto *Operand = dyn_cast<Instruction>(Store->getValueOperand());
+ Store->eraseFromParent();
+ if (Operand != nullptr && Operand->use_empty() && !Operand->mayHaveSideEffects())
+ Operand->eraseFromParent();
+ }
+ return Calls.size();
+}
+} // namespace
+
+unsigned LLVMZludaRewriteMmaF16Wmma(LLVMModuleRef ModuleRef)
+{
+ Module *M = unwrap(ModuleRef);
+ Function *Mma = M->getFunction(kMmaF16);
+ if (Mma == nullptr || Mma->use_empty())
+ return 0;
+ LLVMContext &Context = M->getContext();
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *V2 = FixedVectorType::get(I32, 2);
+ Type *V4 = FixedVectorType::get(I32, 4);
+ Type *V8 = FixedVectorType::get(I32, 8);
+ AttributeList Attributes = AttributeList::get(Context, AttributeList::FunctionIndex,
+ AttrBuilder(Context, Mma->getAttributes().getFnAttrs()));
+ Helpers H;
+ H.AFromB = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_a_from_b", Attributes, V8, V2);
+ H.BFromA = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_b_from_a", Attributes, V8, V4);
+ H.Mma = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_mma_f16", Attributes, V2, V8, V8, V2);
+ H.FromNv = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_from_nv", Attributes, V2, V2);
+ H.ToNv = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_to_nv", Attributes, V2, V2);
+ H.AFromBPair = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_a_from_b_pair", Attributes, V8, V2, V2);
+ H.MmaPair = M->getOrInsertFunction("__zluda_ptx_impl_d4r_wmma_t_mma_f16_pair", Attributes, V4, V8, V8, V2, V2);
+ unsigned Rewritten = 0;
+ for (Function &F : *M)
+ if (!F.isDeclaration())
+ Rewritten += rewriteFunction(F, Mma, H);
+ return Rewritten;
+}
+
bool LLVMZludaParseCommandLineOptions(int argc, const char *const *argv,
char **ErrorMessage)
{
diff --git a/ptx/lib/zluda_ptx_impl.cpp b/ptx/lib/zluda_ptx_impl.cpp
index 69d1faa..c11e06a 100644
--- a/ptx/lib/zluda_ptx_impl.cpp
+++ b/ptx/lib/zluda_ptx_impl.cpp
@@ -1235,6 +1235,184 @@ extern "C"
}
}
+// gfx11 WMMA lowering of mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16.
+//
+// llvm_zluda's MMA rewrite (LLVMZludaRewriteMmaF16Wmma) replaces each such MMA
+// with these helpers. It computes D^T = B^T A^T + C^T with
+// v_wmma_f32_16x16x16_f16, so the 16x8 accumulator occupies WMMA result
+// rows 0..7, i.e. VGPRs 0..3 of the f32 result: lane l holds D[m][n] for
+// m = l % 16 and n = 2v + l / 16, v = 0..3 (the "transposed WMMA layout").
+// Those four values are kept as two f16x2 words (v0 | v1 << 16,
+// v2 | v3 << 16), the same register footprint as NVIDIA's own fragment.
+// Converting between the two layouts is pure 16-bit data movement, so the
+// rewrite keeps accumulators in WMMA layout across chained MMAs and converts
+// only where other code reads or writes them. Each MMA still rounds its result
+// to f16, as the .f16 accumulator requires; only the f32 summation order
+// within one MMA differs from the dot-product fallback.
+typedef _Float16 wmma_half16 __attribute__((ext_vector_type(16)));
+typedef float wmma_float8 __attribute__((ext_vector_type(8)));
+typedef uint32_t wmma_uint8 __attribute__((ext_vector_type(8)));
+__device__ wmma_float8 __zluda_wmma_f32_16x16x16_f16(wmma_half16 a, wmma_half16 b, wmma_float8 c) __asm("llvm.amdgcn.wmma.f32.16x16x16.f16.v8f32.v16f16");
+
+__device__ static uint32_t wmma_lane()
+{
+ return __builtin_amdgcn_mbcnt_lo(~0u, 0u);
+}
+
+__device__ static uint32_t wmma_from_lane(uint32_t lane, uint32_t value)
+{
+ return uint32_t(__builtin_amdgcn_ds_bpermute(int(lane << 2), int(value)));
+}
+
+// Gathers WMMA operand dword q for every lane from two NVIDIA fragment
+// registers: rows r < 8 take `low` from lane 4r+q, rows r >= 8 take `high`
+// from lane 4(r-8)+q (r = lane % 16). Swapping `high` between quad halves
+// (DPP quad_perm [2,3,0,1]) parks the value rows r >= 8 need in lane
+// 4(r-8)+(q^2), so one bpermute serves both halves.
+__device__ static uint32_t wmma_gather_rows(uint32_t low, uint32_t high_swapped, uint32_t q)
+{
+ uint32_t lane = wmma_lane();
+ uint32_t row = lane % 16;
+ uint32_t source = lane % 4 == q ? low : high_swapped;
+ uint32_t from = row < 8 ? row * 4 + q : (row - 8) * 4 + (q ^ 2);
+ return wmma_from_lane(from, source);
+}
+
+__device__ static uint32_t wmma_swap_quad_halves(uint32_t value)
+{
+ return uint32_t(__builtin_amdgcn_mov_dpp(int(value), 0b01'00'11'10, 0xf, 0xf, true));
+}
+
+extern "C"
+{
+ // WMMA A operand (lane l holds row l % 16 over k) = B^T, from an NVIDIA B
+ // fragment (lane 4n+q: B[2q..2q+1][n] in .x, B[2q+8..2q+9][n] in .y).
+ // Rows 8..15 (padding columns of the 16x8 B) are zero.
+ wmma_uint8 FUNC(d4r_wmma_t_a_from_b)(uint2::Native_vec_ b)
+ {
+ // Keep the scheduler from hoisting these shuffles (and the second
+ // candidate of each select) far ahead of the WMMA that consumes them,
+ // which otherwise spills them to scratch.
+ __builtin_amdgcn_sched_barrier(0);
+ uint32_t n = wmma_lane() % 16;
+ uint32_t source = (n % 8) * 4;
+ wmma_uint8 result;
+ for (uint32_t q = 0; q < 4; q++)
+ {
+ uint32_t low = wmma_from_lane(source + q, b.x);
+ uint32_t high = wmma_from_lane(source + q, b.y);
+ result[q] = n < 8 ? low : 0u;
+ result[q + 4] = n < 8 ? high : 0u;
+ }
+ return result;
+ }
+
+ // WMMA B operand (lane l holds column l % 16 over k) = A^T, i.e. row
+ // m = l % 16 of the NVIDIA A fragment (lane 4g+t: rows g/g+8, k 2t..2t+1 in
+ // .x/.y and k 2t+8..2t+9 in .z/.w).
+ wmma_uint8 FUNC(d4r_wmma_t_b_from_a)(uint4::Native_vec_ a)
+ {
+ // Keep the scheduler from hoisting these shuffles (and the second
+ // candidate of each select) far ahead of the WMMA that consumes them,
+ // which otherwise spills them to scratch.
+ __builtin_amdgcn_sched_barrier(0);
+ uint32_t ay = wmma_swap_quad_halves(a.y);
+ uint32_t aw = wmma_swap_quad_halves(a.w);
+ wmma_uint8 result;
+ for (uint32_t p = 0; p < 4; p++)
+ {
+ result[p] = wmma_gather_rows(a.x, ay, p);
+ result[p + 4] = wmma_gather_rows(a.z, aw, p);
+ }
+ return result;
+ }
+
+ // Paired form: the WMMA A operand rows 0..7 are B0^T and rows 8..15 are
+ // B1^T, so one WMMA computes two m16n8k16 MMAs that share A.
+ wmma_uint8 FUNC(d4r_wmma_t_a_from_b_pair)(uint2::Native_vec_ b0, uint2::Native_vec_ b1)
+ {
+ // Keep the scheduler from hoisting these shuffles (and the second
+ // candidate of each select) far ahead of the WMMA that consumes them,
+ // which otherwise spills them to scratch.
+ __builtin_amdgcn_sched_barrier(0);
+ uint32_t b1x = wmma_swap_quad_halves(b1.x);
+ uint32_t b1y = wmma_swap_quad_halves(b1.y);
+ wmma_uint8 result;
+ for (uint32_t q = 0; q < 4; q++)
+ {
+ result[q] = wmma_gather_rows(b0.x, b1x, q);
+ result[q + 4] = wmma_gather_rows(b0.y, b1y, q);
+ }
+ return result;
+ }
+
+ // Two accumulators (for B0 and B1) in transposed WMMA layout: .xy is the
+ // first, .zw the second; they are WMMA result VGPRs 0..3 and 4..7.
+ uint4::Native_vec_ FUNC(d4r_wmma_t_mma_f16_pair)(wmma_uint8 a_t, wmma_uint8 b_t, uint2::Native_vec_ c0_t, uint2::Native_vec_ c1_t)
+ {
+ f16x2 c01 = std::bit_cast<f16x2>(c0_t.x);
+ f16x2 c23 = std::bit_cast<f16x2>(c0_t.y);
+ f16x2 c45 = std::bit_cast<f16x2>(c1_t.x);
+ f16x2 c67 = std::bit_cast<f16x2>(c1_t.y);
+ wmma_float8 c = {float(c01.x), float(c01.y), float(c23.x), float(c23.y),
+ float(c45.x), float(c45.y), float(c67.x), float(c67.y)};
+ wmma_float8 d = __zluda_wmma_f32_16x16x16_f16(std::bit_cast<wmma_half16>(a_t), std::bit_cast<wmma_half16>(b_t), c);
+ f16x2 d01{static_cast<f16>(d[0]), static_cast<f16>(d[1])};
+ f16x2 d23{static_cast<f16>(d[2]), static_cast<f16>(d[3])};
+ f16x2 d45{static_cast<f16>(d[4]), static_cast<f16>(d[5])};
+ f16x2 d67{static_cast<f16>(d[6]), static_cast<f16>(d[7])};
+ return {std::bit_cast<uint32_t>(d01), std::bit_cast<uint32_t>(d23),
+ std::bit_cast<uint32_t>(d45), std::bit_cast<uint32_t>(d67)};
+ }
+
+ // D = round_f16(A B + C) with C and D in the transposed WMMA layout.
+ uint2::Native_vec_ FUNC(d4r_wmma_t_mma_f16)(wmma_uint8 a_t, wmma_uint8 b_t, uint2::Native_vec_ c_t)
+ {
+ f16x2 c01 = std::bit_cast<f16x2>(c_t.x);
+ f16x2 c23 = std::bit_cast<f16x2>(c_t.y);
+ wmma_float8 c = {float(c01.x), float(c01.y), float(c23.x), float(c23.y), 0.0f, 0.0f, 0.0f, 0.0f};
+ wmma_float8 d = __zluda_wmma_f32_16x16x16_f16(std::bit_cast<wmma_half16>(a_t), std::bit_cast<wmma_half16>(b_t), c);
+ f16x2 d01{static_cast<f16>(d[0]), static_cast<f16>(d[1])};
+ f16x2 d23{static_cast<f16>(d[2]), static_cast<f16>(d[3])};
+ return {std::bit_cast<uint32_t>(d01), std::bit_cast<uint32_t>(d23)};
+ }
+
+ // NVIDIA accumulator fragment (lane 4g+t: D[g][2t..2t+1] in .x,
+ // D[g+8][2t..2t+1] in .y) -> transposed WMMA layout.
+ uint2::Native_vec_ FUNC(d4r_wmma_t_from_nv)(uint2::Native_vec_ c)
+ {
+ uint32_t lane = wmma_lane();
+ uint32_t m = lane % 16;
+ uint32_t shift = (lane / 16) * 16;
+ uint32_t source = (m % 8) * 4;
+ uint32_t halves[4];
+ for (uint32_t v = 0; v < 4; v++)
+ {
+ uint32_t low_rows = wmma_from_lane(source + v, c.x);
+ uint32_t high_rows = wmma_from_lane(source + v, c.y);
+ halves[v] = ((m >= 8 ? high_rows : low_rows) >> shift) & 0xffffu;
+ }
+ return {halves[0] | (halves[1] << 16), halves[2] | (halves[3] << 16)};
+ }
+
+ // Transposed WMMA layout -> NVIDIA accumulator fragment.
+ uint2::Native_vec_ FUNC(d4r_wmma_t_to_nv)(uint2::Native_vec_ d)
+ {
+ uint32_t lane = wmma_lane();
+ uint32_t g = lane / 4;
+ uint32_t t = lane % 4;
+ uint32_t sources[4] = {g, g + 16, g + 8, g + 24}; // D[g][2t], D[g][2t+1], D[g+8][2t], D[g+8][2t+1]
+ uint32_t halves[4];
+ for (uint32_t i = 0; i < 4; i++)
+ {
+ uint32_t v01 = wmma_from_lane(sources[i], d.x);
+ uint32_t v23 = wmma_from_lane(sources[i], d.y);
+ halves[i] = ((t >= 2 ? v23 : v01) >> ((t % 2) * 16)) & 0xffffu;
+ }
+ return {halves[0] | (halves[1] << 16), halves[2] | (halves[3] << 16)};
+ }
+}
+
template <typename T, typename A, typename B, int BOffset, typename OcklFunc>
__device__ T dp2a_impl(T a, T b, T c, OcklFunc ockl_func)
{
diff --git a/zluda_common/examples/d4r_fatbin_ptx.rs b/zluda_common/examples/d4r_fatbin_ptx.rs
new file mode 100644
index 0000000..50f7a09
--- /dev/null
+++ b/zluda_common/examples/d4r_fatbin_ptx.rs
@@ -0,0 +1,36 @@
+// Extracts (and decompresses) every PTX file from captured CUDA fatbin images.
+// usage: d4r_fatbin_ptx <outdir> <image>...
+use cuda_types::dark_api::FatbinFileHeader;
+use std::{env, fs, path::PathBuf};
+use zluda_common::{CodeLibraryRef, CodeModuleRef};
+
+fn main() {
+ let mut args = env::args().skip(1);
+ let outdir = PathBuf::from(args.next().expect("output directory"));
+ fs::create_dir_all(&outdir).expect("create output directory");
+ for path in args {
+ let mut bytes = fs::read(&path).expect("read image");
+ bytes.extend_from_slice(&[0; 16]);
+ let stem = PathBuf::from(&path).file_stem().unwrap().to_string_lossy().into_owned();
+ let library = unsafe { CodeLibraryRef::try_load(bytes.as_ptr().cast()) }.expect("load image");
+ let mut count = 0;
+ unsafe {
+ library.iterate_modules(|_, module| {
+ let text = match module {
+ Ok(CodeModuleRef::Text(ptx)) => Some(ptx.to_string()),
+ Ok(CodeModuleRef::File(file)) if file.header.kind == FatbinFileHeader::HEADER_KIND_PTX => file
+ .get_or_decompress_content(true)
+ .ok()
+ .map(|content| String::from_utf8_lossy(&content).trim_end_matches('\0').to_string()),
+ _ => None,
+ };
+ if let Some(text) = text {
+ let name = outdir.join(format!("{stem}-{count:02}.ptx"));
+ fs::write(&name, text).expect("write PTX");
+ count += 1;
+ }
+ })
+ };
+ println!("{path}: {count} PTX modules");
+ }
+}
File diff suppressed because it is too large. Load diff