forked from MrBaro75/Warrior_EA
147 lines
5.5 KiB
C++
147 lines
5.5 KiB
C++
// Numerical gradient check for the fused sequence-LSTM kernels
| |||
// (CPU_LSTMSeqForward / CPU_LSTMSeqBackward in WarriorCPU.cpp).
| |||
//
| |||
// Why this exists: those kernels implement backpropagation-through-time from
| |||
// scratch. A wrong BPTT does not crash and does not look wrong in a training
| |||
// log - it just learns worse than it should, which is indistinguishable from
| |||
// "this architecture does not suit the data". The only way to know the gradient
| |||
// is right is to check it against finite differences.
| |||
//
| |||
// Loss is L = sum(k * h_k) over the final hidden state, so dL/dh_{T-1} = [0,1,2,...],
| |||
// an asymmetric vector - a symmetric one can hide index-swap bugs (e.g. reading
| |||
// gate f where i was meant).
| |||
//
| |||
// Build (from a VS x64 developer prompt, in this folder):
| |||
// cl /nologo /EHsc /O2 /std:c++17 lstm_seq_gradcheck.cpp WarriorCPU.lib
| |||
// Run: lstm_seq_gradcheck.exe -> prints max relative error, exits 0 on PASS.
| |||
| |||
#include <cstdio>
| |||
#include <cmath>
| |||
#include <vector>
| |||
#include <random>
| |||
#include "WarriorCPU.h"
| |||
| |||
static const int H = 5; // hidden units
| |||
static const int IW = 3; // per-step input width
| |||
static const int T = 4; // timesteps - must be > 1 or the recurrence is untested
| |||
| |||
struct Ctx
| |||
{
| |||
CpuHandle h;
| |||
int w, in, cg, cc, ch, out, og, wg, ig;
| |||
};
| |||
| |||
// Runs the forward pass and returns L = sum(k * h_k) over the final hidden state.
| |||
static double Forward(Ctx &c, const std::vector<double> &weights, const std::vector<double> &inputs)
| |||
{
| |||
CPU_BufferWrite(c.h, c.w, weights.data(), (int)weights.size());
| |||
CPU_BufferWrite(c.h, c.in, inputs.data(), (int)inputs.size());
| |||
if(!CPU_LSTMSeqForward(c.h, c.w, c.in, c.cg, c.cc, c.ch, c.out, H, IW, T))
| |||
{
| |||
std::printf("FAIL: SeqForward returned 0 (err=%d)\n", CPU_GetLastError(c.h));
| |||
std::exit(2);
| |||
}
| |||
std::vector<double> out((size_t)H, 0.0);
| |||
CPU_BufferRead(c.h, c.out, out.data(), H);
| |||
double loss = 0.0;
| |||
for(int k = 0; k < H; k++)
| |||
loss += k * out[k];
| |||
return loss;
| |||
}
| |||
| |||
int main()
| |||
{
| |||
Ctx c;
| |||
c.h = CPU_Init(2);
| |||
const int perGate = H * (H + IW + 1);
| |||
const int wTotal = 4 * perGate;
| |||
c.w = CPU_BufferCreate(c.h, wTotal);
| |||
c.in = CPU_BufferCreate(c.h, T * IW);
| |||
c.cg = CPU_BufferCreate(c.h, T * 4 * H);
| |||
c.cc = CPU_BufferCreate(c.h, T * H);
| |||
c.ch = CPU_BufferCreate(c.h, T * H);
| |||
c.out = CPU_BufferCreate(c.h, H);
| |||
c.og = CPU_BufferCreate(c.h, H);
| |||
c.wg = CPU_BufferCreate(c.h, wTotal);
| |||
c.ig = CPU_BufferCreate(c.h, T * IW);
| |||
| |||
std::mt19937 rng(12345);
| |||
std::uniform_real_distribution<double> dist(-0.6, 0.6);
| |||
std::vector<double> weights((size_t)wTotal), inputs((size_t)(T * IW));
| |||
for(auto &v : weights)
| |||
v = dist(rng);
| |||
for(auto &v : inputs)
| |||
v = dist(rng);
| |||
| |||
// dL/dh_{T-1} for L = sum(k * h_k)
| |||
std::vector<double> dOut((size_t)H);
| |||
for(int k = 0; k < H; k++)
| |||
dOut[k] = (double)k;
| |||
CPU_BufferWrite(c.h, c.og, dOut.data(), H);
| |||
| |||
Forward(c, weights, inputs); // populate the caches the backward pass reads
| |||
if(!CPU_LSTMSeqBackward(c.h, c.w, c.in, c.cg, c.cc, c.ch, c.og, c.wg, c.ig, H, IW, T))
| |||
{
| |||
std::printf("FAIL: SeqBackward returned 0 (err=%d)\n", CPU_GetLastError(c.h));
| |||
return 2;
| |||
}
| |||
std::vector<double> dW((size_t)wTotal, 0.0), dX((size_t)(T * IW), 0.0);
| |||
CPU_BufferRead(c.h, c.wg, dW.data(), wTotal);
| |||
CPU_BufferRead(c.h, c.ig, dX.data(), T * IW);
| |||
| |||
// Central differences. The kernels floor gate derivatives at MIN_ACTIVATION_DERIVATIVE (1e-4),
| |||
// which is a deliberate deviation from the true derivative near saturation, so a weight whose
| |||
// gate is saturated will legitimately disagree. Skip those rather than report a false failure:
| |||
// what is being verified here is the BPTT wiring - which term flows where - not the floor.
| |||
const double EPS = 1e-6;
| |||
double worstW = 0.0, worstX = 0.0;
| |||
int checkedW = 0, skipped = 0;
| |||
for(int i = 0; i < wTotal; i += 7) // stride: 4*perGate is 720 here, no need to check every one
| |||
{
| |||
double save = weights[i];
| |||
weights[i] = save + EPS;
| |||
double lp = Forward(c, weights, inputs);
| |||
weights[i] = save - EPS;
| |||
double lm = Forward(c, weights, inputs);
| |||
weights[i] = save;
| |||
double num = (lp - lm) / (2 * EPS);
| |||
double den = std::max(1.0, std::max(std::fabs(num), std::fabs(dW[i])));
| |||
double rel = std::fabs(num - dW[i]) / den;
| |||
if(rel > 1e-4 && std::fabs(num) < 1e-3 && std::fabs(dW[i]) < 1e-3)
| |||
{
| |||
skipped++; // both ~zero: dominated by the derivative floor
| |||
continue;
| |||
}
| |||
checkedW++;
| |||
if(rel > worstW)
| |||
worstW = rel;
| |||
}
| |||
for(int i = 0; i < T * IW; i++)
| |||
{
| |||
double save = inputs[i];
| |||
inputs[i] = save + EPS;
| |||
double lp = Forward(c, weights, inputs);
| |||
inputs[i] = save - EPS;
| |||
double lm = Forward(c, weights, inputs);
| |||
inputs[i] = save;
| |||
double num = (lp - lm) / (2 * EPS);
| |||
double den = std::max(1.0, std::max(std::fabs(num), std::fabs(dX[i])));
| |||
double rel = std::fabs(num - dX[i]) / den;
| |||
if(rel > worstX)
| |||
worstX = rel;
| |||
}
| |||
| |||
std::printf("checked %d weights (%d skipped near derivative floor), %d inputs\n", checkedW, skipped, T * IW);
| |||
std::printf("max relative error dW=%.3e dX=%.3e\n", worstW, worstX);
| |||
| |||
// Guard against the check trivially passing on an all-zero gradient.
| |||
double mag = 0.0;
| |||
for(double v : dX)
| |||
mag += std::fabs(v);
| |||
std::printf("sum|dX| = %.6f (must be non-zero, else nothing was tested)\n", mag);
| |||
| |||
bool pass = (worstW < 1e-5 && worstX < 1e-5 && mag > 1e-6);
| |||
std::printf("%s\n", pass ? "PASS" : "FAIL");
| |||
CPU_Shutdown(c.h);
| |||
return pass ? 0 : 1;
| |||
}
|