kronos-mql5/Include/Kronos/KronosTransformerCore.mqh
2026-07-05 05:03:49 +00:00

319 lines
13 KiB
MQL5

//+------------------------------------------------------------------+
//| KronosTransformerCore.mqh |
//| MMQ — Muhammad Minhas Qamar |
//| www.mql5.com/en/articles/23304 |
//+------------------------------------------------------------------+
#property copyright "MMQ — Muhammad Minhas Qamar"
#property link "https://www.mql5.com/en/articles/23304"
#property version "1.00"
#ifndef KRONOS_TRANSFORMER_CORE_MQH
#define KRONOS_TRANSFORMER_CORE_MQH
//--- transformer core constants
#define KR_ROPE_BASE 10000.0
#define KR_NORM_EPS 1e-5
//+------------------------------------------------------------------+
//| Linear (PyTorch convention): y = x @ W^T. The weight is loaded |
//| pre-transposed (KronosLoadMatrixT stores W as [in,out] == W^T), |
//| so this is a plain MatMul with NO per-call transpose. Profiling |
//| showed the old per-call W.Transpose() was ~60% of the forward |
//| pass; the transpose now happens once at load. Math is identical. |
//+------------------------------------------------------------------+
matrix LinearT(const matrix &X, const matrix &W) { return X.MatMul(W); }
//+------------------------------------------------------------------+
//| Add a per-output-column bias to every row of Y. |
//+------------------------------------------------------------------+
void AddRowBias(matrix &Y, const vector &b)
{
ulong T = Y.Rows(), O = Y.Cols();
for(ulong i = 0; i < T; i++)
for(ulong j = 0; j < O; j++)
Y[i][j] += b[j];
}
//+------------------------------------------------------------------+
//| RMSNorm (row-wise): out = x / sqrt(mean(x^2) + eps) * weight. |
//+------------------------------------------------------------------+
matrix RMSNorm(const matrix &X, const vector &w, double eps = KR_NORM_EPS)
{
ulong T = X.Rows(), D = X.Cols();
matrix out = matrix::Zeros(T, D);
for(ulong i = 0; i < T; i++)
{
double ms = 0.0;
for(ulong j = 0; j < D; j++)
ms += X[i][j] * X[i][j];
ms /= (double)D;
double scale = 1.0 / MathSqrt(ms + eps);
for(ulong j = 0; j < D; j++)
out[i][j] = X[i][j] * scale * w[j];
}
return out;
}
//+------------------------------------------------------------------+
//| SiLU activation: x * sigmoid(x). |
//+------------------------------------------------------------------+
double SiLU(double x) { return x / (1.0 + MathExp(-x)); }
//+------------------------------------------------------------------+
//| SwiGLU feed-forward: w2( SiLU(w1 x) * w3 x ), all bias-free. |
//+------------------------------------------------------------------+
matrix SwiGLU(const matrix &X, const matrix &W1, const matrix &W3, const matrix &W2)
{
matrix H = LinearT(X, W1);
matrix G = LinearT(X, W3);
ulong T = H.Rows(), FF = H.Cols();
matrix gated = matrix::Zeros(T, FF);
for(ulong i = 0; i < T; i++)
for(ulong j = 0; j < FF; j++)
gated[i][j] = SiLU(H[i][j]) * G[i][j];
return LinearT(gated, W2);
}
//+------------------------------------------------------------------+
//| Build the RoPE cos/sin tables (T, hd). emb = cat(freqs, freqs), |
//| so the two halves of each row are duplicated. |
//+------------------------------------------------------------------+
void RoPETables(ulong T, ulong hd, matrix &cosT, matrix &sinT)
{
ulong half = hd / 2;
cosT = matrix::Zeros(T, hd);
sinT = matrix::Zeros(T, hd);
for(ulong t = 0; t < T; t++)
for(ulong k = 0; k < half; k++)
{
double inv_freq = 1.0 / MathPow(KR_ROPE_BASE, (2.0 * (double)k) / (double)hd);
double ang = (double)t * inv_freq;
double c = MathCos(ang), s = MathSin(ang);
cosT[t][k] = c;
cosT[t][k + half] = c;
sinT[t][k] = s;
sinT[t][k + half] = s;
}
}
//+------------------------------------------------------------------+
//| Apply RoPE: out = x*cos + rotate_half(x)*sin, where |
//| rotate_half(x) = cat(-x2, x1). |
//+------------------------------------------------------------------+
matrix ApplyRoPE(const matrix &X, const matrix &cosT, const matrix &sinT)
{
ulong T = X.Rows(), hd = X.Cols(), half = hd / 2;
matrix out = matrix::Zeros(T, hd);
for(ulong t = 0; t < T; t++)
for(ulong k = 0; k < hd; k++)
{
double rh = (k < half) ? -X[t][k + half] : X[t][k - half];
out[t][k] = X[t][k] * cosT[t][k] + rh * sinT[t][k];
}
return out;
}
//+------------------------------------------------------------------+
//| Extract one head's columns [c0, c0+hd) into a (T, hd) matrix. |
//+------------------------------------------------------------------+
matrix SliceCols(const matrix &M, ulong c0, ulong hd)
{
ulong T = M.Rows();
matrix s = matrix::Zeros(T, hd);
for(ulong i = 0; i < T; i++)
for(ulong k = 0; k < hd; k++)
s[i][k] = M[i][c0 + k];
return s;
}
//+------------------------------------------------------------------+
//| Write one head's (T, hd) output back into columns [c0, c0+hd). |
//+------------------------------------------------------------------+
void WriteCols(matrix &M, const matrix &S, ulong c0)
{
ulong T = S.Rows(), hd = S.Cols();
for(ulong i = 0; i < T; i++)
for(ulong k = 0; k < hd; k++)
M[i][c0 + k] = S[i][k];
}
//+------------------------------------------------------------------+
//| Scaled dot-product attention for one head. |
//| Qh(Tq,hd), Kh(Tk,hd), Vh(Tk,hd). causal=true masks j>i and so |
//| needs Tq==Tk. scores = Q.Kt * inv_sqrt -> stable row-softmax |
//| -> weights . V. Returns Oh(Tq,hd). |
//+------------------------------------------------------------------+
matrix SDPA(const matrix &Qh, const matrix &Kh, const matrix &Vh, bool causal)
{
ulong Tq = Qh.Rows(), Tk = Kh.Rows(), hd = Qh.Cols();
double inv_sqrt = 1.0 / MathSqrt((double)hd);
//--- scaled scores
matrix scores = Qh.MatMul(Kh.Transpose()); // (Tq,Tk)
scores *= inv_sqrt;
//--- row-wise softmax with optional causal mask
for(ulong i = 0; i < Tq; i++)
{
//--- causal mask: query i attends to keys 0..i only
ulong jmax = causal ? i : (Tk - 1);
double smax = -DBL_MAX;
for(ulong j = 0; j <= jmax; j++)
if(scores[i][j] > smax)
smax = scores[i][j];
double denom = 0.0;
for(ulong j = 0; j < Tk; j++)
{
if(causal && j > i)
{
scores[i][j] = 0.0;
continue;
}
double e = MathExp(scores[i][j] - smax);
scores[i][j] = e;
denom += e;
}
double inv = 1.0 / denom;
for(ulong j = 0; j < Tk; j++)
scores[i][j] *= inv; // masked entries already 0
}
return scores.MatMul(Vh); // (Tq,hd)
}
//+------------------------------------------------------------------+
//| Causal multi-head self-attention with RoPE. |
//| q/k/v/out projections carry a bias. |
//+------------------------------------------------------------------+
matrix MHA(const matrix &X,
const matrix &Wq, const vector &bq,
const matrix &Wk, const vector &bk,
const matrix &Wv, const vector &bv,
const matrix &Wo, const vector &bo,
int n_heads)
{
ulong T = X.Rows();
ulong d_model = X.Cols();
ulong hd = d_model / (ulong)n_heads;
//--- q/k/v projections (with bias)
matrix Q = LinearT(X, Wq);
AddRowBias(Q, bq);
matrix K = LinearT(X, Wk);
AddRowBias(K, bk);
matrix V = LinearT(X, Wv);
AddRowBias(V, bv);
//--- shared RoPE tables for all heads
matrix cosT, sinT;
RoPETables(T, hd, cosT, sinT);
matrix ctx = matrix::Zeros(T, d_model);
//--- per-head causal attention
for(int h = 0; h < n_heads; h++)
{
ulong c0 = (ulong)h * hd;
matrix Qs = SliceCols(Q, c0, hd); // named locals: MQL5 passes matrices by reference only
matrix Ks = SliceCols(K, c0, hd);
matrix Qh = ApplyRoPE(Qs, cosT, sinT);
matrix Kh = ApplyRoPE(Ks, cosT, sinT);
matrix Vh = SliceCols(V, c0, hd);
matrix Oh = SDPA(Qh, Kh, Vh, true); // causal
WriteCols(ctx, Oh, c0);
}
//--- output projection (with bias)
matrix out = LinearT(ctx, Wo);
AddRowBias(out, bo);
return out;
}
//+------------------------------------------------------------------+
//| Non-causal multi-head CROSS-attention with RoPE (inference). |
//| q from Xq (sibling embed), k/v from Xkv (context); each query |
//| attends to ALL key positions. q/k/v/out carry a bias. The |
//| predictor's dep_layer uses n_heads=4 (head_dim=128), NOT 8. |
//| |
//| RoPE quirk (matches RotaryPositionalEmbedding.forward): the |
//| cos/sin cache is sized to the QUERY length and the same cache is |
//| applied to the keys. So when Tq==1 (a single broadcast s1 pick) |
//| every key is rotated at position 0; when Tq==Tk keys rotate by |
//| their own positions. We index the key rotation by (Tq==1 ? 0:j), |
//| reusing the query's table. |
//+------------------------------------------------------------------+
matrix CrossMHA(const matrix &Xq, const matrix &Xkv,
const matrix &Wq, const vector &bq,
const matrix &Wk, const vector &bk,
const matrix &Wv, const vector &bv,
const matrix &Wo, const vector &bo,
int n_heads)
{
ulong Tq = Xq.Rows();
ulong Tk = Xkv.Rows();
ulong d_model = Xq.Cols();
ulong hd = d_model / (ulong)n_heads; // scaling handled inside SDPA
//--- q from Xq, k/v from Xkv (all with bias)
matrix Q = LinearT(Xq, Wq);
AddRowBias(Q, bq);
matrix K = LinearT(Xkv, Wk);
AddRowBias(K, bk);
matrix V = LinearT(Xkv, Wv);
AddRowBias(V, bv);
//--- single cache sized to the query length, reused for keys (PyTorch quirk).
//--- valid only when Tq==1 (broadcast) or Tq==Tk (position-matched); any other
//--- mix would have failed PyTorch's broadcast, so reject it loudly.
if(!(Tq == 1 || Tq == Tk))
{ PrintFormat("CrossMHA: unsupported Tq=%I64u, Tk=%I64u (need Tq==1 or Tq==Tk)", Tq, Tk); }
matrix cosT, sinT;
RoPETables(Tq, hd, cosT, sinT);
//--- per-key rotation table: row j uses position (Tq==1 ? 0 : j)
matrix cosK = matrix::Zeros(Tk, hd), sinK = matrix::Zeros(Tk, hd);
for(ulong j = 0; j < Tk; j++)
{
ulong p = (Tq == 1) ? 0 : j; // broadcast when single query
for(ulong k = 0; k < hd; k++)
{
cosK[j][k] = cosT[p][k];
sinK[j][k] = sinT[p][k];
}
}
matrix ctx = matrix::Zeros(Tq, d_model);
//--- per-head non-causal cross-attention
for(int h = 0; h < n_heads; h++)
{
ulong c0 = (ulong)h * hd;
matrix Qs = SliceCols(Q, c0, hd);
matrix Ks = SliceCols(K, c0, hd);
matrix Qh = ApplyRoPE(Qs, cosT, sinT);
matrix Kh = ApplyRoPE(Ks, cosK, sinK);
matrix Vh = SliceCols(V, c0, hd);
matrix Oh = SDPA(Qh, Kh, Vh, false); // non-causal: all keys
WriteCols(ctx, Oh, c0);
}
//--- output projection (with bias)
matrix out = LinearT(ctx, Wo);
AddRowBias(out, bo);
return out;
}
//+------------------------------------------------------------------+
//| Pre-norm block: |
//| x += MHA(RMSNorm1(x)); x += SwiGLU(RMSNorm2(x)). |
//+------------------------------------------------------------------+
matrix TransformerBlock(const matrix &X,
const vector &norm1_w,
const matrix &Wq, const vector &bq,
const matrix &Wk, const vector &bk,
const matrix &Wv, const vector &bv,
const matrix &Wo, const vector &bo,
int n_heads,
const vector &norm2_w,
const matrix &W1, const matrix &W3, const matrix &W2)
{
//--- attention sub-block: x1 = x + MHA(RMSNorm1(x))
matrix n1 = RMSNorm(X, norm1_w); // named locals: no temporaries by reference
matrix a = MHA(n1, Wq, bq, Wk, bk, Wv, bv, Wo, bo, n_heads);
matrix x1 = X + a;
//--- feed-forward sub-block: out = x1 + SwiGLU(RMSNorm2(x1))
matrix n2 = RMSNorm(x1, norm2_w);
matrix f = SwiGLU(n2, W1, W3, W2);
return x1 + f;
}
#endif // KRONOS_TRANSFORMER_CORE_MQH
//+------------------------------------------------------------------+