Foundry/attention/attention.c

467 lines
14 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
#include "attention.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <float.h>
/* ============================================================= */
/* Config */
/* ============================================================= */
fa_config fa_config_default(int num_heads, int head_dim) {
fa_config config = {
.num_heads = num_heads,
.head_dim = head_dim,
.dropout = 0.0f,
.causal = false,
.scale = 1.0f / sqrtf((float)head_dim)
};
return config;
}
void fa_print_config(fa_config* config) {
if (!config) {
printf("(null config)\n");
return;
}
printf("Attention config: heads=%d, head_dim=%d, scale=%.4f, causal=%s\n",
config->num_heads, config->head_dim, config->scale,
config->causal ? "true" : "false");
}
/* ============================================================= */
/* Causal Mask */
/* ============================================================= */
tm_tensor* fa_causal_mask(int seq_len) {
int64_t shape[] = {1, 1, seq_len, seq_len};
tm_tensor* mask = tm_zeros(shape, 4, TM_F32);
if (!mask) return NULL;
float* data = tm_data_f32(mask);
/* Fill upper triangle with -inf */
for (int i = 0; i < seq_len; i++) {
for (int j = i + 1; j < seq_len; j++) {
data[i * seq_len + j] = -INFINITY;
}
}
return mask;
}
/* ============================================================= */
/* Core Attention */
/* ============================================================= */
/* Simple attention for 2D Q, K, V (seq_len x dim) */
static tm_tensor* attention_2d(tm_tensor* Q, tm_tensor* K, tm_tensor* V,
float scale, bool causal) {
if (!Q || !K || !V) return NULL;
int seq_len = (int)Q->shape[0];
int kv_len = (int)K->shape[0];
/* Compute Q @ K^T */
tm_tensor* Kt = tm_transpose(K, 0, 1);
if (!Kt) return NULL;
tm_tensor* scores = tm_matmul(Q, Kt);
tm_release(Kt);
if (!scores) return NULL;
/* Scale */
float* s_data = tm_data_f32(scores);
for (int64_t i = 0; i < scores->numel; i++) {
s_data[i] *= scale;
}
/* Apply causal mask */
if (causal) {
for (int i = 0; i < seq_len; i++) {
for (int j = i + 1; j < kv_len; j++) {
s_data[i * kv_len + j] = -INFINITY;
}
}
}
/* Softmax over last dimension */
tm_tensor* attn_weights = tm_softmax(scores, 1);
tm_release(scores);
if (!attn_weights) return NULL;
/* Attention output = weights @ V */
tm_tensor* output = tm_matmul(attn_weights, V);
tm_release(attn_weights);
return output;
}
tm_tensor* fa_attention(tm_tensor* Q, tm_tensor* K, tm_tensor* V, fa_config* config) {
if (!Q || !K || !V || !config) return NULL;
float scale = config->scale;
if (scale == 0.0f) {
scale = 1.0f / sqrtf((float)config->head_dim);
}
/* Handle different input shapes */
if (Q->ndim == 2) {
/* Simple 2D attention */
return attention_2d(Q, K, V, scale, config->causal);
}
if (Q->ndim == 3) {
/* Batched attention: (batch, seq, dim) */
int batch = (int)Q->shape[0];
int seq_len = (int)Q->shape[1];
int dim = (int)Q->shape[2];
int64_t out_shape[] = {batch, seq_len, dim};
tm_tensor* output = tm_zeros(out_shape, 3, TM_F32);
if (!output) return NULL;
/* Process each batch */
for (int b = 0; b < batch; b++) {
/* Extract slices - simplified: just compute directly */
int64_t q_shape[] = {seq_len, dim};
tm_tensor* q_slice = tm_zeros(q_shape, 2, TM_F32);
tm_tensor* k_slice = tm_zeros(q_shape, 2, TM_F32);
tm_tensor* v_slice = tm_zeros(q_shape, 2, TM_F32);
float* q_data = tm_data_f32(Q);
float* k_data = tm_data_f32(K);
float* v_data = tm_data_f32(V);
memcpy(tm_data_f32(q_slice), q_data + b * seq_len * dim, seq_len * dim * sizeof(float));
memcpy(tm_data_f32(k_slice), k_data + b * seq_len * dim, seq_len * dim * sizeof(float));
memcpy(tm_data_f32(v_slice), v_data + b * seq_len * dim, seq_len * dim * sizeof(float));
tm_tensor* out_slice = attention_2d(q_slice, k_slice, v_slice, scale, config->causal);
if (out_slice) {
memcpy(tm_data_f32(output) + b * seq_len * dim,
tm_data_f32(out_slice), seq_len * dim * sizeof(float));
tm_release(out_slice);
}
tm_release(q_slice);
tm_release(k_slice);
tm_release(v_slice);
}
return output;
}
return NULL;
}
tm_tensor* fa_attention_mask(tm_tensor* Q, tm_tensor* K, tm_tensor* V,
tm_tensor* mask, fa_config* config) {
/* For now, just use causal attention if mask provided */
fa_config cfg = *config;
if (mask) {
cfg.causal = true;
}
return fa_attention(Q, K, V, &cfg);
}
/* ============================================================= */
/* Multi-Head Attention */
/* ============================================================= */
tm_tensor* fa_multi_head(tm_tensor* x, tm_tensor* Wq, tm_tensor* Wk, tm_tensor* Wv,
tm_tensor* Wo, fa_config* config) {
if (!x || !Wq || !Wk || !Wv || !Wo || !config) return NULL;
/* Project to Q, K, V */
tm_tensor* Q = tm_matmul(x, Wq);
tm_tensor* K = tm_matmul(x, Wk);
tm_tensor* V = tm_matmul(x, Wv);
if (!Q || !K || !V) {
tm_release(Q);
tm_release(K);
tm_release(V);
return NULL;
}
/* Compute attention */
tm_tensor* attn = fa_attention(Q, K, V, config);
tm_release(Q);
tm_release(K);
tm_release(V);
if (!attn) return NULL;
/* Output projection */
tm_tensor* output = tm_matmul(attn, Wo);
tm_release(attn);
return output;
}
tm_tensor* fa_self_attention(tm_tensor* x, tm_tensor* Wqkv, tm_tensor* Wo,
fa_config* config) {
if (!x || !Wqkv || !Wo || !config) return NULL;
/* Project to combined QKV */
tm_tensor* qkv = tm_matmul(x, Wqkv);
if (!qkv) return NULL;
/* Split into Q, K, V (assuming Wqkv projects to 3 * hidden_dim) */
int hidden = config->num_heads * config->head_dim;
int64_t seq_len = x->shape[x->ndim - 2];
int64_t q_shape[] = {seq_len, hidden};
tm_tensor* Q = tm_zeros(q_shape, 2, TM_F32);
tm_tensor* K = tm_zeros(q_shape, 2, TM_F32);
tm_tensor* V = tm_zeros(q_shape, 2, TM_F32);
if (!Q || !K || !V) {
tm_release(qkv);
tm_release(Q);
tm_release(K);
tm_release(V);
return NULL;
}
float* qkv_data = tm_data_f32(qkv);
float* q_data = tm_data_f32(Q);
float* k_data = tm_data_f32(K);
float* v_data = tm_data_f32(V);
for (int64_t i = 0; i < seq_len; i++) {
memcpy(q_data + i * hidden, qkv_data + i * 3 * hidden, hidden * sizeof(float));
memcpy(k_data + i * hidden, qkv_data + i * 3 * hidden + hidden, hidden * sizeof(float));
memcpy(v_data + i * hidden, qkv_data + i * 3 * hidden + 2 * hidden, hidden * sizeof(float));
}
tm_release(qkv);
/* Compute attention */
tm_tensor* attn = fa_attention(Q, K, V, config);
tm_release(Q);
tm_release(K);
tm_release(V);
if (!attn) return NULL;
/* Output projection */
tm_tensor* output = tm_matmul(attn, Wo);
tm_release(attn);
return output;
}
/* ============================================================= */
/* KV Cache */
/* ============================================================= */
struct fa_context {
tm_tensor** k_cache; /* K cache per layer */
tm_tensor** v_cache; /* V cache per layer */
int num_layers;
int batch_size;
int max_seq_len;
int num_heads;
int head_dim;
int current_len; /* Current sequence length in cache */
};
fa_context* fa_context_new(int batch_size, int max_seq_len, int num_heads,
int head_dim, int num_layers) {
fa_context* ctx = calloc(1, sizeof(fa_context));
if (!ctx) return NULL;
ctx->num_layers = num_layers;
ctx->batch_size = batch_size;
ctx->max_seq_len = max_seq_len;
ctx->num_heads = num_heads;
ctx->head_dim = head_dim;
ctx->current_len = 0;
ctx->k_cache = calloc(num_layers, sizeof(tm_tensor*));
ctx->v_cache = calloc(num_layers, sizeof(tm_tensor*));
if (!ctx->k_cache || !ctx->v_cache) {
fa_context_free(ctx);
return NULL;
}
/* Allocate cache tensors */
int64_t shape[] = {batch_size, max_seq_len, num_heads * head_dim};
for (int i = 0; i < num_layers; i++) {
ctx->k_cache[i] = tm_zeros(shape, 3, TM_F32);
ctx->v_cache[i] = tm_zeros(shape, 3, TM_F32);
if (!ctx->k_cache[i] || !ctx->v_cache[i]) {
fa_context_free(ctx);
return NULL;
}
}
return ctx;
}
void fa_context_free(fa_context* ctx) {
if (!ctx) return;
if (ctx->k_cache) {
for (int i = 0; i < ctx->num_layers; i++) {
tm_release(ctx->k_cache[i]);
}
free(ctx->k_cache);
}
if (ctx->v_cache) {
for (int i = 0; i < ctx->num_layers; i++) {
tm_release(ctx->v_cache[i]);
}
free(ctx->v_cache);
}
free(ctx);
}
void fa_context_clear(fa_context* ctx) {
if (!ctx) return;
ctx->current_len = 0;
/* Zero out cache tensors */
for (int i = 0; i < ctx->num_layers; i++) {
if (ctx->k_cache[i]) {
memset(tm_data_f32(ctx->k_cache[i]), 0,
ctx->k_cache[i]->numel * sizeof(float));
}
if (ctx->v_cache[i]) {
memset(tm_data_f32(ctx->v_cache[i]), 0,
ctx->v_cache[i]->numel * sizeof(float));
}
}
}
int fa_context_seq_len(fa_context* ctx) {
return ctx ? ctx->current_len : 0;
}
tm_tensor* fa_attention_cached(tm_tensor* Q, tm_tensor* K, tm_tensor* V,
fa_context* ctx, int layer_idx, int pos,
fa_config* config) {
if (!Q || !K || !V || !ctx || !config) return NULL;
if (layer_idx < 0 || layer_idx >= ctx->num_layers) return NULL;
if (pos >= ctx->max_seq_len) return NULL;
int dim = ctx->num_heads * ctx->head_dim;
/* Update cache with new K, V at position pos */
float* k_cache_data = tm_data_f32(ctx->k_cache[layer_idx]);
float* v_cache_data = tm_data_f32(ctx->v_cache[layer_idx]);
float* k_data = tm_data_f32(K);
float* v_data = tm_data_f32(V);
/* Copy new K, V to cache at pos */
int new_tokens = (int)K->shape[K->ndim - 2];
for (int t = 0; t < new_tokens; t++) {
int cache_pos = pos + t;
if (cache_pos < ctx->max_seq_len) {
memcpy(k_cache_data + cache_pos * dim, k_data + t * dim, dim * sizeof(float));
memcpy(v_cache_data + cache_pos * dim, v_data + t * dim, dim * sizeof(float));
}
}
ctx->current_len = pos + new_tokens;
/* Create K, V views from cache up to current_len */
int64_t kv_shape[] = {ctx->current_len, dim};
tm_tensor* K_full = tm_zeros(kv_shape, 2, TM_F32);
tm_tensor* V_full = tm_zeros(kv_shape, 2, TM_F32);
if (!K_full || !V_full) {
tm_release(K_full);
tm_release(V_full);
return NULL;
}
memcpy(tm_data_f32(K_full), k_cache_data, ctx->current_len * dim * sizeof(float));
memcpy(tm_data_f32(V_full), v_cache_data, ctx->current_len * dim * sizeof(float));
/* Compute attention */
tm_tensor* output = fa_attention(Q, K_full, V_full, config);
tm_release(K_full);
tm_release(V_full);
return output;
}
/* ============================================================= */
/* RoPE */
/* ============================================================= */
tm_tensor* fa_rope_freqs(int max_seq_len, int head_dim) {
int64_t shape[] = {max_seq_len, head_dim / 2};
tm_tensor* freqs = tm_zeros(shape, 2, TM_F32);
if (!freqs) return NULL;
float* data = tm_data_f32(freqs);
float theta = 10000.0f;
for (int pos = 0; pos < max_seq_len; pos++) {
for (int i = 0; i < head_dim / 2; i++) {
float freq = 1.0f / powf(theta, (float)(2 * i) / head_dim);
data[pos * (head_dim / 2) + i] = (float)pos * freq;
}
}
return freqs;
}
void fa_rope(tm_tensor* Q, tm_tensor* K, int start_pos, int head_dim) {
if (!Q || !K) return;
int seq_len = (int)Q->shape[Q->ndim - 2];
int dim = (int)Q->shape[Q->ndim - 1];
float* q_data = tm_data_f32(Q);
float* k_data = tm_data_f32(K);
float theta = 10000.0f;
for (int pos = 0; pos < seq_len; pos++) {
int abs_pos = start_pos + pos;
for (int i = 0; i < head_dim / 2; i++) {
float freq = 1.0f / powf(theta, (float)(2 * i) / head_dim);
float angle = (float)abs_pos * freq;
float cos_val = cosf(angle);
float sin_val = sinf(angle);
/* Apply rotation to Q */
int idx1 = pos * dim + i * 2;
int idx2 = pos * dim + i * 2 + 1;
if (idx2 < (int)Q->numel) {
float q0 = q_data[idx1];
float q1 = q_data[idx2];
q_data[idx1] = q0 * cos_val - q1 * sin_val;
q_data[idx2] = q0 * sin_val + q1 * cos_val;
}
/* Apply rotation to K */
if (idx2 < (int)K->numel) {
float k0 = k_data[idx1];
float k1 = k_data[idx2];
k_data[idx1] = k0 * cos_val - k1 * sin_val;
k_data[idx2] = k0 * sin_val + k1 * cos_val;
}
}
}
}