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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions src/enzyme_ad/jax/Implementations/TritonAutoDiffOpInterfaceImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@

#include "Enzyme/MLIR/Implementations/CoreDialectsAutoDiffImplementations.h"
#include "Enzyme/MLIR/Interfaces/AutoDiffOpInterface.h"
#include "Enzyme/MLIR/Interfaces/AutoDiffTypeInterface.h"
#include "Enzyme/MLIR/Interfaces/GradientUtils.h"
#include "Enzyme/MLIR/Interfaces/GradientUtilsReverse.h"

#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/IR/IRMapping.h"

#include "triton/Dialect/Triton/IR/Dialect.h"

#include "src/enzyme_ad/jax/Implementations/XLADerivatives.h"
Expand Down Expand Up @@ -86,6 +90,201 @@ class AutoDiffTritonFuncFunctionInterface
}
};

/// Forward mode autodiff for Triton ReduceOp.
///
/// Triton reduce syntax:
/// %out = "tt.reduce"(%input) <{axis = 0 : i32}> ({
/// ^bb0(%arg0: f32, %arg1: f32):
/// %res = arith.addf %arg0, %arg1 : f32
/// tt.reduce.return %res : f32
/// }) : (tensor<64xf32>) -> f32
///
/// Key differences from StableHLO:
/// - No init_value operand (Triton infers identity from combiner)
/// - Block args are scalars (f32), not rank-0 tensors
/// - Single axis attribute, not array of dimensions
/// - Result can be scalar when reducing all dimensions
///
/// For sum reduction: d(sum(x)) = sum(dx)
/// For max/min: derivative flows through the selected element
class AutoDiffTritonReduceFwd
: public AutoDiffOpInterface::ExternalModel<AutoDiffTritonReduceFwd,
triton::ReduceOp> {
public:
LogicalResult createForwardModeTangent(Operation *orig, OpBuilder &builder,
MGradientUtils *gutils) const {
auto reduce = cast<triton::ReduceOp>(orig);
auto &body = reduce.getCombineOp().front();

// Get the inner combiner operation (skip terminator)
if (body.getOperations().size() != 2) {
return orig->emitError()
<< "Triton reduce autodiff only supports single-op combiners";
}
Operation &innerOp = body.front();

// For max/min reductions, use control flow handler
if (isa<arith::MaxNumFOp, arith::MinNumFOp, arith::MaximumFOp,
arith::MinimumFOp>(innerOp)) {
llvm::SmallDenseSet<unsigned> operandPositionsToShadow;
llvm::SmallDenseSet<unsigned> resultPositionsToShadow;
for (unsigned i = 0; i < orig->getNumOperands(); i++) {
if (!gutils->isConstantValue(orig->getOperand(i)))
operandPositionsToShadow.insert(i);
}
for (unsigned i = 0; i < orig->getNumResults(); i++) {
if (!gutils->isConstantValue(orig->getResult(i)))
resultPositionsToShadow.insert(i);
}
return mlir::enzyme::detail::controlFlowForwardHandler(
orig, builder, gutils, operandPositionsToShadow,
resultPositionsToShadow);
}

// Only support sum (addf/addi) reductions
if (!isa<arith::AddFOp, arith::AddIOp>(innerOp)) {
return orig->emitError()
<< "Triton reduce autodiff only supports add/max/min combiners, "
"got: "
<< innerOp.getName();
}

// For sum: d(sum(x)) = sum(dx)
// Get the primal operation
Operation *primal = gutils->getNewFromOriginal(orig);

// Clone the reduce with shadow inputs
IRMapping map;
for (Value input : reduce.getSrcs()) {
if (gutils->isConstantValue(input)) {
// Create zero shadow for constant inputs
auto iface = dyn_cast<AutoDiffTypeInterface>(input.getType());
if (!iface) {
return orig->emitError()
<< "Cannot differentiate reduce with non-differentiable type";
}
map.map(input, iface.createNullValue(builder, input.getLoc()));
} else {
map.map(input, gutils->invertPointerM(input, builder));
}
}

// Clone entire op with remapped inputs (this correctly handles the region)
Operation *shadowReduce = builder.clone(*orig, map);

// Clean up: Enzyme may have added shadow block arguments to the primal.
// For sum reduction, we don't need shadows in the combiner body.
// Remove inverted pointers for the inner op result and block arguments.
auto invInnerResult = gutils->invertedPointers.lookup(innerOp.getResult(0));
gutils->invertedPointers.erase(innerOp.getResult(0));
if (invInnerResult && invInnerResult.getDefiningOp())
gutils->erase(invInnerResult.getDefiningOp());

// Remove shadow block arguments from primal
auto primalReduce = cast<triton::ReduceOp>(primal);
auto &primalBody = primalReduce.getCombineOp().front();
BitVector baToErase(primalBody.getNumArguments());
for (auto ba : body.getArguments()) {
auto invBA = gutils->invertedPointers.lookup(ba);
if (invBA) {
gutils->invertedPointers.erase(ba);
if (auto invBABlock = dyn_cast<BlockArgument>(invBA)) {
baToErase.set(invBABlock.getArgNumber());
}
}
}
primalBody.eraseArguments(baToErase);

// Set derivatives for results
for (auto [origRes, shadowRes] :
llvm::zip(orig->getResults(), shadowReduce->getResults())) {
if (!gutils->isConstantValue(origRes)) {
gutils->setDiffe(origRes, shadowRes, builder);
}
}

gutils->eraseIfUnused(orig);
return success();
}
};

