Skip to content

Commit e6563a6

Browse files
committed
ROCm backend: arch-tunable QMV, WMMA flash attention, arena allocator, SliceUpdate donation
Arch detection and tuning: - RocmArchTier enum: RDNA 2/3/3.5/4/CDNA with fine-grained gfx detection - HWInfo struct: CU count, SIMDs, L2 size, WMMA capability from hipDeviceProp - ArchTuning: per-arch kernel parameters (QMV tile_n, crossover thresholds) - Runtime TILE_N for qmv_tiled_kernel via kernel argument (no template bloat) - MLX_ROCM_QMV_TILE_N env var for manual tuning WMMA flash attention: - flash_attention_wmma.hip: rocwmma 16x16x16 tiled kernel for bf16/fp16 - Dispatches for prefill (qL > 4) on supported head dims (64/128/256) - Integrated into ScaledDotProductAttention dispatch Arena allocator (DecodeArena): - Deterministic bump allocator for HIP Graph capture - Hooked into RocmAllocator malloc/free path - Proven: 18 KB per decode step with stable addresses SliceUpdate donation: - Skip base array copy when input has unique ownership (refcount==1) - Helps prefill path (200 donated during prompt processing) GPU memcpy: - mlx_gpu_memcpy_async (extern C) for direct KV cache writes - gpu_arena/gpu_graph wrapper functions for engine integration
1 parent 6b3713e commit e6563a6

10 files changed

Lines changed: 806 additions & 91 deletions

File tree

mlx/backend/rocm/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ set(HIP_SOURCES
140140
${CMAKE_CURRENT_SOURCE_DIR}/rope.hip
141141
${CMAKE_CURRENT_SOURCE_DIR}/scaled_dot_product_attention.hip
142142
${CMAKE_CURRENT_SOURCE_DIR}/flash_attention.hip
143+
${CMAKE_CURRENT_SOURCE_DIR}/flash_attention_wmma.hip
143144
${CMAKE_CURRENT_SOURCE_DIR}/scan.hip
144145
${CMAKE_CURRENT_SOURCE_DIR}/softmax.hip
145146
${CMAKE_CURRENT_SOURCE_DIR}/sort.hip

mlx/backend/rocm/allocator.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,13 @@ Buffer RocmAllocator::malloc(size_t size) {
357357
"Please use CPU backend instead.");
358358
}
359359

360+
// Arena fast path: deterministic bump allocation for HIP Graph capture
361+
if (arena_.active()) {
362+
RocmBuffer* buf = arena_.malloc(size);
363+
if (buf) return Buffer{buf};
364+
// Arena exhausted — fall through to normal path
365+
}
366+
360367
auto orig_size = size;
361368
std::unique_lock lock(mutex_);
362369

@@ -433,6 +440,12 @@ void RocmAllocator::free(Buffer buffer) {
433440
return;
434441
}
435442

443+
// Arena fast path: no-op (memory freed in bulk on arena.end())
444+
if (arena_.active()) {
445+
arena_.free(buf);
446+
return;
447+
}
448+
436449
std::unique_lock lock(mutex_);
437450
active_memory_ -= buf->size;
438451

@@ -530,6 +543,77 @@ void RocmAllocator::clear_cache() {
530543
buffer_cache_.clear();
531544
}
532545

