Detailed math notes for every tensor operation implemented in the Java makemore backend. Each section explains the forward computation, an illustrative numeric example, and how gradients propagate (matching PyTorch semantics).
- Forward:
out[i,j] = a[i,j] + b[i,j] - Example:
a = [[1,2],[3,4]],b = [[5,6],[7,8]]→out = [[6,8],[10,12]] - Backward:
dL/da = dL/dout,dL/db = dL/dout
- Forward:
out[i,j] = a[i,j] * b[i,j] - Example:
a = [[1,2],[3,4]],b = [[2,0],[1,5]]→out = [[2,0],[3,20]] - Backward:
dL/da = (dL/dout) ⊙ b,dL/db = (dL/dout) ⊙ a
- Forward:
out[i,j] = tanh(x[i,j]) - Example:
x = [0, 1]→out ≈ [0, 0.7616] - Backward:
dL/dx = (1 - tanh(x)^2) ⊙ dL/dout
- Forward:
out[i,j] = max(0, x[i,j]) - Example:
x = [-1, 0.5]→out = [0, 0.5] - Backward: Pass gradient only where
x > 0
- Forward: Broadcast a row vector across rows:
out[r,c] = matrix[r,c] + row[c] - Example:
matrix = [[1,2],[3,4]],row = [10,20]→out = [[11,22],[13,24]] - Backward: Matrix receives full gradient, row accumulates column-wise sums of
dL/dout
- Forward: Reduce along axis:
axis=0(rows) →out[0,c] = Σ_r x[r,c]
axis=1(cols) →out[r,0] = Σ_c x[r,c] - Example:
x = [[1,2],[3,4]]
axis=0→[4,6];axis=1→[3,7] - Backward: Broadcast reduced gradient back to original shape
- Forward:
mean = sum(axis)/count - Example:
x = [[1,2],[3,4]],axis=0→[2,3] - Backward: Same as sum but scaled by
1/count
- Forward: Standard matrix multiplication
(m×k) @ (k×n) = (m×n) - Example:
a = [[1,2],[3,4]],b = [[5,6],[7,8]]→[[19,22],[43,50]] - Backward:
dL/da = dL/dout @ bᵀ
dL/db = aᵀ @ dL/dout
- Forward (mirrors
CpuBackend.embeddingstep-by-step):- Flatten
indicesin row-major order (blockCount = batchSize * seqLen). - For each flattened position
i, read the integer tokenidx. - Copy the slice
weight[idx, :]intoout[i, :]. - Store
idxingatheredIdx[i]for backward. Output shape:(batchSize * seqLen, embedDim).
- Flatten
- Example:
weight = [[1,0],[0,1],[2,2]],indices = [[0,2],[1,0]]
Flattened indices[0,2,1,0]→ output rows[[1,0],[2,2],[0,1],[1,0]]. - Backward: For each output grad row
gradOut[i], look up its cached tokenidx = gatheredIdx[i]and accumulate (+=)gradOut[i]intoweightGrad[idx]. Repeated tokens therefore add up automatically (identical to PyTorch’s scatter-add). - PyTorch parity:
torch.nn.functional.embedding.
- Forward (exactly as in
CpuBackend.crossEntropy):- For each batch row
i, computemaxLogit = max_j logits[i,j]. - Compute
exp_j = exp(logits[i,j] - maxLogit), sum them tosumExp. - Normalize
prob[i,j] = exp_j / sumExpand cache it for backward. - Loss for row
i:ℓ_i = -log(prob[i, target_i]) = log(sumExp) + maxLogit - logits[i,target_i]. - Return the mean
(1/batch) Σ_i ℓ_i.
- For each batch row
- Example:
logits = [[2,0]], target=0
max=2,exp = [1, e^{-2}],sumExp ≈ 1.1353,prob ≈ [0.881, 0.119],loss ≈ 0.1278. - Backward: With cached
prob, setgrad[i,j] = prob[i,j] - 1whenjis the target, otherwiseprob[i,j]. Scale by1/batchand multiply by upstream loss grad (outGrad[0]). This matchessoftmax(logits) - one_hot(target). - PyTorch parity:
torch.nn.functional.cross_entropy.
- Forward: Checks that
newRows * newCols == oldRows * oldCols, then copies the linear buffer into a tensor wrapper with the new shape. Conceptually equivalent to PyTorch’sview. No data rearrangement beyond reinterpretation. - Example:
(2×3)matrixreshaped to[[1,2,3], [4,5,6]](3×2)becomes[[1,2], [3,4], [5,6]] - Backward: During
backward, the grad buffer from the reshape output is copied back into the parent’s grad array (same linear order), effectively an identity mapping. - PyTorch parity:
tensor.view.
- Algorithm in
CpuBackend.sgdStep:- Access the parameter’s data buffer (
double[]orfloat[]). - Access the gradient buffer of the same size.
- For every element
i, computedata[i] -= lr * grad[i]. - No momentum/weight decay is applied—this is the vanilla PyTorch SGD update.
- Access the parameter’s data buffer (
- Example:
param=[1.0, 0.5],grad=[0.2, -0.4],lr=0.1
→[0.98, 0.54]. - Note: Gradients remain stored after the update; the optimizer caller should zero them (e.g.,
SGD.zeroGrad(...)) before the next backward pass.