@@ -524,6 +524,31 @@ def get_replica_idx_list(replicas_path):
524524 return rep_list
525525
526526
527+ def _ntk_from_jacobian (jacobian_func , params ):
528+ """The NTK from an already-built jacobian function, as a host NumPy array.
529+
530+ Split out of :func:`compute_ntk` so that callers with an *epoch loop* can build
531+ the jacobian **once** and reuse it (see ``compute_eigenvalues_for_replica``).
532+
533+ Returns
534+ -------
535+ ntk : np.ndarray
536+ The NTK matrix, flattened to (nflavours * n_xgrid) x (nflavours * n_xgrid)
537+ ntk_shape : tuple
538+ Shape of the NTK matrix before flattening
539+ """
540+ jacobian = jacobian_func (params )
541+
542+ # Compute NTK (nf,ng,nf,ng) -> assumes shape from jacobian
543+ ntk = jnp .einsum ("ijk,lmk->ijlm" , jacobian , jacobian )
544+
545+ # Flatten to (nflavors * n_xgrid) × (nflavors * n_xgrid)
546+ d1 , d2 , d3 , d4 = ntk .shape # d1=nf, d2=ng, d3=nf, d4=ng
547+ ntk = ntk .reshape (d1 * d2 , d3 * d4 , order = NTK_ORDERING )
548+
549+ return np .asarray (ntk ), (d1 , d2 , d3 , d4 )
550+
551+
527552def compute_ntk (pdf_model , params , ** kwargs ):
528553 """
529554 Compute the NTK matrix given model parameters.
@@ -546,26 +571,17 @@ def compute_ntk(pdf_model, params, **kwargs):
546571 Shape of the NTK matrix
547572 """
548573 pdf_func = pdf_model .grid_values_func (XGRID , ** kwargs )
549- jacobian_func = jax .jacfwd (pdf_func )
550- jacobian = jacobian_func (params )
574+ ntk , shape = _ntk_from_jacobian (jax .jacfwd (pdf_func ), params )
551575
552- # Compute NTK (nf,ng,nf,ng) -> assumes shape from jacobian
553- ntk = jnp .einsum ("ijk,lmk->ijlm" , jacobian , jacobian )
554-
555- # Flatten to (nflavors * n_xgrid) × (nflavors * n_xgrid)
556- d1 , d2 , d3 , d4 = ntk .shape # d1=nf, d2=ng, d3=nf, d4=ng
557- ntk = ntk .reshape (d1 * d2 , d3 * d4 , order = NTK_ORDERING )
558-
559- # Materialise the NTK to host (NumPy), then drop JAX's compilation cache. Each call
576+ # The NTK is on the host by now, so drop JAX's compilation cache. Each call
560577 # rebuilds the model -> a fresh jacfwd -> a new XLA program with this epoch's weights
561578 # baked in; without clearing, those compiled programs accumulate (~0.3 GB/call). This
562579 # matters because the report recomputes the *same* snapshot epochs many times (the
563580 # eigenvector_grid is resolved per presentation leaf, and the bounded functools cache
564581 # thrashes when those epochs interleave), so the per-call leak compounds -> OOM.
565- ntk = np .asarray (ntk )
566582 jax .clear_caches ()
567583
568- return ntk , ( d1 , d2 , d3 , d4 )
584+ return ntk , shape
569585
570586
571587def compute_eigendecomposition (ntk_matrix , hermitian = True ):
@@ -650,12 +666,22 @@ def compute_eigenvalues_for_replica(
650666 if epoch in pending_epochs
651667 }
652668
669+ # Build the jacobian ONCE for this replica, not once per epoch. It depends only
670+ # on the model and ``kwargs`` -- never on the weights, which enter as an argument
671+ # -- so a single ``jit`` cache key serves every epoch: one trace, one compiled
672+ # program, reused.
673+ # Do not clear caches inside this loop: it is process-global and replicas run in a
674+ # ThreadPoolExecutor, so one thread clearing would force its siblings to recompile.
675+ # ``jac`` is released with this frame when the replica is done.
676+ pdf_func = pdf_model .grid_values_func (XGRID , ** kwargs )
677+ jac = jax .jit (jax .jacfwd (pdf_func ))
678+
653679 for epoch , param_file in param_files .items ():
654680 if max_epoch is not None and epoch > max_epoch :
655681 continue
656682 params = jnp .load (param_file )["params" ]
657683
658- ntk , shape = compute_ntk ( pdf_model , params , ** kwargs )
684+ ntk , shape = _ntk_from_jacobian ( jac , params )
659685 if ntk_shape is None :
660686 ntk_shape = shape
661687
0 commit comments