546+
// ---------------------------------------------------------------------------
547+
// DecodeArena implementation
548+
// ---------------------------------------------------------------------------
549+
550+
DecodeArena::~DecodeArena() {
551+
end();
552+
}
553+
554+
bool DecodeArena::begin(size_t capacity_bytes) {
555+
if (base_) end();
556+
557+
// Align capacity to page boundary
558+
capacity_bytes = (capacity_bytes + 4095) & ~size_t(4095);
559+
560+
bool managed = false;
561+
void* data = nullptr;
562+
try {
563+
data = rocm_unified_malloc(capacity_bytes, managed);
564+
} catch (...) {
565+
return false;
566+
}
567+
568+
base_ = data;
569+
capacity_ = capacity_bytes;
570+
offset_ = 0;
571+
is_managed_ = managed;
572+
desc_index_ = 0;
573+
descriptors_.clear();
574+
descriptors_.reserve(512); // Typical decode step has ~300 allocations
575+
return true;
576+
}
577+
578+
void DecodeArena::reset() {
579+
offset_ = 0;
580+
desc_index_ = 0;
581+
}
582+
583+
void DecodeArena::end() {
584+
if (!base_) return;
585+
rocm_unified_free(base_, is_managed_);
586+
base_ = nullptr;
587+
capacity_ = 0;
588+
offset_ = 0;
589+
descriptors_.clear();
590+
desc_index_ = 0;
591+
}
592+
593+
RocmBuffer* DecodeArena::malloc(size_t size) {
594+
if (!base_) return nullptr;
595+
596+
// Align to 256 bytes for GPU access patterns
597+
size_t aligned = (size + 255) & ~size_t(255);
598+
if (offset_ + aligned > capacity_) return nullptr;
599+
600+
void* ptr = static_cast<char*>(base_) + offset_;
601+
offset_ += aligned;
602+
603+
// Reuse or create a RocmBuffer descriptor
604+
if (desc_index_ < descriptors_.size()) {
605+
auto& d = descriptors_[desc_index_];
606+
d.data = ptr;
607+
d.size = size;
608+
desc_index_++;
609+
return &d;
610+
}
611+
612+
descriptors_.push_back(RocmBuffer{ptr, size, is_managed_, -1});
613+
desc_index_++;
614+
return &descriptors_.back();
615+
}
616+
533617
RocmAllocator& allocator() {
534618
static RocmAllocator* allocator_ = new RocmAllocator;
535619
return *allocator_;

mlx/backend/rocm/allocator.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,55 @@ class SlabAllocator {
9292
SizeClassPool pools_[kNumSizeClasses];
9393
};
9494

95+
// ---------------------------------------------------------------------------
96+
// DecodeArena — deterministic bump allocator for HIP Graph capture
97+
// ---------------------------------------------------------------------------
98+
// During decode, the allocation pattern is fixed: same sizes in the same
99+
// order every step. The arena allocates from a pre-sized contiguous buffer,
100+
// guaranteeing identical pointers on each reset+replay cycle.
101+
//
102+
// Usage:
103+
// arena.begin(estimated_bytes); // allocate backing buffer
104+
// // ... run decode step (allocations go through arena) ...
105+
// arena.reset(); // rewind bump pointer for next step
106+
// // ... replay same step (same pointers) ...
107+
// arena.end(); // release backing buffer
108+
109+
class DecodeArena {
110+
public:
111+
DecodeArena() = default;
112+
~DecodeArena();
113+
114+
// Allocate the backing buffer and enter arena mode.
115+
bool begin(size_t capacity_bytes);
116+
117+
// Rewind the bump pointer. Next cycle returns same addresses.
118+
void reset();
119+
120+
// Leave arena mode and free the backing buffer.
121+
void end();
122+
123+
// Bump-allocate from the arena. Returns nullptr if inactive or exhausted.
124+
RocmBuffer* malloc(size_t size);
125+
126+
// No-op free (bulk-freed on end()).
127+
void free(RocmBuffer* /*buf*/) {}
128+
129+
bool active() const { return base_ != nullptr; }
130+
size_t used() const { return offset_; }
131+
size_t capacity() const { return capacity_; }
132+
133+
private:
134+
void* base_{nullptr};
135+
size_t capacity_{0};
136+
size_t offset_{0};
137+
bool is_managed_{false};
138+
139+
// Pre-allocated RocmBuffer descriptors (recycled on reset)
140+
std::vector<RocmBuffer> descriptors_;
141+
size_t desc_index_{0};
142+
};
143+
95144
// ---------------------------------------------------------------------------
96145
// RocmAllocator
97146
// ---------------------------------------------------------------------------
@@ -126,6 +175,14 @@ class RocmAllocator : public allocator::Allocator {
126175
size_t active_memory_{0};
127176
size_t peak_memory_{0};
128177
SlabAllocator slab_allocator_;
178+
179+
public:
180+
// Arena mode for HIP Graph capture.
181+
// When active, malloc() returns deterministic addresses from the arena.
182+
DecodeArena& arena() { return arena_; }
183+
184+
private:
185+
DecodeArena arena_;
129186
};
130187

131188
RocmAllocator& allocator();

mlx/backend/rocm/device/config.h

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,83 @@ constexpr int kRMSNormBlockSize = 256;
8181
// Attention constants
8282
constexpr int kAttentionBlockSize = 256;
8383

84+
// ---- Architecture tier detection and per-arch kernel tuning ----
85+
//
86+
// RocmArchTier provides fine-grained GPU generation identification.
87+
// ArchTuning holds per-arch parameters for kernel dispatch decisions.
88+
// Both are usable from host code and kernel dispatch logic.
89+
90+
enum class RocmArchTier {
91+
Rdna2, // gfx10xx: RDNA 2, Wave32, no WMMA
92+
Rdna3, // gfx1100-gfx1103: RDNA 3, Wave32, WMMA, 96KB LDS
93+
Rdna35, // gfx1150-gfx1152: RDNA 3.5, Wave32, WMMA, 64KB LDS, 32MB IC
94+
Rdna4, // gfx1200-gfx1201: RDNA 4, Wave32, enhanced WMMA
95+
Cdna, // gfx9xx: MI-series, Wave64
96+
};
97+
98+
// Hardware capabilities detected at runtime from hipDeviceProp_t.
99+
struct HWInfo {
100+
RocmArchTier tier;
101+
int num_cus; // Compute units (multiProcessorCount)
102+
int simds_per_cu; // SIMDs per CU (2 for RDNA, 4 for CDNA)
103+
int max_threads_per_cu; // Max resident threads per CU
104+
int shared_mem_per_cu; // Shared/LDS memory per CU in bytes
105+
int l2_cache_bytes; // L2/Infinity Cache size
106+
bool has_wmma; // WMMA/tensor core support
107+
};
108+
109+
// Per-architecture tuning parameters for quantized matvec and attention kernels.
110+
struct ArchTuning {
111+
// QMV tiled kernel
112+
int qmv_tile_n; // Output columns per block (L2 reuse)
113+
// QMV↔GEMM crossover M thresholds
114+
int qmv_crossover_small; // For K<=2048, N<=2048
115+
int qmv_crossover_medium; // For K<=4096, N<=4096
116+
int qmv_crossover_large; // For larger shapes
117+
// Flash attention
118+
int fa_block_m; // Queries per flash attention block
119+
int fa_block_n; // Keys per iteration
120+
};
121+
122+
// Auto-tune based on detected hardware. Adjusts tile sizes based on actual
123+
// CU count to balance occupancy vs L2 reuse.
124+
inline ArchTuning get_arch_tuning(RocmArchTier tier) {
125+
// Defaults per tier — used when HWInfo isn't available
126+
switch (tier) {
127+
case RocmArchTier::Rdna2:
128+
return ArchTuning{8, 28, 20, 14, 128, 64};
129+
case RocmArchTier::Rdna3:
130+
return ArchTuning{16, 36, 24, 16, 64, 64};
131+
case RocmArchTier::Rdna35:
132+
// 40 CUs: TILE_N=16 gives best occupancy/reuse balance
133+
return ArchTuning{16, 36, 24, 16, 64, 64};
134+
case RocmArchTier::Rdna4:
135+
return ArchTuning{32, 40, 28, 18, 64, 64};
136+
case RocmArchTier::Cdna:
137+
default:
138+
return ArchTuning{16, 20, 14, 10, 128, 64};
139+
}
140+
}
141+
142+
// Auto-tune using full hardware info. Adjusts TILE_N based on CU count:
143+
// fewer CUs → larger tiles for more L2 reuse per block.
144+
inline ArchTuning get_arch_tuning(const HWInfo& hw) {
145+
auto t = get_arch_tuning(hw.tier);
146+
147+
// Auto-tune QMV tile_n based on CU count.
148+
// Benchmarking shows TILE_N=16 is optimal for RDNA 3/3.5 regardless
149+
// of CU count — TILE_N=32 creates 1024-thread blocks that reduce
150+
// occupancy. Only go to 8 for very low CU counts.
151+
if (hw.tier == RocmArchTier::Rdna3 || hw.tier == RocmArchTier::Rdna35 ||
152+
hw.tier == RocmArchTier::Rdna4) {
153+
if (hw.num_cus <= 16) {
154+
t.qmv_tile_n = 8; // Very small APU: maximize occupancy
155+
} else {
156+
t.qmv_tile_n = 16; // All other RDNA 3+: best balance
157+
}
158+
}
159+
160+
return t;
161+
}
162+
84163
} // namespace mlx::core::rocm

