"""Experimental Metal 4 online-softmax attention with configurable Q/K tiles. Dense, noncausal, batch-one, equal-head attention only. No approximated sparsity. Not selected by the runtime unless full-pipeline validation supports a speed win. """ from __future__ import annotations import argparse import gc import json from functools import lru_cache from pathlib import Path import mlx.core as mx from h3_mlx.benchmark import error, measure from h3_mlx.ops import attention from h3_mlx.runtime import configure_memory, environment @lru_cache(maxsize=None) def build(bq, bk, groups): return mx.fast.metal_kernel( name=f"h3_flash_{bq}_{bk}_{groups}", input_names=["q", "k", "v"], output_names=["out"], ensure_row_contiguous=False, header=""" #include #include using namespace mpp::tensor_ops; """, source=r""" const int N = q_shape[0], H = q_shape[1], D = 128; int sg = thread_position_in_threadgroup.x / 32; int row = (threadgroup_position_in_grid.x * SG + sg) * BQ; int head = threadgroup_position_in_grid.y; // SIMD groups own independent query tiles; the last group may be empty. auto Q = tensor(const_cast(q) + head*q_strides[1], dextents(D,N), array{1,int(q_strides[0])}); auto K = tensor(const_cast(k) + head*k_strides[1], dextents(D,N), array{1,int(k_strides[0])}); auto V = tensor(const_cast(v) + head*v_strides[1], dextents(D,N), array{1,int(v_strides[0])}); auto O = tensor(out + head*D, dextents(D,N), array{1,H*D}); if (row >= N) return; constexpr auto qkdesc = matmul2d_descriptor(BQ,BK,128,false,true,false); constexpr auto pvdesc = matmul2d_descriptor(BQ,128,BK,false,false,false,matmul2d_descriptor::mode::multiply_accumulate); matmul2d qkop; matmul2d pvop; auto qtile = Q.template slice<128,dynamic_extent>(0,row); auto ktile = K.template slice<128,dynamic_extent>(0,0); auto vt = V.template slice<128,dynamic_extent>(0,0); auto vtile = V.template slice<128,dynamic_extent>(0,0); auto qs = qkop.template get_left_input_cooperative_tensor(); qs.load(qtile); auto scores = qkop.template get_destination_cooperative_tensor(); auto maximum = qkop.template get_row_reduction_destination_cooperative_tensor(); auto denom = maximum; auto tile_max = maximum; auto tile_sum = maximum; auto alpha = maximum; auto prob = pvop.template get_left_input_cooperative_tensor(); auto p_shape = tensor(const_cast(q), extents(), array{1,BK}); auto accum = pvop.template get_destination_cooperative_tensor(); #pragma unroll for (int i=0;i(), array{1,BK}); for (int start=0;start(0,start); qkop.run(qs,kk,scores); #pragma unroll for (auto it=scores.begin();it!=scores.end();++it) { auto coord = scores.get_multidimensional_index(it); *it = start+coord[0](0,start); pvop.run(prob,vv,accum); } #pragma unroll for (auto it=accum.begin();it!=accum.end();++it) *it /= *denom.map_iterator(it); #pragma unroll for (auto it=accum.begin();it!=accum.end();++it) { if (accum.is_valid_element(it)) { auto coord=accum.get_multidimensional_index(it); if (row+coord[1]