/// Forward mode autodiff for Triton DotOp.
///
/// tt.dot computes: result = a @ b + c
/// Forward mode: d(result) = da @ b + a @ db + dc
///
/// We chain dot operations using the accumulator:
/// shadow = dc (or zero if c is constant)
/// if a is active: shadow = dot(da, b, shadow)
/// if b is active: shadow = dot(a, db, shadow)
class AutoDiffTritonDotFwd

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern could be implemented in tablegen, it may need to specify an unimplemented rule for the reverse

: public AutoDiffOpInterface::ExternalModel<AutoDiffTritonDotFwd,
triton::DotOp> {
public:
LogicalResult createForwardModeTangent(Operation *orig, OpBuilder &builder,
MGradientUtils *gutils) const {
auto dot = cast<triton::DotOp>(orig);
Value a = dot.getA();
Value b = dot.getB();
Value c = dot.getC();

Value newA = gutils->getNewFromOriginal(a);
Value newB = gutils->getNewFromOriginal(b);

Value shadow;
if (!gutils->isConstantValue(c)) {
shadow = gutils->invertPointerM(c, builder);
} else {
auto iface = cast<AutoDiffTypeInterface>(orig->getResult(0).getType());
shadow = iface.createNullValue(builder, orig->getLoc());
}

if (!gutils->isConstantValue(a)) {
Value shadowA = gutils->invertPointerM(a, builder);
IRMapping map;
map.map(a, shadowA);
map.map(b, newB);
map.map(c, shadow);
shadow = builder.clone(*orig, map)->getResult(0);
}

if (!gutils->isConstantValue(b)) {
Value shadowB = gutils->invertPointerM(b, builder);
IRMapping map;
map.map(a, newA);
map.map(b, shadowB);
map.map(c, shadow);
shadow = builder.clone(*orig, map)->getResult(0);
}

Value origResult = orig->getResult(0);
if (!gutils->isConstantValue(origResult)) {
gutils->setDiffe(origResult, shadow, builder);
}

gutils->eraseIfUnused(orig);
return success();
}
};

/// Control flow interface for Triton ReduceOp (needed for max/min)
class AutoDiffTritonReduceCF
: public ControlFlowAutoDiffOpInterface::ExternalModel<
AutoDiffTritonReduceCF, triton::ReduceOp> {
public:
Operation *createWithShadows(Operation *op, OpBuilder &builder,
MGradientUtils *gutils, Operation *original,
ValueRange remappedOperands,
TypeRange rettys) const {
auto reduce = cast<triton::ReduceOp>(original);
auto newOp = builder.create<triton::ReduceOp>(
original->getLoc(), remappedOperands, reduce.getAxis());
IRMapping mapping;
reduce.getCombineOp().cloneInto(&newOp.getCombineOp(), mapping);
return newOp;
}
};

} // end anonymous namespace