mlx/backend/rocm/eval.cpp

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,17 @@
1111
namespace mlx::core::gpu {
1212

1313
void init() {
14-
// Force initialization of ROCm runtime
1514
hipFree(nullptr);
1615
}
1716

1817
void new_stream(Stream s) {
19-
// Force initialization of ROCm by creating an event, so the HIP runtime and
20-
// our HIP event pool get destroyed last.
2118
rocm::HipEvent(hipEventDefault);
22-
// Ensure the static stream objects get created.
2319
rocm::get_command_encoder(s);
2420
}
2521

2622
void eval(array& arr) {
2723
auto outputs = arr.outputs();
2824
{
29-
// If the array is a tracer hold a reference
30-
// to its inputs so they don't get donated
3125
std::vector<array> inputs;
3226
if (arr.is_tracer()) {
3327
inputs = arr.inputs();
@@ -36,9 +30,7 @@ void eval(array& arr) {
3630
}
3731

3832
auto& encoder = rocm::get_command_encoder(arr.primitive().stream());
39-
// Keep used buffers alive until kernel finishes running.
4033
for (auto& in : arr.inputs()) {
41-
// Except for the donated one.
4234
if (in.data_shared_ptr() != arr.data_shared_ptr()) {
4335
encoder.add_temporary(in);
4436
}
@@ -58,3 +50,35 @@ void synchronize(Stream s) {
5850
}
5951

6052
} // namespace mlx::core::gpu
53+
54+
// --- GPU memcpy for direct KV cache writes ---
55+
extern "C" void mlx_gpu_memcpy_async(void* dst, const void* src, size_t bytes) {
56+
auto& enc = mlx::core::rocm::get_command_encoder(
57+
mlx::core::default_stream(mlx::core::Device::gpu));
58+
enc.launch_kernel([=](hipStream_t stream) {
59+
(void)hipMemcpyAsync(dst, src, bytes, hipMemcpyDeviceToDevice, stream);
60+
});
61+
}
62+
63+
// --- Arena + Graph wrappers (called from engine code without HIP headers) ---
64+
namespace mlx::core {
65+
66+
bool gpu_arena_begin(size_t capacity) {
67+
return rocm::allocator().arena().begin(capacity);
68+
}
69+
void gpu_arena_reset() { rocm::allocator().arena().reset(); }
70+
void gpu_arena_end() { rocm::allocator().arena().end(); }
71+
size_t gpu_arena_used() { return rocm::allocator().arena().used(); }
72+
bool gpu_arena_active() { return rocm::allocator().arena().active(); }
73+
74+
static rocm::CommandEncoder& graph_encoder() {
75+
return rocm::get_command_encoder(default_stream(Device::gpu));
76+
}
77+
78+
bool gpu_graph_begin_capture() { graph_encoder().begin_capture(); return true; }
79+
bool gpu_graph_end_capture() { return graph_encoder().end_capture(); }
80+
bool gpu_graph_replay() { return graph_encoder().replay(); }
81+
void gpu_graph_reset() { graph_encoder().reset_graph(); }
82+
bool gpu_graph_available() { return graph_encoder().has_graph(); }
83+
84+
} // namespace mlx::core

0 commit comments

Comments
 (0)