Skip to content

Commit 87470ae

Browse files
authored
speed update tie correct (#701)
* speed update tie correct Signed-off-by: Intron7 <severin.dicks@icloud.com> * address coderabbit Signed-off-by: Intron7 <severin.dicks@icloud.com> --------- Signed-off-by: Intron7 <severin.dicks@icloud.com>
1 parent aed9008 commit 87470ae

7 files changed

Lines changed: 457 additions & 80 deletions

File tree

src/rapids_singlecell/_cuda/wilcoxon/kernels_wilcoxon_ovo.cuh

Lines changed: 308 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,19 +155,23 @@ __global__ void ovo_rank_dense_vs_ref_kernel(
155155
// LARGE/HUGE rank kernel; LARGE smem-sorts, HUGE reads CUB-sorted groups.
156156
// Post-sort mid-rank/tie body is shared and each group owns its output row.
157157
template <bool SMEM_SORT>
158-
__global__ void ovo_rank_sorted_kernel(
159-
const float* __restrict__ ref_sorted, const float* __restrict__ grp_in,
160-
const int* __restrict__ grp_offsets,
161-
const double* __restrict__ ref_tie_sums, double* __restrict__ rank_sums,
162-
double* __restrict__ tie_corr, int n_ref, int n_all_grp, int n_cols,
163-
int n_groups, bool compute_tie_corr, int large_padded, int skip_n_grp_le) {
158+
__global__ void ovo_rank_sorted_kernel(const float* __restrict__ ref_sorted,
159+
const float* __restrict__ grp_in,
160+
const int* __restrict__ grp_offsets,
161+
const double* __restrict__ ref_tie_sums,
162+
double* __restrict__ rank_sums,
163+
double* __restrict__ tie_corr, int n_ref,
164+
int n_all_grp, int n_cols, int n_groups,
165+
bool compute_tie_corr, int large_padded,
166+
int skip_n_grp_le, int skip_n_grp_gt) {
164167
int col = blockIdx.x;
165168
int grp = blockIdx.y;
166169
if (col >= n_cols || grp >= n_groups) return;
167170

168171
int g_start = grp_offsets[grp];
169172
int n_grp = grp_offsets[grp + 1] - g_start;
170173
if (n_grp <= skip_n_grp_le) return;
174+
if (n_grp > skip_n_grp_gt) return;
171175
if (n_grp == 0) {
172176
if (threadIdx.x == 0) {
173177
rank_sums[grp * n_cols + col] = 0.0;
@@ -214,6 +218,214 @@ __global__ void ovo_rank_sorted_kernel(
214218
&tie_corr[grp * n_cols + col]);
215219
}
216220

221+
// LARGE analytic-zero path for nonnegative sparse data.
222+
// Sort only stored positives; zeros are handled from counts.
223+
__global__ void ovo_rank_smem_analytic_kernel(
224+
const float* __restrict__ ref_sorted, const float* __restrict__ grp_in,
225+
const int* __restrict__ grp_offsets,
226+
const double* __restrict__ ref_tie_sums, double* __restrict__ rank_sums,
227+
double* __restrict__ tie_corr, int n_ref, int n_all_grp, int n_cols,
228+
int n_groups, bool compute_tie_corr, int skip_n_grp_le, int skip_n_grp_gt) {
229+
int col = blockIdx.x;
230+
int grp = blockIdx.y;
231+
if (col >= n_cols || grp >= n_groups) return;
232+
233+
int g_start = grp_offsets[grp];
234+
int n_grp = grp_offsets[grp + 1] - g_start;
235+
if (n_grp <= skip_n_grp_le) return;
236+
if (n_grp > skip_n_grp_gt) return;
237+
if (n_grp == 0) {
238+
if (threadIdx.x == 0) {
239+
rank_sums[grp * n_cols + col] = 0.0;
240+
if (compute_tie_corr) tie_corr[grp * n_cols + col] = 1.0;
241+
}
242+
return;
243+
}
244+
245+
const float* ref_col = ref_sorted + (long long)col * n_ref;
246+
const float* grp_col = grp_in + (long long)col * n_all_grp + g_start;
247+
248+
extern __shared__ float grp_smem[];
249+
__shared__ double warp_buf[WARP_REDUCE_BUF];
250+
__shared__ int sh_nnz;
251+
__shared__ int sh_ref_zeros;
252+
if (threadIdx.x == 0) {
253+
sh_nnz = 0;
254+
sh_ref_zeros = sorted_upper_bound(ref_col, 0, n_ref, 0.0f);
255+
}
256+
__syncthreads();
257+
258+
for (int i = threadIdx.x; i < n_grp; i += blockDim.x) {
259+
float v = grp_col[i];
260+
if (v > 0.0f) grp_smem[atomicAdd(&sh_nnz, 1)] = v;
261+
}
262+
__syncthreads();
263+
int nnz = sh_nnz;
264+
int ref_zeros = sh_ref_zeros;
265+
int n_grp_zero = n_grp - nnz;
266+
int total_zero = ref_zeros + n_grp_zero;
267+
268+
// Pad positives to a power of two for bitonic_sort_smem.
269+
int padded = 1;
270+
while (padded < nnz) padded <<= 1;
271+
for (int i = nnz + threadIdx.x; i < padded; i += blockDim.x)
272+
grp_smem[i] = __int_as_float(0x7f800000);
273+
__syncthreads();
274+
if (nnz > 1) bitonic_sort_smem(grp_smem, padded);
275+
__syncthreads();
276+
277+
// Positive ranks are shifted by group zeros, which sort before positives.
278+
double zero_rank =
279+
(total_zero > 0) ? ((double)total_zero + 1.0) / 2.0 : 0.0;
280+
double local_sum =
281+
(threadIdx.x == 0) ? (double)n_grp_zero * zero_rank : 0.0;
282+
int ref_lb = 0, ref_ub = 0, grp_lb = 0, grp_ub = 0;
283+
for (int i = threadIdx.x; i < nnz; i += blockDim.x) {
284+
OvoRank r = ovo_mid_rank(ref_col, n_ref, grp_smem, nnz, grp_smem[i],
285+
ref_lb, ref_ub, grp_lb, grp_ub);
286+
local_sum += r.mid_rank + (double)n_grp_zero;
287+
}
288+
double total = wilcoxon_block_sum(local_sum, warp_buf);
289+
if (threadIdx.x == 0) rank_sums[grp * n_cols + col] = total;
290+
291+
if (!compute_tie_corr) return;
292+
__syncthreads();
293+
294+
// Add nonzero tie deltas; zero ties are added below via T(ref+grp)-T(ref).
295+
double local_tie = 0.0;
296+
for (int i = threadIdx.x; i < nnz; i += blockDim.x) {
297+
if (i == 0 || grp_smem[i] != grp_smem[i - 1]) {
298+
float v = grp_smem[i];
299+
int gub = sorted_upper_bound(grp_smem, i + 1, nnz, v);
300+
double cg = (double)(gub - i);
301+
int rlo = sorted_lower_bound(ref_col, 0, n_ref, v);
302+
int rub = sorted_upper_bound(ref_col, rlo, n_ref, v);
303+
double cr = (double)(rub - rlo);
304+
double group_tie = (cg > 1.0) ? (cg * cg * cg - cg) : 0.0;
305+
local_tie += group_tie;
306+
if (cr > 0.0) {
307+
double comb = cr + cg;
308+
double ref_tie = (cr > 1.0) ? (cr * cr * cr - cr) : 0.0;
309+
local_tie += comb * comb * comb - comb - ref_tie - group_tie;
310+
}
311+
}
312+
}
313+
double tie = wilcoxon_block_sum(local_tie, warp_buf);
314+
if (threadIdx.x == 0) {
315+
double zd = 0.0;
316+
if (total_zero > 1)
317+
zd += (double)total_zero * total_zero * total_zero - total_zero;
318+
if (ref_zeros > 1)
319+
zd -= (double)ref_zeros * ref_zeros * ref_zeros - ref_zeros;
320+
tie_corr[grp * n_cols + col] =
321+
finalize_tie_corr(n_ref + n_grp, ref_tie_sums[col] + tie + zd);
322+
}
323+
}
324+
325+
// Compact HUGE-band positives and emit [base, base + nnz) segments.
326+
__global__ void compact_huge_nonzeros_kernel(
327+
const float* __restrict__ grp_dense, const int* __restrict__ grp_offsets,
328+
const int* __restrict__ group_ids, float* __restrict__ grp_nz,
329+
int* __restrict__ seg_begins, int* __restrict__ seg_ends, int n_all_grp,
330+
int n_sort_groups, int sb_cols) {
331+
int col = blockIdx.x;
332+
int local = blockIdx.y;
333+
if (col >= sb_cols || local >= n_sort_groups) return;
334+
int g = group_ids[local];
335+
int g_start = grp_offsets[g];
336+
int n_grp = grp_offsets[g + 1] - g_start;
337+
size_t base = (size_t)col * n_all_grp + g_start;
338+
int f = col * n_sort_groups + local;
339+
340+
__shared__ int cnt;
341+
if (threadIdx.x == 0) cnt = 0;
342+
__syncthreads();
343+
for (int i = threadIdx.x; i < n_grp; i += blockDim.x) {
344+
float v = grp_dense[base + i];
345+
if (v > 0.0f) grp_nz[base + atomicAdd(&cnt, 1)] = v;
346+
}
347+
__syncthreads();
348+
if (threadIdx.x == 0) {
349+
seg_begins[f] = (int)base;
350+
seg_ends[f] = (int)base + cnt;
351+
}
352+
}
353+
354+
// Rank HUGE-band groups from sorted positives plus the zero block.
355+
__global__ void ovo_rank_huge_analytic_kernel(
356+
const float* __restrict__ ref_sorted,
357+
const float* __restrict__ grp_nz_sorted,
358+
const int* __restrict__ grp_offsets, const int* __restrict__ group_ids,
359+
const int* __restrict__ seg_begins, const int* __restrict__ seg_ends,
360+
const double* __restrict__ ref_tie_sums, double* __restrict__ rank_sums,
361+
double* __restrict__ tie_corr, int n_ref, int n_all_grp, int n_cols,
362+
int n_sort_groups, bool compute_tie_corr) {
363+
int col = blockIdx.x;
364+
int local = blockIdx.y;
365+
if (col >= n_cols || local >= n_sort_groups) return;
366+
int grp = group_ids[local];
367+
int g_start = grp_offsets[grp];
368+
int n_grp = grp_offsets[grp + 1] - g_start;
369+
int f = col * n_sort_groups + local;
370+
int b = seg_begins[f];
371+
int nnz = seg_ends[f] - b;
372+
const float* nz = grp_nz_sorted + b;
373+
const float* ref_col = ref_sorted + (long long)col * n_ref;
374+
375+
__shared__ double warp_buf[WARP_REDUCE_BUF];
376+
__shared__ int sh_ref_zeros;
377+
if (threadIdx.x == 0)
378+
sh_ref_zeros = sorted_upper_bound(ref_col, 0, n_ref, 0.0f);
379+
__syncthreads();
380+
int ref_zeros = sh_ref_zeros;
381+
int n_grp_zero = n_grp - nnz;
382+
int total_zero = ref_zeros + n_grp_zero;
383+
double zero_rank =
384+
(total_zero > 0) ? ((double)total_zero + 1.0) / 2.0 : 0.0;
385+
386+
double local_sum =
387+
(threadIdx.x == 0) ? (double)n_grp_zero * zero_rank : 0.0;
388+
int ref_lb = 0, ref_ub = 0, grp_lb = 0, grp_ub = 0;
389+
for (int i = threadIdx.x; i < nnz; i += blockDim.x) {
390+
OvoRank r = ovo_mid_rank(ref_col, n_ref, nz, nnz, nz[i], ref_lb, ref_ub,
391+
grp_lb, grp_ub);
392+
local_sum += r.mid_rank + (double)n_grp_zero;
393+
}
394+
double total = wilcoxon_block_sum(local_sum, warp_buf);
395+
if (threadIdx.x == 0) rank_sums[grp * n_cols + col] = total;
396+
397+
if (!compute_tie_corr) return;
398+
__syncthreads();
399+
double local_tie = 0.0;
400+
for (int i = threadIdx.x; i < nnz; i += blockDim.x) {
401+
if (i == 0 || nz[i] != nz[i - 1]) {
402+
float v = nz[i];
403+
int gub = sorted_upper_bound(nz, i + 1, nnz, v);
404+
double cg = (double)(gub - i);
405+
int rlo = sorted_lower_bound(ref_col, 0, n_ref, v);
406+
int rub = sorted_upper_bound(ref_col, rlo, n_ref, v);
407+
double cr = (double)(rub - rlo);
408+
double group_tie = (cg > 1.0) ? (cg * cg * cg - cg) : 0.0;
409+
local_tie += group_tie;
410+
if (cr > 0.0) {
411+
double comb = cr + cg;
412+
double ref_tie = (cr > 1.0) ? (cr * cr * cr - cr) : 0.0;
413+
local_tie += comb * comb * comb - comb - ref_tie - group_tie;
414+
}
415+
}
416+
}
417+
double tie = wilcoxon_block_sum(local_tie, warp_buf);
418+
if (threadIdx.x == 0) {
419+
double zd = 0.0;
420+
if (total_zero > 1)
421+
zd += (double)total_zero * total_zero * total_zero - total_zero;
422+
if (ref_zeros > 1)
423+
zd -= (double)ref_zeros * ref_zeros * ref_zeros - ref_zeros;
424+
tie_corr[grp * n_cols + col] =
425+
finalize_tie_corr(n_ref + n_grp, ref_tie_sums[col] + tie + zd);
426+
}
427+
}
428+
217429
// MEDIUM tie helper: sorted-reference contribution, one block per column.
218430
// Rank kernels add only group-only/ref-overlap deltas.
219431
__global__ void ref_tie_sum_kernel(const float* __restrict__ ref_sorted,
@@ -318,6 +530,93 @@ __global__ void ovo_rank_medium_kernel(
318530
finalize_tie_corr(n_ref + n_grp, ref_tie_sums[col] + tie_delta);
319531
}
320532

321-
// WARP/SMALL tiers were removed; MEDIUM now covers all groups <=
322-
// OVO_MEDIUM_MAX. Restore notes live in
323-
// .claude/wilcoxon-warp-small-tiers-removed.md.
533+
// MEDIUM analytic-zero path for nonnegative sparse data.
534+
__global__ void ovo_rank_medium_analytic_kernel(
535+
const float* __restrict__ ref_sorted, const float* __restrict__ grp_dense,
536+
const int* __restrict__ grp_offsets,
537+
const double* __restrict__ ref_tie_sums, double* __restrict__ rank_sums,
538+
double* __restrict__ tie_corr, int n_ref, int n_all_grp, int n_cols,
539+
int n_groups, bool compute_tie_corr, int skip_n_grp_le, int max_n_grp_le) {
540+
int col = blockIdx.x;
541+
int grp = blockIdx.y;
542+
if (col >= n_cols || grp >= n_groups) return;
543+
544+
int g_start = grp_offsets[grp];
545+
int n_grp = grp_offsets[grp + 1] - g_start;
546+
if (n_grp <= skip_n_grp_le || n_grp > max_n_grp_le) return;
547+
548+
extern __shared__ char smem_raw[];
549+
float* grp_smem = (float*)smem_raw;
550+
double* warp_buf = (double*)(smem_raw + max_n_grp_le * sizeof(float));
551+
__shared__ int sh_nnz;
552+
__shared__ int sh_ref_zeros;
553+
554+
const float* ref_col = ref_sorted + (long long)col * n_ref;
555+
const float* grp_col = grp_dense + (long long)col * n_all_grp + g_start;
556+
if (threadIdx.x == 0) {
557+
sh_nnz = 0;
558+
sh_ref_zeros = sorted_upper_bound(ref_col, 0, n_ref, 0.0f);
559+
}
560+
__syncthreads();
561+
for (int i = threadIdx.x; i < n_grp; i += blockDim.x) {
562+
float v = grp_col[i];
563+
if (v > 0.0f) grp_smem[atomicAdd(&sh_nnz, 1)] = v;
564+
}
565+
__syncthreads();
566+
int nnz = sh_nnz;
567+
int ref_zeros = sh_ref_zeros;
568+
int n_grp_zero = n_grp - nnz;
569+
int total_zero = ref_zeros + n_grp_zero;
570+
double zero_rank =
571+
(total_zero > 0) ? ((double)total_zero + 1.0) / 2.0 : 0.0;
572+
573+
double local_sum =
574+
(threadIdx.x == 0) ? (double)n_grp_zero * zero_rank : 0.0;
575+
double local_tie = 0.0;
576+
for (int i = threadIdx.x; i < nnz; i += blockDim.x) {
577+
float v = grp_smem[i];
578+
int n_lt_ref = sorted_lower_bound(ref_col, 0, n_ref, v);
579+
int n_eq_ref =
580+
sorted_upper_bound(ref_col, n_lt_ref, n_ref, v) - n_lt_ref;
581+
int n_lt_grp = 0;
582+
int n_eq_grp = 0;
583+
bool first_in_grp = true;
584+
for (int j = 0; j < nnz; ++j) {
585+
float w = grp_smem[j];
586+
if (w < v) ++n_lt_grp;
587+
if (w == v) {
588+
++n_eq_grp;
589+
if (j < i) first_in_grp = false;
590+
}
591+
}
592+
local_sum += (double)(n_lt_ref + n_lt_grp + n_grp_zero) +
593+
((double)(n_eq_ref + n_eq_grp) + 1.0) / 2.0;
594+
if (compute_tie_corr && first_in_grp) {
595+
double cg = (double)n_eq_grp;
596+
double cr = (double)n_eq_ref;
597+
double group_tie = (cg > 1.0) ? (cg * cg * cg - cg) : 0.0;
598+
local_tie += group_tie;
599+
if (cr > 0.0) {
600+
double combined = cr + cg;
601+
double ref_tie = (cr > 1.0) ? (cr * cr * cr - cr) : 0.0;
602+
local_tie += combined * combined * combined - combined -
603+
ref_tie - group_tie;
604+
}
605+
}
606+
}
607+
double total = wilcoxon_block_sum(local_sum, warp_buf);
608+
if (threadIdx.x == 0) rank_sums[grp * n_cols + col] = total;
609+
610+
if (!compute_tie_corr) return;
611+
__syncthreads();
612+
double tie = wilcoxon_block_sum(local_tie, warp_buf);
613+
if (threadIdx.x == 0) {
614+
double zd = 0.0;
615+
if (total_zero > 1)
616+
zd += (double)total_zero * total_zero * total_zero - total_zero;
617+
if (ref_zeros > 1)
618+
zd -= (double)ref_zeros * ref_zeros * ref_zeros - ref_zeros;
619+
tie_corr[grp * n_cols + col] =
620+
finalize_tie_corr(n_ref + n_grp, ref_tie_sums[col] + tie + zd);
621+
}
622+
}

src/rapids_singlecell/_cuda/wilcoxon/wilcoxon.cu

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,13 @@ static void launch_ovo_rank_dense_tiered_unsorted_ref(
366366
"dense OVO group offsets D2H");
367367
auto t1 = make_ovo_tier_plan(h_offsets.data(), n_groups);
368368
int max_grp_size = t1.max_grp_size;
369-
bool run_large = t1.above_medium && t1.run_large;
370-
bool run_huge = t1.above_medium && !run_large;
369+
bool run_huge = compute_tie_corr && t1.run_huge;
371370

372371
std::vector<int> h_sort_group_ids;
373372
int n_sort_groups = n_groups;
374373
if (run_huge) {
375374
h_sort_group_ids =
376-
make_sort_group_ids(h_offsets.data(), n_groups, OVO_MEDIUM_MAX);
375+
make_sort_group_ids(h_offsets.data(), n_groups, t1.huge_skip_le);
377376
n_sort_groups = (int)h_sort_group_ids.size();
378377
}
379378

@@ -507,7 +506,8 @@ static void launch_ovo_rank_dense_tiered_unsorted_ref(
507506
ovo_dispatch_tiers(ref_sub, grp_sub, grp_offsets, t1, sc,
508507
d_sort_group_ids, n_sort_groups, grp_cub_temp_bytes,
509508
sb_grp_items_actual, tpb_rank, n_ref, n_all_grp,
510-
sb_cols, n_groups, compute_tie_corr, stream);
509+
sb_cols, n_groups, compute_tie_corr,
510+
/*analytic_zeros=*/false, stream);
511511

512512
cuda_check(
513513
cudaMemcpy2DAsync(rank_sums + col, n_cols * sizeof(double),
@@ -552,14 +552,13 @@ static void launch_ovo_rank_dense_host_streaming(
552552

553553
auto tier_plan = make_ovo_tier_plan(h_grp_offsets, n_groups);
554554
int max_grp_size = tier_plan.max_grp_size;
555-
bool run_large = tier_plan.above_medium && tier_plan.run_large;
556-
bool run_huge = tier_plan.above_medium && !run_large;
555+
bool run_huge = compute_tie_corr && tier_plan.run_huge;
557556

558557
std::vector<int> h_sort_group_ids;
559558
int n_sort_groups = n_groups;
560559
if (run_huge) {
561-
h_sort_group_ids =
562-
make_sort_group_ids(h_grp_offsets, n_groups, OVO_MEDIUM_MAX);
560+
h_sort_group_ids = make_sort_group_ids(h_grp_offsets, n_groups,
561+
tier_plan.huge_skip_le);
563562
n_sort_groups = (int)h_sort_group_ids.size();
564563
}
565564

@@ -794,7 +793,8 @@ static void launch_ovo_rank_dense_host_streaming(
794793
ovo_dispatch_tiers(ref_sub, grp_sub, d_grp_offsets, tier_plan, sc,
795794
d_sort_group_ids, n_sort_groups, grp_cub_temp_bytes,
796795
sb_grp_items_actual, tpb_rank, n_ref, n_all_grp,
797-
sb_cols, n_groups, compute_tie_corr, stream);
796+
sb_cols, n_groups, compute_tie_corr,
797+
/*analytic_zeros=*/false, stream);
798798

799799
cuda_check(
800800
cudaMemcpy2DAsync(rank_sums + col, n_cols * sizeof(double),

0 commit comments

Comments
 (0)