aboutsummaryrefslogtreecommitdiff
path: root/neural_ode.c
diff options
context:
space:
mode:
authory-jan137 <yousefjan24000@gmail.com>2026-03-17 12:59:35 +0300
committery-jan137 <yousefjan24000@gmail.com>2026-03-17 12:59:35 +0300
commita0e4ff366c375aafc8cbce6baa7e8148c9fe7f9e (patch)
tree8a43f1a810742f25b44f9f6ab894c72cff1db4fc /neural_ode.c
parent06ecf5551c82da0bbbd7b45f16acb59e8e681565 (diff)
Make adjoint functions checkpointed
Diffstat (limited to 'neural_ode.c')
-rw-r--r--neural_ode.c132
1 files changed, 96 insertions, 36 deletions
diff --git a/neural_ode.c b/neural_ode.c
index 3489c82..a2d9740 100644
--- a/neural_ode.c
+++ b/neural_ode.c
@@ -392,33 +392,91 @@ typedef struct {
int nfe;
} AdjointResult;
-AdjointResult adjoint_solve(const DynMLP *net, const double *theta,
- const double *z0, double t0, double t1,
- const double *z1, const double *dL_dz1,
- double atol, double rtol) {
- (void)z0;
+typedef struct {
+ int num_checkpoints;
+ double *times; /* times[0..num_checkpoints], length num_checkpoints+1 */
+ double **states; /* states[0..num_checkpoints], state at each checkpoint time */
+ double *z1; /* separate copy of states[num_checkpoints] = z(t1) */
+ int nfe;
+} ForwardResult;
+
+static void forward_result_free(ForwardResult *fr) {
+ free(fr->times);
+ for (int i = 0; i <= fr->num_checkpoints; i++)
+ free(fr->states[i]);
+ free(fr->states);
+ free(fr->z1);
+}
+
+static ForwardResult forward_solve(const DynMLP *net, const double *theta,
+ const double *z0, double t0, double t1,
+ double atol, double rtol, int num_checkpoints) {
+ int D = net->D;
+ AdjointCtx ac = { *net, theta, D, net->nparams };
+
+ ForwardResult fr;
+ fr.num_checkpoints = num_checkpoints;
+ fr.nfe = 0;
+
+ fr.times = vec_alloc(num_checkpoints + 1);
+ fr.states = (double **)xmalloc((size_t)(num_checkpoints + 1) * sizeof(double *));
+ for (int i = 0; i <= num_checkpoints; i++)
+ fr.states[i] = vec_alloc(D);
+
+ for (int i = 0; i <= num_checkpoints; i++)
+ fr.times[i] = t0 + (t1 - t0) * (double)i / (double)num_checkpoints;
+
+ vec_copy(z0, fr.states[0], D);
+
+ for (int i = 0; i < num_checkpoints; i++) {
+ ODEResult seg = ode_solve(neural_ode_rhs, fr.states[i],
+ fr.times[i], fr.times[i + 1],
+ NULL, D, atol, rtol, &ac);
+ vec_copy(seg.y, fr.states[i + 1], D);
+ fr.nfe += seg.nfe;
+ free(seg.y);
+ }
+
+ fr.z1 = vec_alloc(D);
+ vec_copy(fr.states[num_checkpoints], fr.z1, D);
+
+ return fr;
+}
+
+static AdjointResult adjoint_solve(const DynMLP *net, const double *theta,
+ const ForwardResult *fr, const double *dL_dz1,
+ double atol, double rtol) {
int D = net->D;
int nparams = net->nparams;
int aug_dim = 2 * D + nparams;
- double *aug0 = vec_zeros(aug_dim);
- vec_copy(z1, aug0, D);
- vec_copy(dL_dz1, aug0 + D, D);
+ double *aug = vec_zeros(aug_dim);
+ vec_copy(fr->z1, aug, D);
+ vec_copy(dL_dz1, aug + D, D);
AdjointCtx ac = { *net, theta, D, nparams };
- ODEResult res = ode_solve(adjoint_dynamics, aug0, t1, t0,
- NULL, aug_dim, atol, rtol, &ac);
+ int total_nfe = 0;
+
+ for (int k = fr->num_checkpoints; k >= 1; k--) {
+ /* Replace z with stored checkpoint to prevent numerical drift */
+ vec_copy(fr->states[k], aug, D);
+
+ ODEResult seg = ode_solve(adjoint_dynamics, aug,
+ fr->times[k], fr->times[k - 1],
+ NULL, aug_dim, atol, rtol, &ac);
+ vec_copy(seg.y, aug, aug_dim);
+ total_nfe += seg.nfe;
+ free(seg.y);
+ }
AdjointResult ar;
- ar.dL_dz0 = vec_alloc(D);
+ ar.dL_dz0 = vec_alloc(D);
ar.dL_dtheta = vec_alloc(nparams);
- ar.nfe = res.nfe;
- vec_copy(res.y + D, ar.dL_dz0, D);
- vec_copy(res.y + 2 * D, ar.dL_dtheta, nparams);
+ ar.nfe = total_nfe;
+ vec_copy(aug + D, ar.dL_dz0, D);
+ vec_copy(aug + 2 * D, ar.dL_dtheta, nparams);
- free(res.y);
- free(aug0);
-
+ free(aug);
return ar;
}
@@ -432,23 +490,26 @@ typedef struct {
NeuralODEOutput neural_ode_forward_backward(const DynMLP *net, const double *theta,
const double *z0, double t0, double t1,
- const double *target, double atol, double rtol) {
+ const double *target, double atol, double rtol,
+ int num_checkpoints) {
int D = net->D;
- AdjointCtx ac = { *net, theta, D, net->nparams };
- ODEResult fwd = ode_solve(neural_ode_rhs, z0, t0, t1, NULL, D, atol, rtol, &ac);
- double *dL_dz1 = vec_alloc(D);
- for (int i = 0; i < D; i++) dL_dz1[i] = fwd.y[i] - target[i];
+ ForwardResult fr = forward_solve(net, theta, z0, t0, t1, atol, rtol, num_checkpoints);
- AdjointResult ar = adjoint_solve(net, theta, z0, t0, t1, fwd.y, dL_dz1, atol, rtol);
+ double *dL_dz1 = vec_alloc(D);
+ for (int i = 0; i < D; i++) dL_dz1[i] = fr.z1[i] - target[i];
+
+ AdjointResult ar = adjoint_solve(net, theta, &fr, dL_dz1, atol, rtol);
NeuralODEOutput out;
- out.z1 = fwd.y;
- out.dL_dz0 = ar.dL_dz0;
- out.dL_dtheta = ar.dL_dtheta;
- out.nfe_forward = fwd.nfe;
+ out.z1 = vec_alloc(D);
+ vec_copy(fr.z1, out.z1, D);
+ out.dL_dz0 = ar.dL_dz0;
+ out.dL_dtheta = ar.dL_dtheta;
+ out.nfe_forward = fr.nfe;
out.nfe_backward = ar.nfe;
+ forward_result_free(&fr);
free(dL_dz1);
return out;
}
@@ -502,10 +563,10 @@ static void adam_free(Adam *a) {
static double train_one(const DynMLP *net, const double *theta,
const double *z0, double t0, double t1,
const double *target, double *grad_accum,
- double atol, double rtol,
+ double atol, double rtol, int num_checkpoints,
int *nfe_fwd, int *nfe_bwd) {
NeuralODEOutput out = neural_ode_forward_backward(net, theta, z0, t0, t1,
- target, atol, rtol);
+ target, atol, rtol, num_checkpoints);
double loss = 0.0;
int D = net->D;
for (int i = 0; i < D; i++) {
@@ -530,14 +591,15 @@ typedef struct {
static TrainStepResult train_step(const DynMLP *net, double *theta,
const double **z0s, const double **targets,
double t0, double t1, int batch_size,
- Adam *adam, double atol, double rtol) {
+ Adam *adam, double atol, double rtol, int num_checkpoints) {
int nparams = net->nparams;
double *grad_accum = vec_zeros(nparams);
TrainStepResult res = { 0.0, 0, 0 };
for (int b = 0; b < batch_size; b++) {
res.loss += train_one(net, theta, z0s[b], t0, t1, targets[b],
- grad_accum, atol, rtol, &res.nfe_fwd, &res.nfe_bwd);
+ grad_accum, atol, rtol, num_checkpoints,
+ &res.nfe_fwd, &res.nfe_bwd);
}
res.loss /= (double)batch_size;
for (int i = 0; i < nparams; i++) grad_accum[i] /= (double)batch_size;
@@ -729,7 +791,7 @@ static void test_adjoint_gradients(RNG *r) {
for (int i = 0; i < D; i++) target[i] = rng_normal(r);
NeuralODEOutput out = neural_ode_forward_backward(&net, theta, z0, t0, t1,
- target, atol, rtol);
+ target, atol, rtol, 10);
#define FWD_LOSS(z0_, theta_) ({ \
AdjointCtx ac_ = { net, (theta_), D, np }; \
@@ -785,8 +847,6 @@ static void test_adjoint_gradients(RNG *r) {
free(theta); free(z0); free(target);
}
-
-
static void test_training(RNG *r) {
const int D = 2, H = 16;
const int N = 50, BATCH = 10, ITERS = 300;
@@ -822,7 +882,7 @@ static void test_training(RNG *r) {
batch_tgt[b] = targets[idx];
}
TrainStepResult res = train_step(&net, theta, batch_z0, batch_tgt,
- t0, t1, BATCH, &adam, atol, rtol);
+ t0, t1, BATCH, &adam, atol, rtol, 10);
if ((iter + 1) % 50 == 0)
printf("iter %3d loss=%.4f nfe_fwd=%d\n", iter + 1, res.loss, res.nfe_fwd);
}
@@ -894,7 +954,7 @@ int main(void) {
TrainStepResult res = train_step(&net, theta, batch_z0, batch_tgt,
t0, t1, BATCH, &adam,
- atol_train, rtol_train);
+ atol_train, rtol_train, 10);
if (iter % LOG_EVERY == 0) {
double test_loss = evaluate(&net, theta, &test_ds,