void mlir::enzyme::registerTritonDialectAutoDiffInterface(
Expand All @@ -95,5 +294,8 @@ void mlir::enzyme::registerTritonDialectAutoDiffInterface(
triton::FuncOp::attachInterface<AutoDiffTritonFuncFunctionInterface>(
*context);
triton::PointerType::attachInterface<TritonPointerTypeInterface>(*context);
triton::DotOp::attachInterface<AutoDiffTritonDotFwd>(*context);
triton::ReduceOp::attachInterface<AutoDiffTritonReduceFwd>(*context);
triton::ReduceOp::attachInterface<AutoDiffTritonReduceCF>(*context);
});
}
10 changes: 10 additions & 0 deletions src/enzyme_ad/jax/Implementations/TritonDerivatives.td
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ def MakeRange : TritonInst<"MakeRangeOp">;
def : TritonReturnOp<"ReturnOp">;

def : TritonInactiveOp<"AssertOp">;
def : TritonInactiveOp<"GetProgramIdOp">;
def : TritonInactiveOp<"MakeRangeOp">;
def : TritonInactiveOp<"PrintOp">;

def : ReadOnlyIdentityOp<"triton", "AddPtrOp", [0]>;
def : ReadOnlyIdentityOp<"triton", "AdvanceOp", [0]>;
def : ReadOnlyIdentityOp<"triton", "BroadcastOp", [0]>;
def : ReadOnlyIdentityOp<"triton", "ExpandDimsOp", [0]>;
def : ReadOnlyIdentityOp<"triton", "LoadOp", [0]>;
def : ReadOnlyIdentityOp<"triton", "SplatOp", [0]>;
def : MemoryIdentityOp<"triton", "StoreOp", [1], [0]>;
Expand All @@ -38,3 +42,9 @@ def : TritonDerivative<"FpToFpOp", (Op $x),
(FpToFp (TypeOf $x), (DiffeRet), (FpToFpRoundingMode))
]
>;

// Note: ReduceOp is handled via custom C++ AutoDiffOpInterface in
// TritonAutoDiffOpInterfaceImpl.cpp because Triton's ReduceOp doesn't
// implement RegionBranchOpInterface required by ControlFlowOp.

def : RegionTerminatorOp<"triton", "ReduceReturnOp">;
115 changes: 115 additions & 0 deletions test/lit_tests/diffrules/triton/matmul.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// RUN: enzymexlamlir-opt %s --enzyme-wrap="infn=matmul_kernel outfn= argTys=enzyme_dup,enzyme_dup,enzyme_dup,enzyme_const,enzyme_const,enzyme_const,enzyme_const,enzyme_const,enzyme_const retTys= mode=ForwardMode" --canonicalize | FileCheck %s

