Skip to content

Commit ae4ab36

Browse files
committed
refactor: rewrite blt_chol_inv_mt with row-partition threading and cache-efficient layout
Replace the old model (one thread per cached column, each scanning all rows) with row-partition threading (one thread per row range, processing all cached columns simultaneously). The old model was slower than single-threaded at 4 threads. Key changes: - Row-major flat arrays [row * stride + col_slot] for tmpcol/sumcol, matching the ST layout from commit f4a828c. The inner c-loop is sequential in memory regardless of thread count. - stride = BLT_INV_CACHE_SIZE = 32, decoupled from threadcount. The inner c-loop runs 32 Fused Multiply-Add (FMAs, enabled by -mfma compiler flag from f4a828c) per (j,k) pair regardless of how many threads are used, keeping arithmetic intensity high and amortising per-k overhead. - Work-balanced static partition: prefix sum of actual per-row element counts, plus binary search for equal-work cut-points. Prevents straggler threads on non-uniform bandwidth distributions. - Per-thread spill buffer for writes to rows outside the thread's assigned range; reduced into sumcol after join. Benchmarks on combo NGA dataset (137,801 rows, 329,855,090 elements): Threads Time Speedup (old MT) 1 1005s 1.00x 4 423s 2.38x (was 1760s, 0.57x) 8 305s 3.30x (was 1182s, 0.85x) Results verified correct: max abs diff vs 1-thread baseline = 1.11022e-16. End-to-end snap timing on full NGA dataset (-t 8): Run Wall time CPU time Old MT 245m29s 1000m56s New MT 154m13s 309m21s Speedup 1.59x 3.24x less CPU The CPU time collapse (1001m => 309m) is the clearest signal: the old per-column model generated ~4x more CPU work than the new row-partition model at the same thread count due to cache thrashing. Correctness check (nga.lst diff, old vs new run): residuals and adjusted coordinates are identical. 421 of ~1.8M observation lines differ, all in the uncertainty-propagation columns (computed-error, error-of-residual) at 1e-7 to 1e-8 arcsecond scale — floating-point non-determinism from MT partial-sum reduction order.
1 parent 7534f2e commit ae4ab36

1 file changed

Lines changed: 207 additions & 71 deletions

File tree

src/snaplib/util/bltmatrx_mt.c

Lines changed: 207 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
enum { BLT_UNINIT, BLT_ROWS, BLT_READY };
2020

21+
#define BLT_INV_CACHE_SIZE 32
22+
2123
// static int minrowsize = 1000;
2224
// static double small = 1.0e-10;
2325
// static double abssmall = 1.0e-30;
@@ -44,7 +46,10 @@ static int blt_get_number_of_threads()
4446
return threadcount;
4547
}
4648

