Skip to content

Commit 2fbc9d4

Browse files
committed
feat: add solveOnly() for chord Newton — reuse cached LU factorization
New method that skips equilibration, scatter, and factorLU. Just does: permute RHS → solveLU (reuse cached factors) → iterative refinement. For chord Newton in NR loops: the caller recomputes only the residual f with updated voltages, then calls solveOnly to solve J*delta = -f using the Jacobian factorization from the first NR iteration. Saves ~35ms per chord iteration on c6288 (skip 40ms model eval + 8ms factorLU). The csr_data parameter is still needed for the f64 SpMV in iterative refinement (residual computed with original matrix, not the factored one). Tested: 3x3 matrix, solveOnly matches fresh solve within 2.2e-16. C API: sprux_ffi_solve_only() added. Co-developed-by: Claude Code v2.1.81 (claude-opus-4-6)
1 parent 6286df3 commit 2fbc9d4

5 files changed

Lines changed: 219 additions & 0 deletions

File tree

sprux/sprux/SpruxFFISolver.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,132 @@ int SpruxFFISolver::solve(const double* csr_data, const double* rhs, double* x_o
504504
return itersUsedCpu;
505505
}
506506

507+
// ---------------------------------------------------------------------------
508+
// solveOnly(): reuse cached factorization for chord Newton.
509+
// No equilibrate, no scatter, no factorLU — just permute RHS + solveLU + refine.
510+
// Uses equilibration scales and factored data from the most recent solve().
511+
// ---------------------------------------------------------------------------
512+
513+
int SpruxFFISolver::solveOnly(const double* csr_data, const double* rhs, double* x_out) {
514+
auto& d = *impl_;
515+
const int64_t n = d.n;
516+
const auto& perm = *d.perm;
517+
518+
// Use equilibration scales from the most recent solve() (stored in d.rowScale/colScale
519+
// for CPU path, or in the last-used slot for Metal path).
520+
521+
#ifdef SPRUX_USE_METAL
522+
if (d.useMetal) {
523+
auto& symCtx = d.solver->internalSymbolicContext();
524+
auto& metalCtx = MetalContext::instance();
525+
526+
// The last completed slot has the factored data (solve() calls begin+end, swaps curSlot).
527+
int factSlot = 1 - d.curSlot;
528+
auto& slot = d.slots[factSlot];
529+
530+
// Permute RHS using the cached equilibration scales from the factored solve
531+
for (int64_t j = 0; j < n; j++) {
532+
slot.xGpu.ptr()[perm[j]] = float(slot.rowScale[j] * rhs[d.preproc.rowPerm[j]]);
533+
}
534+
535+
// GPU solve only (no factor) — reuse factored dataGpu + devPivots
536+
void* cmdBuf = metalCtx.createCommandBuffer();
537+
void* encoder = metalCtx.createComputeEncoder(cmdBuf);
538+
symCtx.setExternalEncoder(cmdBuf, encoder);
539+
540+
d.solver->solveLU(slot.dataGpu.ptr(), slot.devPivots.ptr(), slot.xGpu.ptr(), n, 1,
541+
*d.solveCtx, PivotLocation::Device);
542+
543+
// Iterative refinement with the NEW csr_data (for accurate f64 SpMV)
544+
std::fill(slot.xAccum.begin(), slot.xAccum.end(), 0.0);
545+
double bNormSq = 0.0;
546+
for (int64_t j = 0; j < n; j++) bNormSq += rhs[j] * rhs[j];
547+
548+
int itersUsed = 0;
549+
for (int iter = 0; iter < d.maxRefine; iter++) {
550+
symCtx.clearExternalEncoder();
551+
552+
for (int64_t j = 0; j < n; j++) {
553+
slot.xAccum[j] += slot.colScale[j] * double(slot.xGpu.ptr()[perm[j]]);
554+
}
555+
556+
double resNormSq = 0.0;
557+
for (int64_t j = 0; j < n; j++) {
558+
int64_t srcRow = d.preproc.rowPerm[j];
559+
double sum = 0.0;
560+
for (int64_t k = d.csrIndptr[srcRow]; k < d.csrIndptr[srcRow + 1]; k++) {
561+
sum += csr_data[k] * slot.xAccum[d.csrIndices[k]];
562+
}
563+
double residual = rhs[srcRow] - sum;
564+
resNormSq += residual * residual;
565+
slot.xGpu.ptr()[perm[j]] = float(slot.rowScale[j] * residual);
566+
}
567+
itersUsed = iter + 1;
568+
569+
if (d.refineTol > 0 && resNormSq <= d.refineTol * d.refineTol * std::max(bNormSq, 1e-300)) {
570+
break;
571+
}
572+
573+
void* newCmdBuf = metalCtx.createCommandBuffer();
574+
void* newEncoder = metalCtx.createComputeEncoder(newCmdBuf);
575+
symCtx.setExternalEncoder(newCmdBuf, newEncoder);
576+
577+
d.solver->solveLU(slot.dataGpu.ptr(), slot.devPivots.ptr(), slot.xGpu.ptr(), n, 1,
578+
*d.solveCtx, PivotLocation::Device);
579+
}
580+
581+
symCtx.clearExternalEncoder();
582+
583+
for (int64_t j = 0; j < n; j++) {
584+
x_out[j] = slot.xAccum[j] + slot.colScale[j] * double(slot.xGpu.ptr()[perm[j]]);
585+
}
586+
return itersUsed;
587+
}
588+
#endif
589+
590+
// CPU fallback — reuse cached dataCpu + pivotsCpu (from last solve())
591+
auto& bp = d.bpCpu;
592+
for (int64_t j = 0; j < n; j++) {
593+
bp[perm[j]] = float(d.rowScale[j] * rhs[d.preproc.rowPerm[j]]);
594+
}
595+
d.solver->solveLU(d.dataCpu.data(), d.pivotsCpu.data(), bp.data(), n, 1);
596+
597+
for (int64_t j = 0; j < n; j++) {
598+
x_out[j] = d.colScale[j] * double(bp[perm[j]]);
599+
}
600+
601+
double bNormSq = 0.0;
602+
for (int64_t j = 0; j < n; j++) bNormSq += rhs[j] * rhs[j];
603+
int itersUsed = 0;
604+
605+
for (int iter = 0; iter < d.maxRefine; iter++) {
606+
std::fill(bp.begin(), bp.end(), 0.0f);
607+
double resNormSq = 0.0;
608+
for (int64_t j = 0; j < n; j++) {
609+
int64_t srcRow = d.preproc.rowPerm[j];
610+
double sum = 0.0;
611+
for (int64_t k = d.csrIndptr[srcRow]; k < d.csrIndptr[srcRow + 1]; k++) {
612+
sum += csr_data[k] * x_out[d.csrIndices[k]];
613+
}
614+
double residual = rhs[srcRow] - sum;
615+
resNormSq += residual * residual;
616+
bp[perm[j]] = float(d.rowScale[j] * residual);
617+
}
618+
itersUsed = iter + 1;
619+
620+
if (d.refineTol > 0 && resNormSq <= d.refineTol * d.refineTol * std::max(bNormSq, 1e-300)) {
621+
break;
622+
}
623+
624+
d.solver->solveLU(d.dataCpu.data(), d.pivotsCpu.data(), bp.data(), n, 1);
625+
626+
for (int64_t j = 0; j < n; j++) {
627+
x_out[j] += d.colScale[j] * double(bp[perm[j]]);
628+
}
629+
}
630+
return itersUsed;
631+
}
632+
507633
// ---------------------------------------------------------------------------
508634
// dot(): sparse matrix-vector multiply (CPU, f64, no permutation)
509635
// ---------------------------------------------------------------------------