module {
tt.func public @matmul_kernel(%arg0: !tt.ptr<f32> {tt.divisibility = 16 : i32}, %arg1: !tt.ptr<f32> {tt.divisibility = 16 : i32},
%arg2: !tt.ptr<f16> {tt.divisibility = 16 : i32}, %arg3: i32, %arg4: i32,
%arg5: i32, %arg6: i32, %arg7: i32, %arg8: i32) attributes {noinline = false} {
%c31_i32 = arith.constant 31 : i32
%c63_i32 = arith.constant 63 : i32
%c1_i32 = arith.constant 1 : i32
%cst = arith.constant dense<32> : tensor<64x32xi32>
%cst_0 = arith.constant dense<0.000000e+00> : tensor<32x32xf32>
%cst_1 = arith.constant dense<0.000000e+00> : tensor<64x32xf32>
%c32_i32 = arith.constant 32 : i32
%c64_i32 = arith.constant 64 : i32
%true = arith.constant true
%c0_i32 = arith.constant 0 : i32
%c8_i32 = arith.constant 8 : i32
%0 = tt.get_program_id x : i32
%1 = arith.addi %arg3, %c63_i32 : i32
%2 = arith.divsi %1, %c64_i32 : i32
%3 = arith.addi %arg4, %c31_i32 : i32
%4 = arith.divsi %3, %c32_i32 : i32
%5 = arith.muli %4, %c8_i32 : i32
%6 = arith.divsi %0, %5 : i32
%7 = arith.muli %6, %c8_i32 : i32
%8 = arith.subi %2, %7 : i32
%9 = arith.minsi %8, %c8_i32 : i32
%10 = arith.remsi %0, %5 : i32
%11 = arith.remsi %10, %9 : i32
%12 = arith.addi %7, %11 : i32
%13 = arith.divsi %10, %9 : i32
%14 = arith.cmpi sge, %12, %c0_i32 : i32
llvm.intr.assume %14 : i1
%15 = arith.cmpi sge, %13, %c0_i32 : i32
llvm.intr.assume %15 : i1
%16 = arith.cmpi sgt, %arg6, %c0_i32 : i32
llvm.intr.assume %16 : i1
llvm.intr.assume %true : i1
llvm.intr.assume %true : i1
%17 = arith.cmpi sgt, %arg7, %c0_i32 : i32
llvm.intr.assume %17 : i1
%18 = arith.cmpi sgt, %arg8, %c0_i32 : i32
llvm.intr.assume %18 : i1
llvm.intr.assume %true : i1
%19 = arith.muli %12, %c64_i32 : i32
%20 = tt.make_range {end = 64 : i32, start = 0 : i32} : tensor<64xi32>
%21 = tt.splat %19 : i32 -> tensor<64xi32>
%22 = arith.addi %21, %20 : tensor<64xi32>
%23 = tt.splat %arg3 : i32 -> tensor<64xi32>
%24 = arith.remsi %22, %23 : tensor<64xi32>
%25 = arith.muli %13, %c32_i32 : i32
%26 = tt.make_range {end = 32 : i32, start = 0 : i32} : tensor<32xi32>
%27 = tt.splat %25 : i32 -> tensor<32xi32>
%28 = arith.addi %27, %26 : tensor<32xi32>
%29 = tt.splat %arg4 : i32 -> tensor<32xi32>
%30 = arith.remsi %28, %29 : tensor<32xi32>
%31 = tt.expand_dims %24 {axis = 1 : i32} : tensor<64xi32> -> tensor<64x1xi32>
%32 = tt.splat %arg6 : i32 -> tensor<64x1xi32>
%33 = arith.muli %31, %32 : tensor<64x1xi32>
%34 = tt.expand_dims %26 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32>
%35 = tt.broadcast %33 : tensor<64x1xi32> -> tensor<64x32xi32>
%36 = tt.broadcast %34 : tensor<1x32xi32> -> tensor<64x32xi32>
%37 = arith.addi %35, %36 : tensor<64x32xi32>
%38 = tt.splat %arg0 : !tt.ptr<f32> -> tensor<64x32x!tt.ptr<f32>>
%39 = tt.addptr %38, %37 : tensor<64x32x!tt.ptr<f32>>, tensor<64x32xi32>
%40 = tt.expand_dims %26 {axis = 1 : i32} : tensor<32xi32> -> tensor<32x1xi32>
%41 = tt.splat %arg7 : i32 -> tensor<32x1xi32>
%42 = arith.muli %40, %41 : tensor<32x1xi32>
%43 = tt.expand_dims %30 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32>
%44 = tt.broadcast %42 : tensor<32x1xi32> -> tensor<32x32xi32>
%45 = tt.broadcast %43 : tensor<1x32xi32> -> tensor<32x32xi32>
%46 = arith.addi %44, %45 : tensor<32x32xi32>
%47 = tt.splat %arg1 : !tt.ptr<f32> -> tensor<32x32x!tt.ptr<f32>>
%48 = tt.addptr %47, %46 : tensor<32x32x!tt.ptr<f32>>, tensor<32x32xi32>
%49 = arith.addi %arg5, %c31_i32 : i32
%50 = arith.divsi %49, %c32_i32 : i32
%51:3 = scf.for %arg9 = %c0_i32 to %50 step %c1_i32 iter_args(%arg10 = %cst_1, %arg11 = %39, %arg12 = %48) -> (tensor<64x32xf32>, tensor<64x32x!tt.ptr<f32>>, tensor<32x32x!tt.ptr<f32>>) : i32 {
%69 = arith.muli %arg9, %c32_i32 : i32
%70 = arith.subi %arg5, %69 : i32
%71 = tt.splat %70 : i32 -> tensor<1x32xi32>
%72 = arith.cmpi slt, %34, %71 : tensor<1x32xi32>
%73 = tt.broadcast %72 : tensor<1x32xi1> -> tensor<64x32xi1>
%74 = tt.load %arg11, %73, %cst_1 : tensor<64x32x!tt.ptr<f32>>
%75 = tt.splat %70 : i32 -> tensor<32x1xi32>
%76 = arith.cmpi slt, %40, %75 : tensor<32x1xi32>
%77 = tt.broadcast %76 : tensor<32x1xi1> -> tensor<32x32xi1>
%78 = tt.load %arg12, %77, %cst_0 : tensor<32x32x!tt.ptr<f32>>
%79 = tt.dot %74, %78, %arg10, inputPrecision = tf32 : tensor<64x32xf32> * tensor<32x32xf32> -> tensor<64x32xf32>
%80 = tt.addptr %arg11, %cst : tensor<64x32x!tt.ptr<f32>>, tensor<64x32xi32>
%81 = arith.muli %arg7, %c32_i32 : i32
%82 = tt.splat %81 : i32 -> tensor<32x32xi32>
%83 = tt.addptr %arg12, %82 : tensor<32x32x!tt.ptr<f32>>, tensor<32x32xi32>
scf.yield %79, %80, %83 : tensor<64x32xf32>, tensor<64x32x!tt.ptr<f32>>, tensor<32x32x!tt.ptr<f32>>
}
%52 = arith.truncf %51#0 : tensor<64x32xf32> to tensor<64x32xf16>
%53 = tt.expand_dims %22 {axis = 1 : i32} : tensor<64xi32> -> tensor<64x1xi32>
%54 = tt.splat %arg8 : i32 -> tensor<64x1xi32>
%55 = arith.muli %54, %53 : tensor<64x1xi32>
%56 = tt.splat %arg2 : !tt.ptr<f16> -> tensor<64x1x!tt.ptr<f16>>
%57 = tt.addptr %56, %55 : tensor<64x1x!tt.ptr<f16>>, tensor<64x1xi32>
%58 = tt.expand_dims %28 {axis = 0 : i32} : tensor<32xi32> -> tensor<1x32xi32>
%59 = tt.broadcast %57 : tensor<64x1x!tt.ptr<f16>> -> tensor<64x32x!tt.ptr<f16>>
%60 = tt.broadcast %58 : tensor<1x32xi32> -> tensor<64x32xi32>
%61 = tt.addptr %59, %60 : tensor<64x32x!tt.ptr<f16>>, tensor<64x32xi32>
%62 = tt.splat %arg3 : i32 -> tensor<64x1xi32>
%63 = arith.cmpi slt, %53, %62 : tensor<64x1xi32>
%64 = tt.splat %arg4 : i32 -> tensor<1x32xi32>
%65 = arith.cmpi slt, %58, %64 : tensor<1x32xi32>
%66 = tt.broadcast %63 : tensor<64x1xi1> -> tensor<64x32xi1>
%67 = tt.broadcast %65 : tensor<1x32xi1> -> tensor<64x32xi1>
%68 = arith.andi %66, %67 : tensor<64x32xi1>
tt.store %61, %52, %68 : tensor<64x32x!tt.ptr<f16>>
tt.return
}
}
Loading
Loading