Tensor Logic

Source: source/classes/tensorlogic.prg, source/function/fwtensor.c

FiveWin implements Pedro Domingos' Tensor Logic: The Language of AI (arXiv:2510.12269). Its key insight: a logical rule and an Einstein summation are the same operation. A program is just a set of tensor equations; each equation joins the tensors on its right-hand side (product over shared indices), projects out the indices that do not appear on the left-hand side (summation), and optionally applies a nonlinearity. One engine therefore runs both neural networks (attention, MLPs) and symbolic reasoning (Datalog over DBF tables) — and can differentiate any program automatically.

flowchart LR EQ["Tensor equation
Y[i] = f( W[i,j] * X[j] )"] --> N[Neural: attention, MLP, transformer] EQ --> S[Symbolic: Datalog rules over DBFs] EQ --> G["Autodiff: gradient =
same einsum, factors swapped"]

Quick start

  1. Build the FWH library for your compiler variant (e.g. mfwh_new.bat hb32) so tensorlogic.prg and fwtensor.c are in the lib.
  2. From samples\ai, build and run the tutorial companion:
    cd /d C:\fwteam\samples\ai
    C:\fwteam\samples\build_new.bat tltutor hb32
    tltutor.exe
    Output lands in tltutor.log. Walk it side by side with the step-by-step tutorial.
  3. Optional speed: put libopenblas.dll on the PATH or next to the exe (or set FWT_BLAS_DLL). Check with ? FWT_BlasActive().

Tensor equations

oTL := TTensorLogic():New()
oTL:Let( "W", { { 1, -1, 0 }, { 0, 1, -1 } } )     // bind tensors (arrays or FW_Tensor)
oTL:Let( "X", { 1, 2, 0.5 } )
oTL:Let( "B", { 2, -2 } )
oTL:Eq( "Y[i] = step( W[i,j] * X[j] + B[i] )" )    // a perceptron, one equation
oTL:Run()                                          // forward chaining to fixpoint
? oTL:GetArray( "Y" )                              // {1, 0}

Indices repeated on the right are joined; indices missing from the left are summed out. Supported nonlinearities: step, sigmoid, relu, tanh, gelu, softmax (last index), lnorm (layer norm, last index, no affine part — express gamma/beta as further equations). Numeric factors scale a term: "S[p,q] = Q[p,k] * K[q,k] * 0.7071".

Equation syntax

FormMeaning
Out[i,k] = A[i,j] * B[j,k]Join on j, project j (matrix product).
Y[i] = step( W[i,j] * X[j] + B[i] )Several additive terms + nonlinearity.
S[p,q] = Q[p,d] * K[q,d] * 0.7071Numeric scale factor on a term.
A[p,q.] = softmax( S[p,q] )Trailing . marks the softmax axis.
N[p,d] = lnorm( R[p,d] )Layer norm over the last index.
N2[p,d] = N[p,d] * G[d] + Ones[p] * B[d]Affine gamma/beta after lnorm.

Terms with any number of factors are evaluated by chaining pairwise contractions: only the letters still needed for the final output survive each step, so every pairwise step can take the OpenBLAS GEMM fast path.

On parse failure, Eq() returns .F. and sets oTL:cError.

TTensorLogic

MethodDescription
New()Create an empty program (tensors, equations, Adam state).
Let( cName, uData )Bind a tensor (nested array of any rank or FW_Tensor).
Param( cName, uData )Let + mark as learnable (updated by Step() / StepAdam()).
Eq( cEquation )Add a tensor equation (string form). Returns .F. on parse error.
Run( nMaxPasses, nTol )Forward chaining until fixpoint (default 100 passes, tol 1e-6). Recursive Datalog rules converge.
Forward()Single pass in declaration order; caches pre-activations for Backward(). Use this for training, not Run().
ZeroGrad()Clear all accumulated gradients.
Seed( cName, tGrad )Set dL/dName (gradient of the loss w.r.t. an output tensor).
Backward()Reverse sweep over equations. Gradient of an equation = same einsum with factor and output swapped.
GetGrad( cName )Return dL/dName as FW_Tensor.
Step( nLr )SGD: each Param tensor -= lr * grad.
StepAdam( nLr, nB1, nB2, nEps )Adam optimizer (C kernel FWT_AdamUpdate with bias correction). Defaults: β1=0.9, β2=0.999, ε=1e-8.
ClipGrads( nMaxNorm )Global L2 gradient clipping. Returns the pre-clip norm.
ClearActGrads()Drop activation grads, keep param grads (mini-batch accumulation).
SaveParams( cFile, hUser ) / LoadParams( cFile )Checkpoint every Param() tensor as raw float32 plus an optional user hash (vocab, config). Load returns the user hash or NIL on error.
Get( cName ) / GetArray( cName )Result tensor / nested Harbour array (2D).
LetDbf( cName, cAlias, aFields, oDomain )DBF records → boolean relation tensor (one dimension per field).
LetSet( cName, aEntities, oDomain )List of names → unary boolean tensor.
Query( cName, oDomain, nThresh )Result tensor → tuples of entity names (default thresh 0.5).

DATA: hTensors, aEqs, cError, hPre, hGrads, aParams, hAdamM, hAdamV, nAdamT.

Training loop patterns

// Single-sample / full-batch
oTL:Forward()
oTL:ZeroGrad()
oTL:Seed( "Y", FWT_Scale( FWT_Sub( oTL:Get( "Y" ), tTarget ), 2 ) )  // d MSE
oTL:Backward()
oTL:ClipGrads( 1.0 )
oTL:StepAdam( 0.05 )

// Mini-batch accumulation (TTransformerTL style)
oTL:ZeroGrad()
for i := 1 to nBatch
   oT:AccumStep( aIds[i], aTgt[i], 1 / nBatch )   // param grads add up
next
oTL:ClipGrads( 1.0 )
oTL:StepAdam( nLr )

TTLDomain

Shared entity ↔ index map used by all relations so tensor dimensions match.

MethodDescription
New()Empty domain.
Add( cName )Register name if new; return 1-based id.
Id( cName )Id or 0 if unknown.
Name( nId )Name for id.
Len()Number of entities.
Harvest( cAlias, aFields )Register every value of those fields in the open alias. Call before LetDbf().

TTransformerTL

GPT-style transformer facade over a tensor logic program (~15 equations per layer). Heads are an index (no column slicing): WQ[d,h,k] absorbs the per-head split and WO[h,k,d] absorbs concat+Wo. With identical weights it reproduces the classic Transformer class ForwardSeq() closely, and it trains and generates through the generic autodiff — zero transformer-specific backprop code.

MethodDescription
New( nLayers, dModel, nHeads, nVocab )Build program + init params (Xavier-style). Defaults: 1, 8, 2, 4.
BuildProgram()Emit residual stream, multi-head attention, FFN, vocab projection as equations.
InitParams()Random (reproducible) Param() tensors for all weights.
LoadFromTransformer( oT )Copy weights from classic Transformer for parity tests.
SetInput( aIds )One-hot X, causal Mask, Pos, ones vectors.
ForwardSeq( aIds )→ nested array of probs [seq][vocab].
TrainStep( aIds, aTargets, nLr )CE loss step (ZeroGrad + Backward + Step). Returns loss.
AccumStep( aIds, aTargets, nScale )Accumulate grads only (for mini-batches). Returns scaled loss.
EvalLoss( aIds, aTargets )Forward-only CE (validation).
Generate( aSeed, nNew )Greedy argmax generation of nNew tokens.
oTLT := TTransformerTL():New( 2, 8, 2, nVocab )
aProbs := oTLT:ForwardSeq( { 2, 3, 4, 5, 6 } )
nLoss  := oTLT:TrainStep( aIds, aTargets, 0.01 )
aOut   := oTLT:Generate( { 1 }, 8 )
// multi-batch:
oTLT:oTL:ZeroGrad()
for i := 1 to nBatch
   oTLT:AccumStep( aWin[i], aTgt[i], 1 / nBatch )
next
oTLT:oTL:ClipGrads( 1.0 )
oTLT:oTL:StepAdam( nLr )

Attention skeleton as equations (from BuildProgram()):

oTL:Eq( "Q[h,p,k] = H0[p,d] * WQ[d,h,k]" )
oTL:Eq( "S[h,p,q] = Q[h,p,k] * K[h,q,k] * 0.5 + OnesH[h] * Mask[p,q]" )
oTL:Eq( "A[h,p,q.] = softmax( S[h,p,q] )" )
oTL:Eq( "O[h,p,k] = A[h,p,q] * V[h,q,k]" )
oTL:Eq( "Att[p,d] = O[h,p,k] * WO[h,k,d]" )
oTL:Eq( "N1[p,d] = lnorm( Att[p,d] + H0[p,d] )" )   // residual + layer norm
// ... FFN, second norm, vocabulary projection, softmax

Datalog over DBF tables

USE tlfam ALIAS FAM NEW              // fields PARENT, CHILD
oDom := TTLDomain():New()
oDom:Harvest( "FAM", { "PARENT", "CHILD" } )       // fix the domain first
oTL:LetDbf( "Parent", "FAM", { "PARENT", "CHILD" }, oDom )
oTL:LetSet( "Male", { "abe", "homer" }, oDom )
oTL:Let( "NotEq", TL_NotEq( oDom:Len() ) )
oTL:Let( "Ancestor", FWT_New( { oDom:Len(), oDom:Len() } ) )

oTL:Eq( "Father[x,y] = step( Parent[x,y] * Male[x] )" )
oTL:Eq( "Sibling[x,y] = step( Parent[p,x] * Parent[p,y] * NotEq[x,y] )" )
oTL:Eq( "Ancestor[x,y] = step( Parent[x,y] + Ancestor[x,z] * Parent[z,y] )" )  // recursive
oTL:Run( 20 )                                      // fixpoint
AEval( oTL:Query( "Ancestor", oDom ), {| a | QOut( a[1] + " -> " + a[2] ) } )

Reasoning in embedding space (the paper's neural-symbolic bridge): embed entities as unit vectors, embed the relation, query it back, and use a temperature sigmoid — T→0 is deductive (hard 0/1), higher T analogical:

oE:Let( "Emb", TL_UnitEmb( n, 256, 1.234 ) )
oE:Eq( "EmbR[i,j] = Parent[x,y] * Emb[x,i] * Emb[y,j]" )
oE:Eq( "Score[x,y] = EmbR[i,j] * Emb[x,i] * Emb[y,j]" )   // ~1 iff (x,y) in Parent
tSoft := FWT_Sigmoid( tShifted, nTemperature )

Helpers (PRG)

FunctionDescription
TL_TensorND( aNested )Nested Harbour array (any rank) → FW_Tensor.
TL_NotEq( n )[n,n] ones except diagonal (Datalog x != y guard). Same as FWT_NotEq.
TL_UnitEmb( n, d, nSeed )Unit-vector embeddings [n,d] (deterministic). Same as FWT_UnitEmb.
TL_Rnd2 / TL_Rnd3 / TL_Const1Xavier-ish init and constant vectors used by TTransformerTL.

C engine (fwtensor.c)

FunctionDescription
FWT_New( aShape ) / FWT_FromArray / FWT_ToArray / FWT_Shape / FWT_Size / FWT_Get / FWT_SetCreate and inspect flat float32 tensors (GC-managed).
FWT_EinSum( cSpec, t1, ... )Generalized Einstein summation, e.g. "ij,jk->ik", "pd,qd->pq", up to 6 operands. The tensor-logic primitive.
FWT_MatMul / FWT_Add / FWT_AddBias / FWT_Scale / FWT_TransposeCore linear-algebra ops (MatMul uses BLAS when available).
FWT_SoftmaxLast / FWT_LNormLastSoftmax / layer norm over the last dimension, any rank.
FWT_Softmax / FWT_LayerNorm2D row-wise variants (classic GPT-2 path).
FWT_Sigmoid( t, nT ) / FWT_Step( t, nThresh ) / FWT_ReLU / FWT_Tanh / FWT_GELUElementwise nonlinearities; sigmoid takes the reasoning temperature.
FWT_ActDeriv / FWT_SoftmaxBackwardLast / FWT_LNormBackwardLastBackward kernels used by TTensorLogic:Backward().
FWT_Mul / FWT_Sub / FWT_Fill / FWT_Clone / FWT_MaxDiffElementwise and utility operations.
FWT_AdamUpdate / FWT_SumSq / FWT_ScaleInPlaceAdam step, gradient norms, clipping.
FWT_GetBytes / FWT_SetBytesRaw float32 buffer as binary string (checkpointing).
FWT_OneHot( aIds, nVocab )[len,vocab] one-hot from 1-based token ids.
FWT_CausalMask( nSeq, nVal )Additive attention mask (0 on/below diagonal, nVal above; default −1e9).
FWT_SinPos( nSeq, nDim )Sinusoidal positional encoding [seq,dim].
FWT_NotEq( n ) / FWT_UnitEmb( n, d, nSeed )C-speed builders for Datalog / embedding helpers.
FWT_GatherRows / FWT_SliceCols / FWT_SetColsEmbedding lookup and column views (classic transformer path).
FWT_LoadSafe( cFile, nOffset, aShape )Read float32 tensor from a safetensors-style blob.
FWT_BlasActive().T. when an OpenBLAS sgemm backend is loaded.

OpenBLAS acceleration

fwtensor.c loads OpenBLAS dynamically at first use — never a hard dependency: libopenblas.dll on the PATH or beside the exe, or the FWT_BLAS_DLL environment variable (set it to off to force the portable C loops; ILP64 builds such as numpy's libscipy_openblas64_ are detected too). FWT_MatMul and every 2-operand FWT_EinSum reducible to a (batched) GEMM run on cblas_sgemm, using BLAS transposes so the common attention patterns need no repacking.

Pattern (Xeon W-2140B, 8c)C loopsOpenBLASPyTorch 2.11 / MKL
matmul 1024x1024x10240.32 GFLOPS497 GFLOPS982 GFLOPS
matmul 512x512x5120.32 GFLOPS290 GFLOPS806 GFLOPS
attention scores p=512 d=640.31 GFLOPS55 GFLOPS466 GFLOPS

numpy's own OpenBLAS reaches 446 GFLOPS on the 1024 matmul on the same machine — the FWH einsum matches BLAS-class performance; the remaining gap to PyTorch is MKL's AVX-512 kernels and lower per-call overhead, not the language.

Samples

Build any sample the same way:

cd /d C:\fwteam\samples\ai
C:\fwteam\samples\build_new.bat <name> hb32
<name>.exe

See Also