47-
static void blt_load_col_cache_mt( bltmatrix *blt, double **tmpcol, double **sumcol,
49+
/* tmpcol and sumcol are row-major flat arrays: [row * stride + col_slot].
50+
stride == threadcount (maximum column slots per batch). */
51+
static void blt_load_col_cache_mt( bltmatrix *blt, double *tmpcol, double *sumcol,
52+
const int stride,
4853
int *dosum, int iget, int nget, int isave, int nsave )
4954
{
5055

@@ -57,6 +62,8 @@ static void blt_load_col_cache_mt( bltmatrix *blt, double **tmpcol, double **sum
5762
int c0;
5863
double *row = blt->row[i].address;
5964
double *r;
65+
double *sc_i = sumcol + i * stride;
66+
double *tc_i = tmpcol + i * stride;
6067

6168
/* If there are rows to be saved, then do so */
6269

@@ -65,7 +72,7 @@ static void blt_load_col_cache_mt( bltmatrix *blt, double **tmpcol, double **sum
6572
for( c0 = dosum[i], r = row+isave+c0-col0; c0 < nsave; c0++, r++ )
6673
{
6774
if( isave+c0 > i ) break;
68-
*r = sumcol[c0][i];
75+
*r = sc_i[c0];
6976
}
7077
}
7178

@@ -74,129 +81,259 @@ static void blt_load_col_cache_mt( bltmatrix *blt, double **tmpcol, double **sum
7481
dosum[i] = c0 = col0 < iget ? 0 : col0-iget;
7582
if( c0 >= nget ) continue;
7683

77-
/* Retrieve the cols and initiallize the summation */
84+
/* Retrieve the cols and initialise the summation */
7885

7986
for( r = row + iget + c0 - col0; c0 < nget; c0++, r++ )
8087
{
8188
if( iget + c0 > i ) break;
82-
tmpcol[c0][i] = *r;
83-
sumcol[c0][i] = 0.0;
89+
tc_i[c0] = *r;
90+
sc_i[c0] = 0.0;
8491
}
8592
}
8693
}
8794

88-
static void blt_chol_inv_mt_sumcol( bltmatrix *blt, int *dosum, double *sumcol, double *tmpcol, int i1, int c )
95+
/* Accumulates the contribution of BLT rows [j_start, j_end) into sumcol via
96+
* the Cholesky inversion recurrence.
97+
*
98+
* dosum[r] – index of the first active column slot for BLT row r.
99+
* tmpcol – row-major flat array [row * stride + col_slot], read-only.
100+
* sumcol – row-major flat array [row * stride + col_slot]; this thread
101+
* owns rows k in [j_start, j_end) and writes there directly
102+
* (no race — exclusive ownership).
103+
* spill – private row-major flat array [(k - spill_base) * stride + col_slot]
104+
* for cross-thread writes (k < j_start); null for thread 0 (unused).
105+
* Caller accumulates into sumcol after all threads join.
106+
* stride – column slots per row (= threadcount).
107+
* spill_base – base row index for spill indexing (= i1+1 for the batch).
108+
*
109+
* Returns the minimum k written into spill, or j_start if no spill writes
110+
* occurred, so the caller can bound the accumulation to [k_min, j_start).
111+
*/
112+
static int blt_chol_inv_mt_rowrange( bltmatrix *blt, const int *dosum,
113+
const double *tmpcol,
114+
double *sumcol, double *spill,
115+
const int spill_base,
116+
const int ncols, const int stride,
117+
const int i1,
118+
const int j_start, const int j_end )
89119
{
90-
int nrow = blt->nrow;
91-
for (int j=i1+1; j < nrow; j++ )
120+
int k_min = j_start;
121+
for( int j = j_start; j < j_end; j++ )
92122
{
93123
int col0 = blt->row[j].col;
94124
double *row = blt->row[j].address;
95125

96126
if( col0 < i1+1 )
97127
{
98-
row += i1+1-col0;
128+
row += i1+1 - col0;
99129
col0 = i1+1;
100130
}
101131

102-
double sj=0;
103-
for( ; col0 <= j; col0++, row++ )
132+
const double *tc_j = tmpcol + j * stride;
133+
double *sc_j = sumcol + j * stride;
134+
135+
for( int k = col0; k <= j; k++, row++ )
104136
{
105-
/* Sum effect of (j,col0) element, value at *row */
106-
if( c >= dosum[col0] && c >= dosum[j] )
137+
double elem = *row;
138+
int c_start = dosum[k] > dosum[j] ? dosum[k] : dosum[j];
139+
const double *tc_k = tmpcol + k * stride;
140+
if( k >= j_start )
141+
{
142+
double *sc_k = sumcol + k * stride;
143+
if( j != k )
107144
{
108-
sumcol[col0] -= *row * tmpcol[j];
109-
if( j != col0 )
145+
for( int c = c_start; c < ncols; c++ )
110146
{
111-
sj -= *row * tmpcol[col0];
147+
sc_k[c] -= elem * tc_j[c];
148+
sc_j[c] -= elem * tc_k[c];
112149
}
113150
}
151+
else
152+
{
153+
for( int c = c_start; c < ncols; c++ )
154+
sc_k[c] -= elem * tc_j[c];
155+
}
156+
}
157+
else /* k < j_start: cross-thread, accumulate into spill */
158+
{
159+
if( k < k_min ) k_min = k;
160+
double *sp_k = spill + (k - spill_base) * stride;
161+
for( int c = c_start; c < ncols; c++ )
162+
{
163+
sp_k[c] -= elem * tc_j[c];
164+
sc_j[c] -= elem * tc_k[c];
165+
}
166+
}
114167
}
115-
sumcol[j] += sj;
116168
}
169+
return k_min;
117170
}
118171

119172
void blt_chol_inv_mt( bltmatrix *blt )
120173
{
121-
int nrow;
122-
int nsave;
123-
int i,i0,i1,c,c1,j;
124-
double *tmp;
125-
double **tmpcol;
126-
double **sumcol;
127-
int *dosum;
128-
long ndone;
129-
130-
int threadcount=blt_get_number_of_threads();
174+
const int threadcount = blt_get_number_of_threads();
131175
if( threadcount < 2 )
132176
{
133177
blt_chol_inv(blt);
134178
return;
135179
}
136180

137-
nrow = blt->nrow;
138-
139-
tmp = (double *) check_malloc( 2 * nrow * threadcount * sizeof(double) );
140-
tmpcol = (double **) check_malloc( 2 * threadcount * sizeof(double *) );
141-
sumcol=tmpcol+threadcount;
181+
const int nrow = blt->nrow;
182+
const int stride = BLT_INV_CACHE_SIZE;
183+
184+
/* Row-major layout: [row * stride + col_slot], so the inner loop over
185+
col_slot is sequential — matches ST commit f4a828c2. stride is fixed at
186+
BLT_INV_CACHE_SIZE (not threadcount) so the inner c loop runs 32 iterations
187+
regardless of thread count, amortising per-k overhead and keeping
188+
arithmetic intensity high. */
189+
std::vector<double> tmp(2 * nrow * stride);
190+
double * const tmpcol_flat = tmp.data();
191+
double * const sumcol_flat = tmp.data() + nrow * stride;
192+
193+
std::vector<int> dosum(nrow);
194+
195+
/* work_prefix[j] = cumulative element count for rows 0..j-1, used to
196+
partition rows by work rather than by count so threads get equal-effort
197+
ranges. Uses unclamped blt->row[j].col (ignores spill_base clamping),
198+
which overestimates actual work for early batches — safe since it only
199+
causes cut-points to land slightly early, never out of range. */
200+
std::vector<long> work_prefix(nrow + 1);
201+
work_prefix[0] = 0;
202+
for( int j = 0; j < nrow; j++ )
203+
work_prefix[j+1] = work_prefix[j] + j - blt->row[j].col;
204+
205+
/* Global work-balanced partition: j_splits[c] is the first row of thread c
206+
when spill_base==0. Computed once here and clamped per-batch. The spill
207+
buffer must be sized from these actual cut-points — a count-equal split
208+
would underallocate when skewed work pushes a cut-point beyond its
209+
count-equal equivalent. */
210+
std::vector<int> j_splits(threadcount + 1);
211+
j_splits[0] = 0;
212+
j_splits[threadcount] = nrow;
213+
{
214+
const long total_work = work_prefix[nrow];
215+
for( int c = 1; c < threadcount; c++ )
216+
{
217+
const long target = (long)c * total_work / threadcount;
218+
int lo = j_splits[c-1], hi = nrow;
219+
while( lo < hi )
220+
{
221+
const int mid = lo + (hi - lo) / 2;
222+
if( work_prefix[mid] < target ) lo = mid + 1;
223+
else hi = mid;
224+
}
225+
j_splits[c] = lo;
226+
}
227+
}
142228

143-
for( i = 0; i < threadcount; i++ )
229+
/* spill_buf holds per-thread spill accumulators for cross-thread writes
230+
(k < j_start for thread t). Thread t's spill covers rows [0, j_splits[t])
231+
at most, each row holding stride col slots. Only O(bandwidth) rows are
232+
ever written; the rest stay zero. Thread 0 has no spill and its pointer
233+
is left null. */
234+
long spill_total = 0;
235+
for( int c = 1; c < threadcount; c++ )
236+
spill_total += (long)j_splits[c] * stride;
237+
std::vector<double> spill_buf(spill_total, 0.0);
238+
std::vector<double *> thread_spill(threadcount, nullptr);
144239
{
145-
tmpcol[i] = tmp + nrow*i;
146-
sumcol[i] = tmp + nrow*(i+threadcount);
240+
double *ptr = spill_buf.data();
241+
for( int i = 1; i < threadcount; i++ )
242+
{
243+
thread_spill[i] = ptr;
244+
ptr += (long)j_splits[i] * stride;
245+
}
147246
}
148-
dosum = (int *) check_malloc( nrow * sizeof(int) );
149247

150248
init_progress_meter( blt->nelement );
151249

152-
ndone = 0;
153-
nsave = 0;
250+
long ndone = 0;
251+
int nsave = 0;
154252

155-
for (i1=nrow-1, i0=nrow-threadcount;
253+
for( int i1 = nrow-1, i0 = nrow-BLT_INV_CACHE_SIZE;
156254
i1 >= 0;
157-
i1=i0-1, i0 -= threadcount )
255+
i1 = i0-1, i0 -= BLT_INV_CACHE_SIZE )
158256
{
159-
160257
if( i0 < 0 ) i0 = 0;
161258

162259
/* Save the cached row data and update with the new values ... */
163260

164-
blt_load_col_cache_mt( blt, tmpcol, sumcol, dosum, i0, i1-i0+1, i1+1, nsave );
261+
blt_load_col_cache_mt( blt, tmpcol_flat, sumcol_flat, stride, dosum.data(),
262+
i0, i1-i0+1, i1+1, nsave );
165263
nsave = i1-i0+1;
166264

167-
/* Sum the data for the rows after i0 into the summation */
265+
/* Sum the data for the rows after i1 into the summation.
266+
Clamp the global work-balanced splits to spill_base for this batch. */
267+
268+
const int spill_base = i1+1;
269+
std::vector<int> j_starts(threadcount + 1);
270+
j_starts[0] = spill_base;
271+
j_starts[threadcount] = nrow;
272+
for( int c = 1; c < threadcount; c++ )
273+
j_starts[c] = j_splits[c] > spill_base ? j_splits[c] : spill_base;
274+
275+
std::vector<int> k_mins(threadcount);
276+
{
277+
std::vector<std::thread> threads;
278+
for( int c = 0; c < threadcount; c++ )
279+
{
280+
const int j_start = j_starts[c];
281+
const int j_end = j_starts[c+1];
282+
double * const spill_c = thread_spill[c];
283+
threads.emplace_back( [=, &k_mins]() {
284+
k_mins[c] = blt_chol_inv_mt_rowrange(
285+
blt, dosum.data(), tmpcol_flat,
286+
sumcol_flat, spill_c,
287+
spill_base, nsave, stride, i1, j_start, j_end );
288+
});
289+
}
290+
for( auto &t : threads ) t.join();
291+
}
168292

169-
std::vector<std::thread> threads;
170-
for( c = 0; c < nsave; c++ )
293+
/* Accumulate each thread's spill into sumcol and re-zero for the next
294+
batch. Thread 0 has no spill (j_start_0 = spill_base, so k >= j_start
295+
always). For threads 1..threadcount-1, only [k_mins[c], j_start_c)
296+
was written — O(bandwidth) rows rather than O(nrow). */
297+
for( int c = 1; c < threadcount; c++ )
171298
{
172-
threads.emplace_back(std::thread( blt_chol_inv_mt_sumcol,
173-
blt, dosum, sumcol[c], tmpcol[c], i1, c ));
299+
double * const sp = thread_spill[c];
300+
for( int j = k_mins[c]; j < j_starts[c]; j++ )
301+
{
302+
double * const sc_j = sumcol_flat + j * stride;
303+
double * const sp_j = sp + (j - spill_base) * stride;
304+
for( int c1 = 0; c1 < nsave; c1++ )
305+
{
306+
sc_j[c1] += sp_j[c1];
307+
sp_j[c1] = 0.0;
308+
}
309+
}
174310
}
175-
176-
for (auto &t : threads){ t.join(); }
177311

178312
/* Now process the cached columns to generate the inverse in
179313
sumcol */
180314

181-
for( c = nsave; c--; )
315+
for( int c = nsave; c--; )
182316
{
183-
double sc;
184-
int ic = i0 + c;
185-
for( j = nrow-1; j > ic; j-- )
317+
const int ic = i0 + c;
318+
double * const sc_ic = sumcol_flat + ic * stride;
319+
double * const tc_ic = tmpcol_flat + ic * stride;
320+
for( int j = nrow-1; j > ic; j-- )
186321
{
187322
/* Calculate the new element [ic,j] in sumcol */
188323
if( c < dosum[j] ) continue;
189-
sc = sumcol[c][j];
190-
sc /= tmpcol[c][ic];
191-
sumcol[c][j] = sc;
324+
double * const sc_j = sumcol_flat + j * stride;
325+
double * const tc_j = tmpcol_flat + j * stride;
326+
double sc = sc_j[c];
327+
sc /= tc_ic[c];
328+
sc_j[c] = sc;
192329
ndone++;
193330
/* Update the sums affected by this element (ic,j) */
194-
for( c1 = dosum[j]; c1 < c; c1++ )
331+
for( int c1 = dosum[j]; c1 < c; c1++ )
195332
{
196333
if( c1 >= dosum[ic] )
197334
{
198-
sumcol[c1][ic] -= sc * tmpcol[c1][j];
199-
sumcol[c1][j] -= sc * tmpcol[c1][ic];
335+
sc_ic[c1] -= sc * tc_j[c1];
336+
sc_j[c1] -= sc * tc_ic[c1];
200337
}
201338
}
202339
}
@@ -206,17 +343,19 @@ void blt_chol_inv_mt( bltmatrix *blt )
206343
sum in FPU, hence improving accuracy, and accuracy is more
207344
critical for diagonal element than for others. */
208345

209-
sc = 1.0/tmpcol[c][ic];
210-
for( j = ic+1; j < nrow; j++ )
346+
double sc = 1.0/tc_ic[c];
347+
for( int j = ic+1; j < nrow; j++ )
211348
{
212-
if( c >= dosum[j] ) sc -= sumcol[c][j] * tmpcol[c][j];
349+
double * const sc_j = sumcol_flat + j * stride;
350+
double * const tc_j = tmpcol_flat + j * stride;
351+
if( c >= dosum[j] ) sc -= sc_j[c] * tc_j[c];
213352
}
214-
sc /= tmpcol[c][ic];
215-
sumcol[c][ic] = sc;
353+
sc /= tc_ic[c];
354+
sc_ic[c] = sc;
216355

217-
for( c1 = dosum[ic]; c1 < c; c1++ )
356+
for( int c1 = dosum[ic]; c1 < c; c1++ )
218357
{
219-
sumcol[c1][ic] -= sc * tmpcol[c1][ic];
358+
sc_ic[c1] -= sc * tc_ic[c1];
220359
}
221360
}
222361

@@ -225,10 +364,7 @@ void blt_chol_inv_mt( bltmatrix *blt )
225364

226365
/* Save the last cached columns back again... */
227366

228-
blt_load_col_cache_mt( blt, tmpcol, sumcol, dosum, 0, 0, 0, nsave );
367+
blt_load_col_cache_mt( blt, tmpcol_flat, sumcol_flat, stride, dosum.data(),
368+
0, 0, 0, nsave );
229369
end_progress_meter();
230-
231-
check_free( tmpcol );
232-
check_free( tmp );
233-
check_free( dosum );
234370
}

0 commit comments

Comments
 (0)