sprux/sprux/SpruxFFISolver.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,27 @@ class SpruxFFISolver {
9191
void beginSolve(const double* csr_data, const double* rhs);
9292
int endSolve(double* x_out);
9393

94+
/**
95+
* Solve with the previously factored matrix (chord Newton).
96+
*
97+
* Reuses the factored LU data from the most recent solve() or beginSolve()
98+
* call. No equilibration, no scatter, no refactorization — just:
99+
* 1. Permute RHS by BTF + AMD ordering + cached equilibration scales
100+
* 2. GPU solveLU (forward/backward substitution)
101+
* 3. CPU f64 iterative refinement
102+
* 4. Unpermute result
103+
*
104+
* For chord Newton: the caller recomputes the residual f with updated
105+
* voltages but reuses the Jacobian factorization from the first NR iteration.
106+
* The csr_data is still needed for the f64 SpMV in iterative refinement.
107+
*
108+
* @param csr_data CSR non-zero values [nnz], f64 (for refinement SpMV)
109+
* @param rhs Right-hand side vector [n], f64
110+
* @param x_out Solution vector [n], f64 (output)
111+
* @return Number of refinement iterations performed.
112+
*/
113+
int solveOnly(const double* csr_data, const double* rhs, double* x_out);
114+
94115
/**
95116
* Sparse matrix-vector multiply: b_out = A @ x (CPU, f64).
96117
*

sprux/sprux/sprux_c_api.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@ int sprux_ffi_dot(sprux_ffi_solver_t h, const double* csr_data, const double* x,
197197
}
198198
}
199199

200+
int sprux_ffi_solve_only(sprux_ffi_solver_t h, const double* csr_data, const double* rhs,
201+
double* x_out) {
202+
try {
203+
return h->solver->solveOnly(csr_data, rhs, x_out);
204+
} catch (...) {
205+
return -1;
206+
}
207+
}
208+
200209
int sprux_ffi_begin_solve(sprux_ffi_solver_t h, const double* csr_data, const double* rhs) {
201210
try {
202211
h->solver->beginSolve(csr_data, rhs);

sprux/sprux/sprux_c_api.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,25 @@ int sprux_ffi_solve(sprux_ffi_solver_t h, const double* csr_data, const double*
197197
*/
198198
int sprux_ffi_dot(sprux_ffi_solver_t h, const double* csr_data, const double* x, double* b_out);
199199

200+
/**
201+
* Solve using the previously factored matrix (chord Newton).
202+
*
203+
* Reuses the LU factorization from the most recent sprux_ffi_solve() call.
204+
* No equilibration, no scatter, no refactorization — just permute RHS,
205+
* forward/backward substitution, and iterative refinement.
206+
*
207+
* For chord Newton: the caller recomputes the residual f with updated
208+
* voltages but reuses the Jacobian factorization.
209+
*
210+
* @param h Solver handle
211+
* @param csr_data CSR non-zero values [nnz], f64 (needed for refinement SpMV)
212+
* @param rhs Right-hand side vector [n], f64
213+
* @param x_out Solution vector [n], f64 (output)
214+
* @return Number of refinement iterations, or -1 on error
215+
*/
216+
int sprux_ffi_solve_only(sprux_ffi_solver_t h, const double* csr_data, const double* rhs,
217+
double* x_out);
218+
200219
/**
201220
* Split-phase solve: submit GPU factor+solve asynchronously.
202221
*

sprux/tests/SpruxFFISolverTest.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,50 @@ TEST(SpruxFFISolver, SmallMatrix) {
223223
EXPECT_LT(relRes, 1e-5) << "Small matrix solve failed";
224224
}
225225

226+
// Test: solveOnly() reuses factored data for chord Newton
227+
TEST(SpruxFFISolver, SolveOnly) {
228+
int32_t n = 3, nnz = 7;
229+
vector<int32_t> indptr = {0, 2, 5, 7};
230+
vector<int32_t> indices = {0, 1, 0, 1, 2, 1, 2};
231+
vector<double> data = {4.0, 1.0, 1.0, 3.0, 1.0, 1.0, 4.0};
232+
233+
SpruxFFISolver solver(n, nnz, indptr.data(), indices.data(), data.data(), 10);
234+
235+
// First: full solve to establish factorization
236+
vector<double> rhs1 = {5.0, 5.0, 5.0};
237+
vector<double> x1(n);
238+
solver.solve(data.data(), rhs1.data(), x1.data());
239+
240+
// Now: solveOnly with a different RHS but same matrix (chord Newton scenario)
241+
vector<double> rhs2 = {1.0, 2.0, 3.0};
242+
vector<double> x2(n);
243+
solver.solveOnly(data.data(), rhs2.data(), x2.data());
244+
245+
// Verify x2 = A^-1 * rhs2
246+
double relRes = 0, bNorm = 0;
247+
for (int i = 0; i < n; i++) {
248+
double Ax_i = 0;
249+
for (int k = indptr[i]; k < indptr[i + 1]; k++) {
250+
Ax_i += data[k] * x2[indices[k]];
251+
}
252+
double r = Ax_i - rhs2[i];
253+
relRes += r * r;
254+
bNorm += rhs2[i] * rhs2[i];
255+
}
256+
relRes = sqrt(relRes / bNorm);
257+
cout << " solveOnly: x = [" << x2[0] << ", " << x2[1] << ", " << x2[2] << "]" << endl;
258+
cout << " solveOnly: relative residual = " << scientific << setprecision(3) << relRes << endl;
259+
EXPECT_LT(relRes, 1e-5) << "solveOnly failed";
260+
261+
// Also test solveOnly vs fresh solve — should give same result
262+
vector<double> x2_ref(n);
263+
solver.solve(data.data(), rhs2.data(), x2_ref.data());
264+
double maxDiff = 0;
265+
for (int i = 0; i < n; i++) maxDiff = max(maxDiff, abs(x2[i] - x2_ref[i]));
266+
cout << " solveOnly vs solve diff: " << scientific << setprecision(3) << maxDiff << endl;
267+
EXPECT_LT(maxDiff, 1e-10) << "solveOnly doesn't match solve";
268+
}
269+
226270
// Test: dot() computes correct SpMV
227271
TEST(SpruxFFISolver, DotProduct) {
228272
string dir = findTestDataDir("c6288_sequence");

0 commit comments

Comments
 (0)