#include "core/ggml_extensions.h" #include "runtime.h" #if __CUDACC_VER_MAJOR__ >= 12 #include #include #include #include #include "sageattention/attn.cuh" #include "sageattention/attn_fp8.cuh" static constexpr int sage_cuda_version = __CUDACC_VER_MAJOR__ * 1000 + __CUDACC_VER_MINOR__ * 10; namespace sd_cuda { struct SageDevice { int cc; int compiled_cc; }; static ggml_sage_attn_mode sage_mode(const SageDevice& device, ggml_sage_attn_mode requested) { if (requested != GGML_SAGE_ATTN_AUTO) return requested; if (sage_cuda_version >= 12040 && device.cc >= 89 && device.compiled_cc >= 89) { if (sage_cuda_version >= 12080 && device.cc != 90) return GGML_SAGE_ATTN_2_PLUS_PLUS; return GGML_SAGE_ATTN_2; } return GGML_SAGE_ATTN_AUTO; } static __device__ __forceinline__ float warp_reduce_max(float x) { #pragma unroll for (int offset = 16; offset > 0; offset >>= 1) x = fmaxf(x, __shfl_xor_sync(0xffffffff, x, offset)); return x; } static __global__ void sage_output(const half* input, float* output, int count) { const int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < count) output[i] = __half2float(input[i]); } struct SageWorkspace { size_t q; size_t k; size_t q_scale; size_t k_scale; size_t k_mean; size_t v; size_t v_scale; size_t output; size_t size; explicit SageWorkspace(const ggml_tensor* dst) { const ggml_tensor* qt = dst->src[0]; const ggml_tensor* kt = dst->src[1]; size = 0; q = reserve(ggml_nelements(qt)); k = reserve(ggml_nelements(kt)); q_scale = reserve(GGML_PAD(qt->ne[1], 128) / 32 * 8 * qt->ne[2] * qt->ne[3] * sizeof(float)); k_scale = reserve(GGML_PAD(kt->ne[1], 64) / 64 * 4 * kt->ne[2] * kt->ne[3] * sizeof(float)); k_mean = reserve(kt->ne[0] * kt->ne[2] * kt->ne[3] * sizeof(float)); v = reserve(kt->ne[0] * GGML_PAD(kt->ne[1], 64) * kt->ne[2] * kt->ne[3]); v_scale = reserve(kt->ne[0] * kt->ne[2] * kt->ne[3] * sizeof(float)); output = reserve(ggml_nelements(dst) * sizeof(half)); } size_t reserve(size_t bytes) { const size_t offset = size; size += GGML_PAD(bytes, 256); return offset; } }; template static __global__ void sage_key_mean(const float* k, float* mean, int length) { const int channel = blockIdx.x * 32 + threadIdx.x % 32; const int row = threadIdx.x / 32; const size_t base = size_t(blockIdx.y) * length * D; float sum = 0.0f; for (int token = row; token < length; token += 8) { sum += k[base + size_t(token) * D + channel]; } __shared__ float sums[256]; sums[threadIdx.x] = sum; __syncthreads(); if (row == 0) { for (int i = 1; i < 8; ++i) { sum += sums[threadIdx.x + i * 32]; } mean[size_t(blockIdx.y) * D + channel] = sum / length; } } template static __global__ void sage_quantize(const float* input, const float* mean, int8_t* output, float* scales, int length) { constexpr int VALUES = D * TOKENS / 256; const int head = blockIdx.y; const int first = blockIdx.x * TOKENS * D; const size_t base = size_t(head) * length * D; float values[VALUES]; float amax = 1e-7f; #pragma unroll for (int i = 0; i < VALUES; ++i) { const int index = first + threadIdx.x + i * 256; float value = 0.0f; if (index < length * D) { value = input[base + index]; if constexpr (SMOOTH) { value -= mean[size_t(head) * D + index % D]; } } values[i] = value; amax = fmaxf(amax, fabsf(value)); } amax = warp_reduce_max(amax); __shared__ float maxima[8]; if (threadIdx.x % 32 == 0) { maxima[threadIdx.x / 32] = amax; } __syncthreads(); if (threadIdx.x < 32) { amax = threadIdx.x < 8 ? maxima[threadIdx.x] : 1e-7f; amax = warp_reduce_max(amax); if (threadIdx.x == 0) { maxima[0] = amax; scales[size_t(head) * gridDim.x + blockIdx.x] = amax / 127.0f; } } __syncthreads(); const float inverse_scale = 127.0f / maxima[0]; #pragma unroll for (int i = 0; i < VALUES; ++i) { const int index = first + threadIdx.x + i * 256; if (index < length * D) { output[base + index] = int8_t(__float2int_rn(values[i] * inverse_scale)); } } } template static __global__ void sage_quantize_per_thread(const float* input, const float* mean, int8_t* output, float* scales, int length) { constexpr int GROUPS = KEY ? 4 : 8; constexpr int TOKENS = KEY ? 64 : 32; constexpr int VALUES = D * TOKENS / GROUPS / 32; const int group = threadIdx.x / 32; const int lane = threadIdx.x % 32; const size_t base = size_t(blockIdx.y) * length * D; float values[VALUES]; float amax = 0.0f; #pragma unroll for (int i = 0; i < VALUES; ++i) { const int index = lane + i * 32; const int row = index / D; const int token = blockIdx.x * TOKENS + (KEY ? row / 2 * 8 + group * 2 + row % 2 : row * 8 + group); float value = token < length ? input[base + size_t(token) * D + index % D] : 0.0f; if constexpr (KEY) { if (token < length) { value -= mean[size_t(blockIdx.y) * D + index % D]; } } values[i] = value; amax = fmaxf(amax, fabsf(value)); } const float scale = warp_reduce_max(amax) / 127.0f + 1e-7f; if (lane == 0) { scales[(size_t(blockIdx.y) * gridDim.x + blockIdx.x) * GROUPS + group] = scale; } #pragma unroll for (int i = 0; i < VALUES; ++i) { const int index = lane + i * 32; const int row = index / D; const int token = blockIdx.x * TOKENS + (KEY ? row / 2 * 8 + group * 2 + row % 2 : row * 8 + group); if (token < length) { const float value = values[i] / scale; output[base + size_t(token) * D + index % D] = int8_t(__float2int_rz(value + copysignf(0.5f, value))); } } } template static __global__ void sage_value_scale(const half* v, float* scales, int length, float scale_max) { const int channel = blockIdx.x * 32 + threadIdx.x % 32; const int row = threadIdx.x / 32; const size_t base = size_t(blockIdx.y) * length * D; float amax = 1e-7f; for (int token = row; token < length; token += 8) { amax = fmaxf(amax, fabsf(__half2float(v[base + size_t(token) * D + channel]))); } __shared__ float maxima[256]; maxima[threadIdx.x] = amax; __syncthreads(); if (row == 0) { for (int i = 1; i < 8; ++i) { amax = fmaxf(amax, maxima[threadIdx.x + i * 32]); } scales[size_t(blockIdx.y) * D + channel] = amax / scale_max; } } template static __global__ void sage_quantize_value(const half* v, const float* scales, int8_t* output, int length, int padded) { __shared__ uint8_t tile[32][D + 4]; const int head = blockIdx.y; const int first = blockIdx.x * 32; const size_t base = size_t(head) * length * D; for (int i = threadIdx.x; i < 32 * D; i += 256) { const int token = first + i / D; const int channel = i % D; const float value = token < length ? __half2float(v[base + size_t(token) * D + channel]) / scales[size_t(head) * D + channel] : 0.0f; // FP8 MMA consumes pairs of tokens in a permuted, transposed V tile. const int row = i / D; const int permuted = row / 16 * 16 + row % 16 / 8 * 2 + row % 8 / 2 * 4 + row % 2; tile[permuted][channel] = __nv_cvt_float_to_fp8(value, __NV_SATFINITE, __NV_E4M3); } __syncthreads(); for (int i = threadIdx.x; i < 32 * D; i += 256) { output[(size_t(head) * D + i / 32) * padded + first + i % 32] = tile[i % 32][i / 32]; } } template static void sage_attn_fp8(cudaStream_t stream, ggml_tensor* dst, char* data, const SageWorkspace& work, float scale) { const ggml_tensor* q = dst->src[0]; const ggml_tensor* k = dst->src[1]; const ggml_tensor* v = dst->src[2]; const int lq = q->ne[1], lk = k->ne[1], padded = GGML_PAD(lk, 64); const int hq = q->ne[2], hk = k->ne[2], batch = q->ne[3]; auto* qi = reinterpret_cast(data + work.q); auto* ki = reinterpret_cast(data + work.k); auto* vi = reinterpret_cast(data + work.v); auto* qs = reinterpret_cast(data + work.q_scale); auto* ks = reinterpret_cast(data + work.k_scale); auto* vs = reinterpret_cast(data + work.v_scale); auto* km = reinterpret_cast(data + work.k_mean); auto* out = reinterpret_cast(data + work.output); sage_key_mean<<>>(static_cast(k->data), km, lk); sage_quantize_per_thread<<>>( static_cast(q->data), nullptr, qi, qs, lq); sage_quantize_per_thread<<>>( static_cast(k->data), km, ki, ks, lk); // The smaller V range keeps the FP16 instruction accumulator finite in SageAttention2++. sage_value_scale<<>>(static_cast(v->data), vs, lk, PLUS_PLUS ? 2.25f : 448.0f); sage_quantize_value<<>>(static_cast(v->data), vs, vi, lk, padded); auto kernel = sageattention::qk_int_sv_f8_attn_kernel<128, 64, 32, 64, D, sageattention::DataType::kInt8, sageattention::QuantGranularity::kPerThread, sageattention::QuantGranularity::kPerThread, float, true, half, sageattention::ComputeUnit::kCudaCore, sageattention::MaskMode::kNone, false, true, false, PLUS_PLUS>; kernel<<>>( qi, ki, vi, out, nullptr, qs, ks, vs, nullptr, lq, lk, hq / hk, D * lq * hq, D, D * lq, D * lk * hk, D, D * lk, D * padded * hk, D * padded, padded, D * lq * hq, D * hq, D, scale); sage_output<<<(ggml_nelements(dst) + 255) / 256, 256, 0, stream>>>(out, static_cast(dst->data), ggml_nelements(dst)); } template static void sage_attn(cudaStream_t stream, ggml_tensor* dst, char* data, ggml_sage_attn_mode mode, float scale) { const ggml_tensor* q = dst->src[0]; const ggml_tensor* k = dst->src[1]; const ggml_tensor* v = dst->src[2]; const int lq = q->ne[1], lk = k->ne[1]; const int hq = q->ne[2], hk = k->ne[2], batch = q->ne[3]; const SageWorkspace work(dst); if (mode == GGML_SAGE_ATTN_2_PLUS_PLUS) { sage_attn_fp8(stream, dst, data, work, scale); return; } if (mode == GGML_SAGE_ATTN_2) { sage_attn_fp8(stream, dst, data, work, scale); return; } auto* qi = reinterpret_cast(data + work.q); auto* ki = reinterpret_cast(data + work.k); auto* qs = reinterpret_cast(data + work.q_scale); auto* ks = reinterpret_cast(data + work.k_scale); auto* km = reinterpret_cast(data + work.k_mean); auto* out = reinterpret_cast(data + work.output); sage_key_mean<<>>(static_cast(k->data), km, lk); sage_quantize<<>>( static_cast(q->data), nullptr, qi, qs, lq); sage_quantize<<>>( static_cast(k->data), km, ki, ks, lk); constexpr size_t shared_bytes = (128 + 64 + 64 * 2) * D; auto kernel = sageattention::qk_int_sv_f16_attn_kernel<128, 64, 32, 64, D, sageattention::DataType::kInt8, sageattention::QuantGranularity::kPerWarp, sageattention::QuantGranularity::kPerWarp, float, false, half, sageattention::ComputeUnit::kTensorCore, sageattention::MaskMode::kNone, false, false>; kernel<<>>( qi, ki, static_cast(v->data), out, nullptr, qs, ks, nullptr, lq, lk, hq / hk, D * lq * hq, D, D * lq, D * lk * hk, D, D * lk, D * lk * hk, D, D * lk, D * lq * hq, D * hq, D, scale); sage_output<<<(ggml_nelements(dst) + 255) / 256, 256, 0, stream>>>(out, static_cast(dst->data), ggml_nelements(dst)); } static bool sage_supports(ggml_backend_dev_t, const ggml_tensor* op, void* user_data) { const auto& device = *static_cast(user_data); SDSageAttentionParams params; if (!sd_ggml_sage_attn_describe(op, ¶ms) || device.cc < 80 || device.compiled_cc < 80) return false; const auto mode = sage_mode(device, params.mode); if (mode != GGML_SAGE_ATTN_AUTO && (sage_cuda_version < 12040 || device.cc < 89 || device.compiled_cc < 89)) return false; if (mode == GGML_SAGE_ATTN_2_PLUS_PLUS && sage_cuda_version < 12080) return false; const auto* q = op->src[0]; const auto* k = op->src[1]; const auto* v = op->src[2]; if (q->ne[0] != 64 && q->ne[0] != 128) return false; for (const auto* t : {q, k, v, op}) { if (ggml_nelements(t) > INT_MAX || t->ne[2] * t->ne[3] > 65535 || (reinterpret_cast(t->data) & 15)) return false; } return k->ne[0] * GGML_PAD(k->ne[1], 64) * k->ne[2] * k->ne[3] <= INT_MAX; } static ggml_status sage_prepare(ggml_backend_t, ggml_tensor* const* nodes, int, void**, size_t* workspace, void*) { *workspace = SageWorkspace(nodes[0]).size; return GGML_STATUS_SUCCESS; } static void sage_destroy(void*, void*) {} static ggml_status sage_execute(const ggml_ext_execution_context* execution, ggml_tensor* const* nodes, int, void*, void* user_data) { ggml_ext_gpu_context context; if (!cuda_context(execution->backend, &context)) return GGML_STATUS_FAILED; SDSageAttentionParams params; if (!sd_ggml_sage_attn_describe(nodes[0], ¶ms)) return GGML_STATUS_FAILED; const auto mode = sage_mode(*static_cast(user_data), params.mode); auto* data = static_cast(ggml_backend_buffer_get_base(execution->workspace)); auto stream = static_cast(context.stream); if (nodes[0]->src[0]->ne[0] == 64) sage_attn<64>(stream, nodes[0], data, mode, params.scale); else sage_attn<128>(stream, nodes[0], data, mode, params.scale); return cudaGetLastError() == cudaSuccess ? GGML_STATUS_SUCCESS : GGML_STATUS_FAILED; } void register_sage_attention(ggml_backend_dev_t device) { const auto info = cuda_device_info(device); if (info.device < 0) return; int previous; cudaDeviceProp properties; cudaFuncAttributes attributes; if (cudaGetDeviceProperties(&properties, info.device) != cudaSuccess || cudaGetDevice(&previous) != cudaSuccess || cudaSetDevice(info.device) != cudaSuccess) return; const auto error = cudaFuncGetAttributes(&attributes, sage_output); if (cudaSetDevice(previous) != cudaSuccess || error != cudaSuccess) return; // PTX compiled for an older target retains that target's instruction guards after JIT. static std::map devices; auto& data = devices[info.device]; data = {properties.major * 10 + properties.minor, std::min(attributes.binaryVersion, attributes.ptxVersion)}; ggml_ext_kernel_desc desc = {}; desc.abi_version = GGML_EXT_ABI_VERSION; desc.name = "sd.sage_attn.cuda.v1"; desc.op = GGML_OP_CUSTOM; desc.custom_op = sd_ggml_sage_attn_op(); desc.priority = 100; desc.supports = sage_supports; desc.prepare = sage_prepare; desc.destroy = sage_destroy; desc.execute = sage_execute; desc.user_data = &data; GGML_ASSERT(ggml_ext_register_kernel_ex(device, &desc, GGML_EXT_KERNEL_CAPTURE_SAFE | GGML_EXT_KERNEL_REPLAY_REUSABLE)); } } #else namespace sd_cuda { void register_sage_attention(ggml_backend_dev_t) {} } #endif