Exact two-pass QSA prompt selection (opt-in STRATA_SELECT_TWO_PASS=1)

Prompt-path block scores in single-pass TF32 with a per-block error bound,
3xTF32 rescoring only near each query's top-k cut-off, so qsa_block_topk
returns the same cells in the same order, ties included. Off unless the
variable is set; says once where it cannot run (HIP, Metal, SYCL, layer-major
path). Adds qsa_two_pass_test, qsa_two_pass_status_test and the GPU parity
test qsa_two_pass_parity.

Ported from PR 4119 (bitaccesscomau); the log line of the per-prompt counters
is shortened without dropping a field.

l620 (sm_120, IQ3_S), greedy ids identical off vs on in every pair:
prompt 128K +1.65% (n=12), 256K +4.10% (n=8), 32K -0.14% (noise).

(cherry picked from commit bbf7c55f1351d913233250fe7869642f7c2b98a1)
This commit is contained in:
bitaccesscomau authored and Niko1221 committed 2026-11-06 15:47:43 +01:00
1 parent f281e3fe26
commit 386f9959f1
11 files changed
+1860 -2

No files matched your search

+18
View File
@@ -403,6 +403,17 @@ if(STRATA_BUILD_TESTS)
target_include_directories(share_rules_test PRIVATE include)
add_test(NAME share_rules_test COMMAND share_rules_test)
# STRATA_SELECT_TWO_PASS: the bound between the single-pass and 3xTF32 QSA block scores and the cut-off rule that
# keeps the selection (header-only, no GPU)
add_executable(qsa_two_pass_test tests/prefill/qsa_two_pass_test.cpp)
target_include_directories(qsa_two_pass_test PRIVATE include)
add_test(NAME qsa_two_pass_test COMMAND qsa_two_pass_test)
# STRATA_SELECT_TWO_PASS: the request's parse and the line it prints once where the scorer cannot run (no GPU)
add_executable(qsa_two_pass_status_test tests/prefill/qsa_two_pass_status_test.cpp)
target_include_directories(qsa_two_pass_status_test PRIVATE include)
add_test(NAME qsa_two_pass_status_test COMMAND qsa_two_pass_status_test
"${CMAKE_CURRENT_BINARY_DIR}/qsa_two_pass_status_test.log")
# STRATA_PREFILL_ROUTED_STREAM: the prompt stream's issuer walk and route gate, on a simulated link and consumer
# (header-only, no GPU)
add_executable(routed_stream_test tests/prefill/routed_stream_test.cpp)
@@ -634,6 +645,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(qsa_scores_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME qsa_scores_parity COMMAND qsa_scores_parity)
set_tests_properties(qsa_scores_parity PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "STRATA_SCORES_THREAD=1")
# STRATA_SELECT_TWO_PASS: the two-pass prompt scores' selection against the tensor-core scorer's, ids and the
# scored-again blocks bitwise (sm_80+, synthetic, no model)
add_executable(qsa_two_pass_parity src/kernels/qsa_two_pass_parity.cpp)
target_include_directories(qsa_two_pass_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
target_link_libraries(qsa_two_pass_parity PRIVATE strata_kernels CUDA::cudart)
add_test(NAME qsa_two_pass_parity COMMAND qsa_two_pass_parity)
set_tests_properties(qsa_two_pass_parity PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "STRATA_SELECT_TWO_PASS=1")
endif()
# QSAROWS (docs/QSAROWS.md): what the T rows of a verification window cost in a QSA layer's decode kernels at a long
# context (block scores, top-k, attention over each row's selection, indexer append), row by row; GPU, synthetic, no
+12
View File
@@ -1119,6 +1119,18 @@ measured. "Exact" means the 10-prompt and 4K/20K identity gate with the switch o
`STRATA_DEQUANT_WIDE` (wide dequantization of Q4_K, Q5_K, Q6_K and IQ4_XS for the prompt's dense weights) is on by default on
sm_86 and sm_61, bit-exact. `STRATA_DEQUANT_WIDE=0` turns it off.
`STRATA_SELECT_TWO_PASS=1` (opt-in; sm_80+, the prompt path's tensor-core block scorer): every block is scored once in
single-pass TF32 with a per-block error bound, and again in the usual 3xTF32 only where the bound reaches the query's
top-k cut-off (a query the bound cannot settle has its 16-query tile scored again in full), so the selected cells, their
order and the ties are exactly the 3xTF32 scorer's (the bound and the rule: `include/strata/kernels/qsa_two_pass.hpp`).
On an RTX 3090 (sm_86, PCIe 3.0, 250 W; the v0.2.6 release defaults, 2 pairs of 2 runs) a 256K prompt read 5.2% faster
(1,887 to 1,984 tokens/s), a 128K prompt 2.2% faster and a 32K prompt the same within 1%; decode and the answers did not
change. Calls whose last query has `STRATA_SELECT_TWO_PASS_MIN` blocks or more take it (default 4096, 16K cells).
`STRATA_SELECT_TWO_PASS_CHECK=1` (a slow diagnostic) runs the 3xTF32 scorer beside it and prints, after each prompt, the
queries whose ids differ (must be 0). Where the scorer cannot run (HIP, Metal and SYCL builds, a card below sm_80 or
`STRATA_QSA_WARP`, the layer-major prompt path) a set switch prints one line saying it is not used there, and the
selection is unchanged.
`STRATA_PREFILL_ROUTED_STREAM=1` (opt-in; the streamed prompt walk of a chunk of 1,024 tokens or more, one GPU, the
issuer thread): the issuer keeps at most `STRATA_PREFILL_ROUTED_STREAM_DEPTH` copies (default 16) queued ahead of the
PCIe link, each layer's routing is handed to it as soon as the layer's router has run, and an expert not yet copied that
+29
View File
@@ -94,4 +94,33 @@ void qsa_block_scores_long(const float* pooled, const float* dead, const float*
/// path: pass null).
int64_t qsa_topk_multi_scratch_ints(int64_t nq);
/// STRATA_SELECT_TWO_PASS=1 (the prompt path, CUDA sm_80+; the bound and the rule: qsa_two_pass.hpp). A scores row
/// that qsa_block_topk turns into exactly the ids of qsa_block_scores_tc's row: every block is scored once in
/// single-pass TF32, the blocks whose error bound reaches the query's top-k cut-off are scored again with
/// qsa_block_scores_tc's own 3xTF32 arithmetic (bit for bit), and a query whose bound cannot be settled has its
/// 16-query tile scored again in full. The kept blocks hold their single-pass score, so the row is NOT
/// qsa_block_scores_tc's: it is for qsa_block_topk only. `scratch` (qsa_two_pass_scratch_bytes(nq_cap, max_blocks),
/// 16-byte aligned) holds the key norms qsa_two_pass_keys wrote for this layer and chunk. False (nothing launched):
/// the switch is off, HIP, a card or STRATA_QSA_WARP where qsa_block_scores_tc would not run its tensor-core kernel,
/// another geometry, nq > nq_cap, too little scratch.
bool qsa_two_pass_on(); ///< STRATA_SELECT_TWO_PASS=1, read once (HIP: false; prints once where unused)
bool qsa_two_pass_check_on(); ///< STRATA_SELECT_TWO_PASS_CHECK=1, read once (a diagnostic, see qsa_two_pass_check)
int64_t qsa_two_pass_min_blocks(); ///< STRATA_SELECT_TWO_PASS_MIN (blocks), read once; default two_pass::kMinBlocksDefault
uint64_t qsa_two_pass_scratch_bytes(int64_t nq_cap, int64_t max_blocks);
/// Once per layer and chunk, after the indexer appends and before the calls: the norms of the pooled keys [0, n_blocks).
bool qsa_two_pass_keys(const float* pooled, int64_t n_blocks, int64_t nq_cap, int64_t max_blocks, void* scratch,
uint64_t scratch_bytes, void* stream);
bool qsa_block_scores_two_pass(const float* pooled, const float* dead, const float* q_idx, const int32_t* steps,
int64_t nq, int64_t nq_cap, int64_t max_blocks, const QsaShapes& s, float* scores,
void* scratch, uint64_t scratch_bytes, void* stream, int64_t active_blocks);
/// STRATA_SELECT_TWO_PASS_CHECK: right after a two-pass call and its qsa_block_topk, against today's row and ids for
/// the same queries (ref_scores from qsa_block_scores_tc, ref_ids from qsa_block_topk on them): counts the queries
/// whose ids differ, the scored-again blocks whose score is not today's bit for bit, and the kept blocks whose
/// single-pass score lies outside the bound (two_pass::Stat).
void qsa_two_pass_check(const float* scores, const int32_t* ids, const float* ref_scores, const int32_t* ref_ids,
const int32_t* steps, int64_t nq, int64_t nq_cap, int64_t max_blocks, int64_t cap,
void* scratch, void* stream);
/// The counters (two_pass::kStatCount of them, this device), read on `stream` (synchronized); reset: zeroed after.
bool qsa_two_pass_stats(unsigned long long* out, bool reset, void* stream);
} // namespace strata::kernels
+141
View File
@@ -0,0 +1,141 @@
// include/strata/kernels/qsa_two_pass.hpp - STRATA_SELECT_TWO_PASS: the prompt path's QSA block scores in two
// precisions with the same selection. Header-only and host/device: the CUDA scorer (qsa_select.cu) and the GPU-free
// test (tests/prefill/qsa_two_pass_test.cpp) read the same constants.
//
// Today (qsa_block_scores_tc, sm_80+) every block score is 3xTF32: per indexer head h and pooled key k, with
// qh = tf32(q), ql = tf32(q - qh), kh = tf32(k), kl = tf32(k - kh) (cvt.rna), the chain of 16 m16n8k8 steps adds
// ql*kh, qh*kl, qh*kh into one FP32 accumulator (48 MMAs), and the score is relu(head 0) + ... + relu(head 3).
// The two-pass scorer computes S1 (qh*kh only, 16 MMAs per head) for every block, bounds |S1 - S3| per block, and
// computes S3 again, bit for bit as today, only for the blocks whose bound reaches the top-k cut-off. Every other
// block keeps S1, which lies strictly on the same side of the cut-off as its S3, so qsa_block_topk returns exactly
// today's cells in today's order (the threshold key, the count above it and the tied cells are unchanged).
//
// THE BOUND (per query q, block b; derived for the PR, conservative on purpose):
// S1_h - S3_h = -(qh.kl + ql.kh) + (accumulation error of S1) - (accumulation error of S3)
// |qh.kl + ql.kh| <= |qh| |kl| + |ql| |kh| (Cauchy-Schwarz, exact TF32 operands)
// accumulation: each MMA's FP32 result is taken to be within kUlps * 2^-23 of the absolute sum of its 9 addends
// (the accumulator and 8 exact TF32 products). A truncating alignment with no guard bits gives at most 9 such
// ulps (8 truncated addends plus the final rounding), so 16 is a margin, not a fit. Over a chain of n MMAs the
// error is at most n * kUlps * 2^-23 * A, A = the chain's absolute sum of products:
// A(S1) <= |qh| |kh|, A(S3) <= |qh| |kh| + |qh| |kl| + |ql| |kh|
// relu is 1-Lipschitz; the head sums ((r0 + r1) + r2) + r3 add at most 3 * 2^-24 relative each (folded into kG3).
// Summed over the four heads, with a = sum_h |qh_h|, c = sum_h |ql_h| (per query) and kh = |kh_b|, kl = |kl_b|:
// eps_b = kl * P + kh * Q + kAbs, P = (1 + kG3) a s, Q = ((1 + kG3) c + (kG1 + kG3) a) s, s = kSlack
// kAbs (2^-100) covers flushed subnormals (inputs, products, accumulators); kSlack the second-order terms.
// All norms are upper bounds (double, rounded up), eps is rounded up and the interval [S1 - eps, S1 + eps] is
// rounded outwards (CUDA __f*_rd / __f*_ru), so the interval is an enclosure of S3 under the stated MMA model.
//
// THE CUT-OFF RULE (per query): T = the width-th largest S3 cell key, the threshold qsa_block_topk computes. With
// T1 = the width-th largest S1 and E = the largest eps over the candidate blocks, T lies in [T1 - E, T1 + E]: the
// width cells with S1 >= T1 have S3 >= T1 - E, and every block with S3 > T1 + E has S1 > T1. A block with
// S1 - eps > T1 + E is above T for certain, one with S1 + eps < T1 - E below it; every other block (including every
// block whose S3 equals T, so the ties) is scored again with today's 3xTF32 arithmetic.
#pragma once
#include <cstdint>
#include <cstring>
#if defined(__CUDACC__) || defined(__HIPCC__)
#define STRATA_TP_HD __host__ __device__
#else
#define STRATA_TP_HD
#endif
namespace strata::kernels::two_pass {
constexpr int kSteps = 16; ///< K = 128 in m16n8k8 steps, one chain per head
constexpr double kUlps = 16.0; ///< accumulation allowance per MMA, FP32 ulps of its absolute sum
constexpr double kG1 = kSteps * kUlps * 0x1p-23; ///< the single-pass chain: 16 MMAs
constexpr double kG3 = 3 * kSteps * kUlps * 0x1p-23 + 0x1p-21; ///< the 3xTF32 chain: 48 MMAs, plus the head sums
constexpr double kSlack = 1.0 + 0x1p-10;
constexpr float kAbs = 0x1p-100f;
constexpr float kMaxEps = 1e30f; ///< larger bounds (or non-finite norms) mean: score this query again
/// Candidate blocks per query held by the band kernel (shared memory); more, and the query is scored again in full.
constexpr int kCandCap = 5120;
/// Blocks re-scored per query and per column residue (b % 8): eight lists of this many; more is a full re-score.
constexpr int kResCap = 128;
constexpr int kSampleStride = 32; ///< the candidate threshold comes from every 32nd block, as the
///< prompt top-k's sample kernel does
/// The calls (a batch of up to 256 queries) that take the two-pass path by default: the batch's last query has at
/// least this many blocks (16,384 cells). STRATA_SELECT_TWO_PASS_MIN=<blocks> moves it (0: every call).
constexpr int64_t kMinBlocksDefault = 4096;
/// The per-query coefficients of eps_b = kl_b * P + kh_b * Q (+ kAbs), from the head norm sums a and c.
STRATA_TP_HD inline void coefficients(double a, double c, double& P, double& Q) {
P = (1.0 + kG3) * a * kSlack;
Q = ((1.0 + kG3) * c + (kG1 + kG3) * a) * kSlack;
}
/// qsa_select.cu's order_key: the float's order as an unsigned key (NaN lowest, -0 as +0).
STRATA_TP_HD inline uint32_t order_key_host(float s) {
const float v = s + 0.0f;
if (!(v == v)) return 0u;
uint32_t b;
std::memcpy(&b, &v, 4);
return (b & 0x80000000u) ? ~b : (b | 0x80000000u);
}
/// The float of a key order_key produced (the inverse, for keys of non-NaN values).
STRATA_TP_HD inline float key_float_host(uint32_t k) {
const uint32_t b = (k & 0x80000000u) ? (k & 0x7fffffffu) : ~k;
float f;
std::memcpy(&f, &b, 4);
return f;
}
/// cvt.rna.tf32.f32 on the host: round to nearest, ties away from zero, 10 explicit mantissa bits (low 13 bits 0).
inline float tf32_rna_host(float x) {
uint32_t b;
std::memcpy(&b, &x, 4);
if ((b & 0x7f800000u) == 0x7f800000u) return x; // Inf and NaN unchanged
b = (b + 0x1000u) & 0xffffe000u; // magnitude rounding; a carry moves into the exponent
float r;
std::memcpy(&r, &b, 4);
return r;
}
/// Scratch the two-pass scorer takes from the caller (bytes, 256-aligned parts): the key norms of every block
/// (one layer, one chunk) and, per call of at most nq_cap queries, the coefficients, modes, residue counts, re-score
/// lists and the 16-query tile flags.
struct Layout {
uint64_t kn = 0, qc = 0, mode = 0, cnt = 0, ids = 0, tiles = 0, total = 0;
};
inline Layout layout(int64_t nq_cap, int64_t max_blocks) {
auto up = [](uint64_t v) { return (v + 255u) & ~uint64_t(255u); };
Layout l;
uint64_t o = 0;
l.kn = o; o += up((uint64_t) max_blocks * 8u);
l.qc = o; o += up((uint64_t) nq_cap * 8u);
l.mode = o; o += up((uint64_t) nq_cap * 4u);
l.cnt = o; o += up((uint64_t) nq_cap * 8u * 4u);
l.ids = o; o += up((uint64_t) nq_cap * 8u * (uint64_t) kResCap * 4u);
l.tiles = o; o += up((uint64_t) ((nq_cap + 15) / 16) * 4u);
l.total = o;
return l;
}
/// The engine's counters (qsa_two_pass_stats): what the scorer did, and with STRATA_SELECT_TWO_PASS_CHECK=1 what
/// the comparison against today's scorer found.
enum Stat : int {
kStCalls = 0, ///< two-pass calls
kStQueries, ///< their queries with a sparse selection (n_kv > width)
kStIdentity, ///< their queries whose selection is every cell (nothing to score)
kStFull, ///< queries flagged for a full re-score (no bound, a list full, too few cells above F)
kStFullTiles, ///< 16-query tiles scored again in full (3xTF32, today's kernel; every query of the tile)
kStBand, ///< blocks scored again (3xTF32) next to the cut-off
kStBandMax, ///< the most blocks scored again for one query
kStCand, ///< candidate blocks (the band kernel's shared list)
kStCandMax,
kStBlocks, ///< blocks scored once (single-pass) in two-pass calls
kChkQueries, ///< check: queries compared (ids)
kChkIdQueries, ///< check: queries whose ids differ from today's
kChkIdEntries, ///< check: id entries that differ
kChkRescored, ///< check: scored-again blocks compared bitwise with today's score
kChkRescoredDiff, ///< check: ... that differ (must be 0)
kChkKept, ///< check: blocks that kept S1 (compared against eps)
kChkKeptOut, ///< check: ... with |S1 - S3| > eps (must be 0)
kChkRatioMax, ///< check: the largest |S1 - S3| / eps seen, as float bits
kStatCount
};
} // namespace strata::kernels::two_pass
@@ -0,0 +1,41 @@
#pragma once
// STRATA_SELECT_TWO_PASS: the request, read once, and the line a set switch prints where the two-pass scorer cannot
// run. The scorer runs only in the CUDA prompt path's chunked walk, on a card where qsa_block_scores_tc runs its
// tensor-core kernel (sm_80+, STRATA_QSA_WARP not set). Elsewhere (HIP, Metal, SYCL, an older card or the warp
// scorer, the layer-major prompt path) the selection runs as without the switch; that was silent, so the switch read
// as active. Header-only, so the rule and the line are tested without a GPU (qsa_two_pass_status_test).
#include <atomic>
#include <cstdio>
namespace strata::kernels::two_pass {
// The switch's own parse: exactly "1" is on (as the two-pass scorer read it before).
inline bool requested_from(const char* v) { return v != nullptr && v[0] == '1' && v[1] == '\0'; }
class RequestStatus {
public:
explicit RequestStatus(const char* flag) : requested_(requested_from(flag)) {}
bool requested() const { return requested_; }
// `not_here` null: this path can run the scorer, so the result is the request (whether a call takes it still
// depends on the call: its size, geometry and scratch). Otherwise the path cannot run it: false, and the first
// such query prints one line naming the path. A supported query never uses up that line.
bool on(const char* not_here, FILE* log = stderr) {
if (!requested_) return false;
if (not_here == nullptr) return true;
if (!warned_.exchange(true)) {
std::fprintf(log, "strata: STRATA_SELECT_TWO_PASS=1 is not used on %s: the two-pass scorer runs only in "
"the CUDA prompt path's tensor-core selection (sm_80+); the selection is unchanged\n",
not_here);
std::fflush(log);
}
return false;
}
private:
const bool requested_;
std::atomic<bool> warned_{false};
};
} // namespace strata::kernels::two_pass
+762
View File
@@ -5,6 +5,8 @@
#include <cstdlib>
#include <cstring>
#include "strata/kernels/qsa_select.hpp"
#include "strata/kernels/qsa_two_pass.hpp"
#include "strata/kernels/qsa_two_pass_status.hpp"
#include "strata/kernels/gfx_arch.hpp"
#include "strata/core/amd_community.hpp"
@@ -3143,4 +3145,764 @@ void qsa_block_scores_long(const float* pooled, const float* dead, const float*
qsa_block_scores(pooled, dead, q_idx, steps, nq, max_blocks, s, scores, stream, active_blocks);
}
// ==== STRATA_SELECT_TWO_PASS (the prompt path's block scores in two precisions; qsa_two_pass.hpp has the bound) ====
// qsa_block_scores_tc computes every block score in 3xTF32. Here every block is scored once in single-pass TF32
// (S1), a per-block bound eps encloses today's 3xTF32 score S3 in [S1 - eps, S1 + eps], and only the blocks whose
// interval reaches the query's top-k cut-off are scored again, with block_scores_tc_kernel's arithmetic, bit for bit.
// qsa_block_topk then reads a row whose every block lies on the same side of the threshold as in today's row and
// whose blocks at the threshold hold today's exact scores: the same threshold, the same ties, the same cells. The row
// itself is not today's (kept blocks hold S1), so it is for qsa_block_topk only: nothing else reads m.sel_scores.
#if !defined(__HIPCC__)
__device__ unsigned long long g_tp_stats[two_pass::kStatCount];
namespace {
namespace tp = strata::kernels::two_pass;
constexpr int TP1_ITER = 8; // 32-block tiles per single-pass CTA (its query fragments are loaded once)
constexpr int TPB_T = 512; // the band kernel's threads (one query per CTA)
static_assert(TPB_T == IDX_HEADS * IDX_DIM, "the band kernel reads one query element per thread");
__device__ __forceinline__ float tp_inf() { return __int_as_float(0x7f800000); }
__device__ __forceinline__ float tp_key_float(uint32_t k) { // order_key's inverse (keys of non-NaN floats)
return __uint_as_float((k & 0x80000000u) ? (k & 0x7fffffffu) : ~k);
}
// The single-pass enclosure of block b's 3xTF32 score from its single-pass score s: [lo, hi], eps rounded up and the
// ends rounded outwards. False: no bound (a non-finite score, norm or coefficient, or a bound past kMaxEps).
__device__ __forceinline__ bool tp_interval(float s, float2 kn, float P, float Q, float& lo, float& hi, float& e) {
e = __fmaf_ru(P, kn.y, __fmaf_ru(Q, kn.x, tp::kAbs));
if (!(e <= tp::kMaxEps) || !(fabsf(s) <= FLT_MAX)) return false;
lo = __fsub_rd(s, e);
hi = __fadd_ru(s, e);
return true;
}
// The key norms (upper bounds): per pooled block |tf32(k)| and |tf32(k - tf32(k))|, the split block_scores_tc_kernel
// makes, in double with every operation rounded up; a non-finite norm becomes +inf (its blocks are never bounded).
__global__ void __launch_bounds__(256) tp_keys_kernel(const float* __restrict__ pooled, int64_t n_blocks,
float2* __restrict__ kn) {
const int64_t b = (int64_t) blockIdx.x * 8 + (threadIdx.x >> 5);
if (b >= n_blocks) return;
const int lane = threadIdx.x & 31;
const float4 v = reinterpret_cast<const float4*>(pooled + b * IDX_DIM)[lane];
const float x[4] = {v.x, v.y, v.z, v.w};
double sh = 0.0, sl = 0.0;
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float h = __uint_as_float(tf32_hi(x[j]));
const float l = __uint_as_float(tf32_hi(x[j] - h));
sh = __dadd_ru(sh, __dmul_ru((double) h, (double) h));
sl = __dadd_ru(sl, __dmul_ru((double) l, (double) l));
}
#pragma unroll
for (int o = 16; o > 0; o >>= 1) {
sh = __dadd_ru(sh, __shfl_xor_sync(0xffffffffu, sh, o));
sl = __dadd_ru(sl, __shfl_xor_sync(0xffffffffu, sl, o));
}
if (lane == 0) {
float kh = __double2float_ru(__dsqrt_ru(sh)), kl = __double2float_ru(__dsqrt_ru(sl));
if (!(kh <= FLT_MAX)) kh = tp_inf();
if (!(kl <= FLT_MAX)) kl = tp_inf();
kn[b] = make_float2(kh, kl);
}
}
// Pass 1: S1 = sum over the heads of relu(tf32(q) . tf32(k)), one MMA per step (block_scores_tc_kernel makes three).
// Warp h is head h and keeps its 16 steps of A fragments in registers; a CTA is one 16-query tile (blockIdx.x, so the
// CTAs that run together read the same keys) and TP1_ITER tiles of 32 blocks. Blocks < n_bid only, as today; the
// tail block is block_scores_tail_kernel's. Also clears the tile flags the band kernel sets.
__global__ void __launch_bounds__(128) tp_score1_kernel(const float* __restrict__ pooled,
const float* __restrict__ q_idx,
const int32_t* __restrict__ steps, int64_t nq,
int64_t max_blocks, int64_t reach, float* __restrict__ out,
int* __restrict__ tiles) {
__shared__ __align__(16) uint32_t sK[TC_NB * TC_KS];
__shared__ float red[IDX_HEADS][TC_QT][TC_NB + 1];
__shared__ int s_nbid[TC_QT];
const int t = threadIdx.x, lane = t & 31, h = t >> 5, gid = lane >> 2, tig = lane & 3;
const int64_t q0 = (int64_t) blockIdx.x * TC_QT;
if (blockIdx.y == 0 && t == 0) tiles[blockIdx.x] = 0;
if (t < TC_QT) s_nbid[t] = q0 + t < nq ? steps[(q0 + t) * kStepCount + kStepNBid] : 0;
uint32_t a[16][4]; // rows gid and gid + 8, k = 8s + tig and 8s + tig + 4 (the m16n8k8 A fragment)
{
const bool r0 = q0 + gid < nq, r1 = q0 + gid + 8 < nq;
const float* qa = q_idx + (r0 ? q0 + gid : 0) * (IDX_HEADS * IDX_DIM) + h * IDX_DIM + tig;
const float* qb = q_idx + (r1 ? q0 + gid + 8 : 0) * (IDX_HEADS * IDX_DIM) + h * IDX_DIM + tig;
#pragma unroll
for (int s = 0; s < 16; ++s) {
a[s][0] = tf32_hi(r0 ? qa[8 * s] : 0.f);
a[s][1] = tf32_hi(r1 ? qb[8 * s] : 0.f);
a[s][2] = tf32_hi(r0 ? qa[8 * s + 4] : 0.f);
a[s][3] = tf32_hi(r1 ? qb[8 * s + 4] : 0.f);
}
}
__syncthreads();
int hi_nbid = 0;
for (int i = 0; i < TC_QT; ++i) hi_nbid = max(hi_nbid, s_nbid[i]);
for (int it = 0; it < TP1_ITER; ++it) {
const int64_t b0 = ((int64_t) blockIdx.y * TP1_ITER + it) * TC_NB;
if (b0 >= reach || b0 >= hi_nbid) break;
__syncthreads(); // the previous tile's sK and red reads are done
for (int i = t; i < TC_NB * IDX_DIM / 4; i += 128) {
const int r = i / (IDX_DIM / 4), c = i % (IDX_DIM / 4);
float4 v = make_float4(0.f, 0.f, 0.f, 0.f);
if (b0 + r < hi_nbid) v = reinterpret_cast<const float4*>(pooled + (b0 + r) * IDX_DIM)[c];
*reinterpret_cast<uint4*>(sK + r * TC_KS + c * 4) =
make_uint4(tf32_hi(v.x), tf32_hi(v.y), tf32_hi(v.z), tf32_hi(v.w));
}
__syncthreads();
float acc[4][4];
#pragma unroll
for (int n = 0; n < 4; ++n) acc[n][0] = acc[n][1] = acc[n][2] = acc[n][3] = 0.f;
#pragma unroll
for (int s = 0; s < 16; ++s) {
#pragma unroll
for (int n = 0; n < 4; ++n) {
const uint32_t* kr = sK + (n * 8 + gid) * TC_KS + 8 * s + tig;
const uint32_t b[2] = {kr[0], kr[4]};
mma_tf32(acc[n], a[s], b);
}
}
#pragma unroll
for (int n = 0; n < 4; ++n) {
const int c = n * 8 + 2 * tig;
red[h][gid][c] = acc[n][0] > 0.0f ? acc[n][0] : 0.0f;
red[h][gid][c + 1] = acc[n][1] > 0.0f ? acc[n][1] : 0.0f;
red[h][gid + 8][c] = acc[n][2] > 0.0f ? acc[n][2] : 0.0f;
red[h][gid + 8][c + 1] = acc[n][3] > 0.0f ? acc[n][3] : 0.0f;
}
__syncthreads();
for (int e = t; e < TC_QT * TC_NB; e += 128) {
const int r = e / TC_NB, c = e % TC_NB;
const int64_t qi = q0 + r, b = b0 + c;
if (qi >= nq || b >= s_nbid[r] || b >= max_blocks) continue;
float score = 0.0f;
#pragma unroll
for (int h2 = 0; h2 < IDX_HEADS; ++h2) score += red[h2][r][c];
out[qi * max_blocks + b] = score;
}
}
}
// block_topk_kernel's threshold over a list in shared memory: the largest key k such that the weighted entries with
// a key >= k reach `width`, 8 bits a pass with that kernel's digit rule (the largest digit whose cells at or above it
// reach the width, else 0). unit: every entry weighs 1 (the sample); else a listed block weighs R and the tail block
// n_bid w_tail. Every thread of the CTA calls it; it ends synchronized.
__device__ uint32_t tp_select(const uint32_t* __restrict__ key, const int32_t* __restrict__ blk, int n, int width,
bool unit, int64_t n_bid, int w_tail, int* hist, int* s_misc) {
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
uint32_t prefix = 0;
int above = 0;
for (int shift = 24; shift >= 0; shift -= 8) {
for (int i = t; i < 256; i += TPB_T) hist[i] = 0;
__syncthreads();
const uint32_t hi_mask = shift == 24 ? 0u : (0xffffffffu << (shift + 8));
for (int i0 = 0; i0 < n; i0 += TPB_T) { // the same trip count in every thread: whole warps match
const int i = i0 + t;
const uint32_t k = i < n ? key[i] : 0u;
const bool hit = i < n && (k & hi_mask) == (prefix & hi_mask);
const bool tail = hit && !unit && blk[i] == n_bid;
const int d = (int) ((k >> shift) & 255u);
const int g = hit && !tail ? d : 256;
const unsigned grp = __match_any_sync(0xffffffffu, g); // one add per digit and warp
if (g < 256 && lane == __ffs(grp) - 1) atomicAdd(&hist[d], (unit ? 1 : R) * __popc(grp));
if (tail) atomicAdd(&hist[d], w_tail);
}
__syncthreads();
if (warp == 0) { // the digit: lane l holds digits 8l .. 8l + 7
int v[8], local = 0;
#pragma unroll
for (int j = 0; j < 8; ++j) { v[j] = hist[lane * 8 + j]; local += v[j]; }
int suf = local; // cells in this lane's digits and every higher one
#pragma unroll
for (int o = 1; o < 32; o <<= 1) {
const int y = __shfl_down_sync(0xffffffffu, suf, o);
if (lane + o < 32) suf += y;
}
int cum = above + suf - local, best = -1, best_cum = 0;
#pragma unroll
for (int j = 7; j >= 0; --j) {
const int dd = lane * 8 + j;
if (best < 0 && dd >= 1 && cum + v[j] >= width) { best = dd; best_cum = cum; }
cum += v[j];
}
const unsigned m = __ballot_sync(0xffffffffu, best >= 0);
const int top = m ? 31 - __clz((int) m) : 0;
const int dsel = __shfl_sync(0xffffffffu, best, top), csel = __shfl_sync(0xffffffffu, best_cum, top);
const int all = __shfl_sync(0xffffffffu, suf, 0), h0 = __shfl_sync(0xffffffffu, v[0], 0);
if (lane == 0) {
s_misc[0] = m ? dsel : 0;
s_misc[1] = m ? csel : above + all - h0; // digit 0: every cell of digits 1..255 is above it
}
}
__syncthreads();
prefix |= (uint32_t) s_misc[0] << shift;
above = s_misc[1];
__syncthreads();
}
return prefix;
}
// Per query: the blocks to score again. 1. the coefficients P, Q (qsa_two_pass.hpp) from the query's head norms;
// 2. a candidate threshold F from every 32nd block (only when the blocks outnumber the list): the ks-th largest
// sampled S1 - eps; 3. one read of every block: the candidates (S1 + eps >= F), the cells with S1 - eps >= F (at least
// `width` of them, so the threshold T of today's row and the single-pass one T1 are both >= F and every block that
// matters below is a candidate) and E = the largest candidate eps; 4. T1 over the candidates; 5. T lies in
// [max(T1 - E, F), T1 + E]: a candidate whose interval reaches it (and is not the exact tail block) is listed for a
// re-score, by its column residue b % 8. A query that cannot be settled (no bound for some candidate, a list or a
// residue list full, too few cells above F) is flagged: its 16-query tile is scored again in full.
__global__ void __launch_bounds__(TPB_T) tp_band_kernel(const float* __restrict__ scores,
const float* __restrict__ q_idx,
const int32_t* __restrict__ steps, int64_t max_blocks,
const float2* __restrict__ kn, float2* __restrict__ qc,
int* __restrict__ mode, int* __restrict__ cnt,
int* __restrict__ ids, int* __restrict__ tiles) {
extern __shared__ __align__(16) uint32_t tp_sm[];
uint32_t* c_key = tp_sm; // [kCandCap] S1 keys (first: the sample)
int32_t* c_blk = reinterpret_cast<int32_t*>(tp_sm + tp::kCandCap); // [kCandCap] their blocks
__shared__ int hist[256];
__shared__ int s_misc[2], s_n, s_bad, s_wf[TPB_T / 32], s_cnt[8];
__shared__ float s_e[TPB_T / 32], s_P, s_Q, s_E;
__shared__ double s_hn[TPB_T / 32][2];
const int t = threadIdx.x, lane = t & 31, warp = t >> 5;
const int64_t qi = blockIdx.x;
const int32_t* st = steps + qi * kStepCount;
const int64_t n_kv = st[kStepNKv], n_bid = st[kStepNBid], width = st[kStepWidth];
if (qi == 0 && t == 0) atomicAdd(&g_tp_stats[tp::kStCalls], 1ull);
if (n_kv <= width) { // every cell is selected: qsa_block_topk reads no score
if (t == 0) { mode[qi] = 0; atomicAdd(&g_tp_stats[tp::kStIdentity], 1ull); }
return;
}
{ // 1. a = sum_h |tf32(q_h)|, c = sum_h |tf32(q_h - tf32(q_h))|, rounded up; warps 4h .. 4h + 3 hold head h
const float x = q_idx[qi * IDX_HEADS * IDX_DIM + t];
const float hx = __uint_as_float(tf32_hi(x));
const float lx = __uint_as_float(tf32_hi(x - hx));
double sh = __dmul_ru((double) hx, (double) hx), sl = __dmul_ru((double) lx, (double) lx);
#pragma unroll
for (int o = 16; o > 0; o >>= 1) {
sh = __dadd_ru(sh, __shfl_xor_sync(0xffffffffu, sh, o));
sl = __dadd_ru(sl, __shfl_xor_sync(0xffffffffu, sl, o));
}
if (lane == 0) { s_hn[warp][0] = sh; s_hn[warp][1] = sl; }
if (t == 0) { s_n = 0; s_bad = 0; }
if (t < 8) s_cnt[t] = 0;
__syncthreads();
if (t == 0) {
double a = 0.0, c = 0.0;
for (int hh = 0; hh < IDX_HEADS; ++hh) {
double eh = 0.0, el = 0.0;
for (int w = 0; w < 4; ++w) { eh = __dadd_ru(eh, s_hn[4 * hh + w][0]); el = __dadd_ru(el, s_hn[4 * hh + w][1]); }
a = __dadd_ru(a, __dsqrt_ru(eh));
c = __dadd_ru(c, __dsqrt_ru(el));
}
double P = 0.0, Q = 0.0;
tp::coefficients(a, c, P, Q); // RN in double: far inside kSlack
s_P = __double2float_ru(P);
s_Q = __double2float_ru(Q);
}
__syncthreads();
}
const float P = s_P, Q = s_Q;
const int64_t nb = n_bid + 1;
const int w_tail = (int) (n_kv - n_bid * R);
const float* sc = scores + qi * max_blocks;
bool full = !(P <= tp::kMaxEps && Q <= tp::kMaxEps);
float F = -tp_inf();
if (!full && nb > tp::kCandCap) { // 2. the sample
int64_t stride = tp::kSampleStride;
while ((nb + stride - 1) / stride > tp::kCandCap) stride *= 2;
const int ns = (int) ((nb + stride - 1) / stride);
for (int i = t; i < ns; i += TPB_T) {
const int64_t b = (int64_t) i * stride;
uint32_t k = 0u; // no bound, or no cells: the lowest key
if (b < n_bid) {
float lo, hi, e;
if (tp_interval(sc[b], kn[b], P, Q, lo, hi, e)) k = order_key(lo);
} else if (w_tail > 0) {
k = order_key(sc[b]);
}
c_key[i] = k;
}
__syncthreads();
const int ks = (int) (2 * ((width + R * stride - 1) / (R * stride)) + 16);
const uint32_t fk = tp_select(c_key, c_blk, ns, ks < ns ? ks : ns, true, n_bid, w_tail, hist, s_misc);
if (fk != 0u) F = tp_key_float(fk);
}
int wf = 0;
float emax = 0.0f;
bool bad = false;
if (!full) { // 3. one read of every block
for (int64_t b = t; b < nb; b += TPB_T) {
float lo, hi, e = 0.0f, s;
int w;
if (b < n_bid) {
w = R;
s = sc[b];
if (!tp_interval(s, kn[b], P, Q, lo, hi, e)) { bad = true; continue; }
} else {
w = w_tail;
if (w == 0) continue;
s = lo = hi = sc[b]; // the tail kernel's score: today's
}
if (lo >= F) wf += w;
if (hi >= F) {
const int pos = atomicAdd(&s_n, 1);
if (pos < tp::kCandCap) { c_key[pos] = order_key(s); c_blk[pos] = (int32_t) b; }
emax = fmaxf(emax, e);
}
}
}
#pragma unroll
for (int o = 16; o > 0; o >>= 1) {
wf += __shfl_xor_sync(0xffffffffu, wf, o);
emax = fmaxf(emax, __shfl_xor_sync(0xffffffffu, emax, o));
}
bad = __any_sync(0xffffffffu, bad);
if (lane == 0) { s_wf[warp] = wf; s_e[warp] = emax; if (bad) s_bad = 1; }
__syncthreads();
if (t == 0) {
int W = 0;
float E = 0.0f;
for (int w = 0; w < TPB_T / 32; ++w) { W += s_wf[w]; E = fmaxf(E, s_e[w]); }
s_misc[0] = W;
s_E = E;
}
__syncthreads();
const int n = s_n;
full = full || s_bad != 0 || n > tp::kCandCap || s_misc[0] < width;
if (!full) {
// 4. T1, then 5. the window [lo_b, hi_b] that holds today's threshold
const uint32_t t1k = tp_select(c_key, c_blk, n, (int) width, false, n_bid, w_tail, hist, s_misc);
const float T1 = tp_key_float(t1k), E = s_E;
const float lo_b = fmaxf(__fsub_rd(T1, E), F), hi_b = __fadd_ru(T1, E);
for (int i = t; i < n; i += TPB_T) {
const int64_t b = c_blk[i];
if (b >= n_bid) continue; // the tail block holds today's score already
float lo, hi, e;
tp_interval(tp_key_float(c_key[i]), kn[b], P, Q, lo, hi, e); // bounded in 3: the same result
if (hi >= lo_b && lo <= hi_b) {
const int r = (int) (b & 7);
const int p = atomicAdd(&s_cnt[r], 1);
if (p < tp::kResCap) ids[(qi * 8 + r) * tp::kResCap + p] = (int32_t) b;
}
}
__syncthreads();
}
if (t == 0) {
int tot = 0, mx = 0;
for (int r = 0; r < 8; ++r) { tot += s_cnt[r]; mx = max(mx, s_cnt[r]); }
if (mx > tp::kResCap) full = true;
atomicAdd(&g_tp_stats[tp::kStQueries], 1ull);
atomicAdd(&g_tp_stats[tp::kStBlocks], (unsigned long long) n_bid);
if (full) {
mode[qi] = 2;
if (atomicExch(&tiles[qi / TC_QT], 1) == 0) atomicAdd(&g_tp_stats[tp::kStFullTiles], 1ull);
atomicAdd(&g_tp_stats[tp::kStFull], 1ull);
} else {
for (int r = 0; r < 8; ++r) cnt[qi * 8 + r] = s_cnt[r];
qc[qi] = make_float2(P, Q);
mode[qi] = 1;
atomicAdd(&g_tp_stats[tp::kStBand], (unsigned long long) tot);
atomicMax(&g_tp_stats[tp::kStBandMax], (unsigned long long) tot);
atomicAdd(&g_tp_stats[tp::kStCand], (unsigned long long) n);
atomicMax(&g_tp_stats[tp::kStCandMax], (unsigned long long) n);
}
}
}
// The listed blocks scored again exactly as block_scores_tc_kernel scores them: the query in its row of the m16
// tile (qi % 16), each block in its column of the n8 tile (b % 8), the same TF32 splits, the same three MMAs per step
// in the same order into the same accumulator, the same relu sum. The other rows and columns hold zeros where
// today's kernel holds other queries and blocks; an MMA output element depends only on its own row of A, column of B
// and accumulator, so the element is today's bit for bit (qsa_two_pass_parity and STRATA_SELECT_TWO_PASS_CHECK
// compare every one with today's kernel). Warp w takes tiles w, w + 4, ...: tile j holds the j-th listed block of
// every residue.
__global__ void __launch_bounds__(128) tp_rescore_kernel(const float* __restrict__ pooled,
const float* __restrict__ q_idx, int64_t max_blocks,
const int* __restrict__ mode, const int* __restrict__ cnt,
const int* __restrict__ ids, const int* __restrict__ tiles,
float* __restrict__ out) {
__shared__ __align__(16) float sq[IDX_HEADS * IDX_DIM];
__shared__ int s_cnt[8];
const int64_t qi = blockIdx.x;
if (mode[qi] != 1 || tiles[qi / TC_QT] != 0) return; // nothing listed, or the full kernel takes the tile
const int t = threadIdx.x, lane = t & 31, warp = t >> 5, gid = lane >> 2, tig = lane & 3;
for (int i = t; i < IDX_HEADS * IDX_DIM; i += 128) sq[i] = q_idx[qi * IDX_HEADS * IDX_DIM + i];
if (t < 8) s_cnt[t] = cnt[qi * 8 + t];
__syncthreads();
int ntile = 0;
for (int r = 0; r < 8; ++r) ntile = max(ntile, s_cnt[r]);
const int row = (int) (qi % TC_QT);
const bool mine0 = gid == (row & 7) && row < 8, mine1 = gid == (row & 7) && row >= 8;
const int* lst = ids + qi * 8 * tp::kResCap;
for (int j = warp; j < ntile; j += 4) {
const int bcol = j < s_cnt[gid] ? lst[gid * tp::kResCap + j] : -1; // this lane's column: gid
const float* kr = pooled + (int64_t) (bcol >= 0 ? bcol : 0) * IDX_DIM;
float acc[IDX_HEADS][4];
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) acc[h][0] = acc[h][1] = acc[h][2] = acc[h][3] = 0.f;
#pragma unroll 4
for (int k0 = 0; k0 < IDX_DIM; k0 += 8) {
const float kx0 = bcol >= 0 ? kr[k0 + tig] : 0.f, kx1 = bcol >= 0 ? kr[k0 + tig + 4] : 0.f;
uint32_t bh[2], bl[2];
bh[0] = tf32_hi(kx0);
bh[1] = tf32_hi(kx1);
bl[0] = tf32_hi(kx0 - __uint_as_float(bh[0]));
bl[1] = tf32_hi(kx1 - __uint_as_float(bh[1]));
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) {
const float* qa = sq + h * IDX_DIM + k0 + tig;
const float x0 = mine0 ? qa[0] : 0.f, x1 = mine1 ? qa[0] : 0.f;
const float x2 = mine0 ? qa[4] : 0.f, x3 = mine1 ? qa[4] : 0.f;
uint32_t ah[4], al[4];
ah[0] = tf32_hi(x0); ah[1] = tf32_hi(x1); ah[2] = tf32_hi(x2); ah[3] = tf32_hi(x3);
al[0] = tf32_hi(x0 - __uint_as_float(ah[0]));
al[1] = tf32_hi(x1 - __uint_as_float(ah[1]));
al[2] = tf32_hi(x2 - __uint_as_float(ah[2]));
al[3] = tf32_hi(x3 - __uint_as_float(ah[3]));
mma_tf32(acc[h], al, bh);
mma_tf32(acc[h], ah, bl);
mma_tf32(acc[h], ah, bh);
}
}
if (gid == (row & 7)) { // row `row`, columns 2 tig and 2 tig + 1
#pragma unroll
for (int jj = 0; jj < 2; ++jj) {
const int c = 2 * tig + jj;
if (j >= s_cnt[c]) continue;
const int64_t b = lst[c * tp::kResCap + j];
float score = 0.0f;
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) {
const float d = row >= 8 ? (jj ? acc[h][3] : acc[h][2]) : (jj ? acc[h][1] : acc[h][0]);
score += d > 0.0f ? d : 0.0f;
}
out[qi * max_blocks + b] = score;
}
}
}
}
// A flagged 16-query tile scored again in full: block_scores_tc_kernel's body as it is above, the CTA taking the tile
// (blockIdx.y) only when the band kernel flagged it and striding over its 128-block ranges. Each 32-block tile is
// computed exactly as today's kernel computes it (the same shared-memory tiles, warps, MMAs and sums), so every
// score of the tile is today's.
__global__ void __launch_bounds__(128) tp_full_kernel(const float* __restrict__ pooled,
const float* __restrict__ q_idx,
const int32_t* __restrict__ steps, int64_t nq,
int64_t max_blocks, int64_t reach,
const int* __restrict__ tiles, float* __restrict__ out) {
if (tiles[blockIdx.y] == 0) return;
extern __shared__ __align__(16) float sm[];
float* sQ = sm; // [TC_QT][TC_QS]
float* sK = sm + TC_QT * TC_QS; // [TC_NB][TC_KS]
__shared__ int s_nbid[TC_QT];
const int t = threadIdx.x, lane = t & 31, warp = t >> 5, gid = lane >> 2, tig = lane & 3;
const int64_t q0 = (int64_t) blockIdx.y * TC_QT;
for (int i = t; i < TC_QT * IDX_HEADS * IDX_DIM / 4; i += 128) {
const int r = i / (IDX_HEADS * IDX_DIM / 4), c = i % (IDX_HEADS * IDX_DIM / 4);
float4 v = make_float4(0.f, 0.f, 0.f, 0.f);
if (q0 + r < nq) v = reinterpret_cast<const float4*>(q_idx + (q0 + r) * IDX_HEADS * IDX_DIM)[c];
*reinterpret_cast<float4*>(sQ + r * TC_QS + c * 4) = v;
}
if (t < TC_QT) s_nbid[t] = q0 + t < nq ? steps[(q0 + t) * kStepCount + kStepNBid] : 0;
int lo_nbid = 0x7fffffff, hi_nbid = 0;
__syncthreads();
for (int i = 0; i < TC_QT; ++i) {
if (q0 + i >= nq) break;
lo_nbid = min(lo_nbid, s_nbid[i]);
hi_nbid = max(hi_nbid, s_nbid[i]);
}
(void) lo_nbid;
for (int64_t x = blockIdx.x; x * TC_NB * TC_ITER < reach; x += gridDim.x) {
for (int it = 0; it < TC_ITER; ++it) {
const int64_t b0 = (x * TC_ITER + it) * TC_NB;
if (b0 >= reach || b0 >= hi_nbid) break;
__syncthreads();
for (int i = t; i < TC_NB * IDX_DIM / 4; i += 128) {
const int r = i / (IDX_DIM / 4), c = i % (IDX_DIM / 4);
float4 v = make_float4(0.f, 0.f, 0.f, 0.f);
if (b0 + r < hi_nbid) v = reinterpret_cast<const float4*>(pooled + (b0 + r) * IDX_DIM)[c];
*reinterpret_cast<float4*>(sK + r * TC_KS + c * 4) = v;
}
__syncthreads();
float acc[IDX_HEADS][4];
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) acc[h][0] = acc[h][1] = acc[h][2] = acc[h][3] = 0.f;
const float* kr = sK + (warp * 8 + gid) * TC_KS;
#pragma unroll 4
for (int k0 = 0; k0 < IDX_DIM; k0 += 8) {
const float kx0 = kr[k0 + tig], kx1 = kr[k0 + tig + 4];
uint32_t bh[2], bl[2];
bh[0] = tf32_hi(kx0);
bh[1] = tf32_hi(kx1);
bl[0] = tf32_hi(kx0 - __uint_as_float(bh[0]));
bl[1] = tf32_hi(kx1 - __uint_as_float(bh[1]));
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) {
const float* qa = sQ + h * IDX_DIM + k0 + tig;
const float x0 = qa[gid * TC_QS], x1 = qa[(gid + 8) * TC_QS];
const float x2 = qa[gid * TC_QS + 4], x3 = qa[(gid + 8) * TC_QS + 4];
uint32_t ah[4], al[4];
ah[0] = tf32_hi(x0); ah[1] = tf32_hi(x1); ah[2] = tf32_hi(x2); ah[3] = tf32_hi(x3);
al[0] = tf32_hi(x0 - __uint_as_float(ah[0]));
al[1] = tf32_hi(x1 - __uint_as_float(ah[1]));
al[2] = tf32_hi(x2 - __uint_as_float(ah[2]));
al[3] = tf32_hi(x3 - __uint_as_float(ah[3]));
mma_tf32(acc[h], al, bh);
mma_tf32(acc[h], ah, bl);
mma_tf32(acc[h], ah, bh);
}
}
const int64_t bc = b0 + warp * 8 + 2 * tig;
#pragma unroll
for (int half = 0; half < 2; ++half) {
const int qr = gid + half * 8;
const int64_t qi = q0 + qr;
if (qi >= nq) continue;
const int nbq = s_nbid[qr];
#pragma unroll
for (int j = 0; j < 2; ++j) {
const int64_t b = bc + j;
if (b >= nbq || b >= max_blocks) continue;
float score = 0.0f;
#pragma unroll
for (int h = 0; h < IDX_HEADS; ++h) {
const float d = acc[h][half * 2 + j];
score += d > 0.0f ? d : 0.0f;
}
out[qi * max_blocks + b] = score;
}
}
}
}
}
// STRATA_SELECT_TWO_PASS_CHECK: one query against today's row and ids. Ids: every entry qsa_block_topk wrote. Scored-
// again blocks (listed, the tail, or every block of a full query): bitwise today's score. Kept blocks: |S1 - S3| <= eps.
__global__ void __launch_bounds__(256) tp_check_kernel(const float* __restrict__ scores, const int32_t* __restrict__ ids,
const float* __restrict__ ref, const int32_t* __restrict__ ref_ids,
const int32_t* __restrict__ steps, int64_t max_blocks,
int64_t cap, const float2* __restrict__ kn,
const float2* __restrict__ qc, const int* __restrict__ mode,
const int* __restrict__ cnt, const int* __restrict__ lists,
const int* __restrict__ tiles) {
extern __shared__ uint32_t tp_mark[];
__shared__ unsigned long long s_acc[6];
__shared__ unsigned int s_ratio;
const int t = threadIdx.x;
const int64_t qi = blockIdx.x;
const int32_t* st = steps + qi * kStepCount;
const int64_t n_kv = st[kStepNKv], n_bid = st[kStepNBid], width = st[kStepWidth];
if (t < 6) s_acc[t] = 0ull;
if (t == 0) s_ratio = 0u;
const int64_t m = n_kv < width ? n_kv : width;
unsigned long long dif = 0;
for (int64_t j = t; j < m; j += 256) dif += ids[qi * cap + j] != ref_ids[qi * cap + j];
const bool sparse = n_kv > width;
const int md = !sparse ? 0 : tiles[qi / TC_QT] != 0 ? 2 : mode[qi]; // a flagged tile: every block scored again
const int64_t nb = n_bid + 1, words = (nb + 31) / 32;
if (sparse) for (int64_t i = t; i < words; i += 256) tp_mark[i] = 0u;
__syncthreads();
atomicAdd(&s_acc[0], dif);
if (sparse && md == 1) {
for (int r = 0; r < 8; ++r) {
const int c = cnt[qi * 8 + r];
for (int j = t; j < c; j += 256) {
const int b = lists[(qi * 8 + r) * tp::kResCap + j];
atomicOr(&tp_mark[b >> 5], 1u << (b & 31));
}
}
}
__syncthreads();
if (sparse) {
const float* s1 = scores + qi * max_blocks;
const float* s3 = ref + qi * max_blocks;
const float2 pq = md == 1 ? qc[qi] : make_float2(0.f, 0.f);
unsigned long long again = 0, again_dif = 0, kept = 0, kept_out = 0;
float ratio = 0.0f;
for (int64_t b = t; b < nb; b += 256) {
const bool exact = md == 2 || b == n_bid || ((tp_mark[b >> 5] >> (b & 31)) & 1u);
if (exact) {
++again;
again_dif += __float_as_uint(s1[b]) != __float_as_uint(s3[b]);
} else {
++kept;
const float e = __fmaf_ru(pq.x, kn[b].y, __fmaf_ru(pq.y, kn[b].x, tp::kAbs));
const double d = fabs((double) s1[b] - (double) s3[b]);
kept_out += !(d <= (double) e);
const float r = (float) (d / (double) e);
ratio = fmaxf(ratio, r == r ? r : tp_inf());
}
}
atomicAdd(&s_acc[1], again);
atomicAdd(&s_acc[2], again_dif);
atomicAdd(&s_acc[3], kept);
atomicAdd(&s_acc[4], kept_out);
atomicMax(&s_ratio, __float_as_uint(ratio));
}
__syncthreads();
if (t == 0) {
atomicAdd(&g_tp_stats[tp::kChkQueries], 1ull);
if (s_acc[0]) atomicAdd(&g_tp_stats[tp::kChkIdQueries], 1ull);
atomicAdd(&g_tp_stats[tp::kChkIdEntries], s_acc[0]);
atomicAdd(&g_tp_stats[tp::kChkRescored], s_acc[1]);
atomicAdd(&g_tp_stats[tp::kChkRescoredDiff], s_acc[2]);
atomicAdd(&g_tp_stats[tp::kChkKept], s_acc[3]);
atomicAdd(&g_tp_stats[tp::kChkKeptOut], s_acc[4]);
atomicMax(&g_tp_stats[tp::kChkRatioMax], (unsigned long long) s_ratio);
}
}
// The card runs qsa_block_scores_tc's tensor-core kernel (the reference of the two-pass scores), and the full kernel
// takes its shared memory. The same dispatch rule as qsa_block_scores_tc: sm_80+, STRATA_QSA_WARP not set.
bool tp_device_ok() {
static int ok[64] = {}; // 0 unknown, 1 yes, 2 no
int dev = 0;
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
if (ok[dev] == 0) {
int major = 0;
bool good = cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) == cudaSuccess;
const char* w = std::getenv("STRATA_QSA_WARP");
const int cc = w && (!std::strcmp(w, "1") || !std::strcmp(w, "select")) ? 7 : strata::cc_major_of(major);
const int bytes = (TC_QT * TC_QS + TC_NB * TC_KS) * (int) sizeof(float);
good = good && cc >= 8 &&
cudaFuncSetAttribute(block_scores_tc_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, bytes) ==
cudaSuccess &&
cudaFuncSetAttribute(tp_full_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, bytes) == cudaSuccess;
cudaGetLastError();
ok[dev] = good ? 1 : 2;
}
return ok[dev] == 1;
}
} // namespace
bool qsa_two_pass_on() {
static two_pass::RequestStatus status(std::getenv("STRATA_SELECT_TWO_PASS"));
if (!status.requested()) return false;
// the card current at this call (a layer split can mix cards): sm_75 or STRATA_QSA_WARP=1|select keep the other
// scorer there, so the switch says once that it is not used; checked here too, so a short prompt says it as well
if (!tp_device_ok()) (void) status.on("this CUDA card or with STRATA_QSA_WARP");
return true;
}
bool qsa_two_pass_check_on() {
static const bool on = [] {
const char* v = std::getenv("STRATA_SELECT_TWO_PASS_CHECK");
return v && v[0] == '1' && v[1] == '\0';
}();
return on;
}
int64_t qsa_two_pass_min_blocks() {
static const int64_t v = [] {
const char* e = std::getenv("STRATA_SELECT_TWO_PASS_MIN");
const long long x = e ? std::atoll(e) : (long long) two_pass::kMinBlocksDefault;
return (int64_t) (x < 0 ? 0 : x);
}();
return v;
}
uint64_t qsa_two_pass_scratch_bytes(int64_t nq_cap, int64_t max_blocks) { return two_pass::layout(nq_cap, max_blocks).total; }
bool qsa_two_pass_keys(const float* pooled, int64_t n_blocks, int64_t nq_cap, int64_t max_blocks, void* scratch,
uint64_t scratch_bytes, void* stream) {
if (!qsa_two_pass_on() || !tp_device_ok() || n_blocks > max_blocks || nq_cap <= 0 || nq_cap > 4096) return false;
const two_pass::Layout L = two_pass::layout(nq_cap, max_blocks);
if (scratch == nullptr || scratch_bytes < L.total || (reinterpret_cast<uintptr_t>(scratch) & 15u) != 0) return false;
if (n_blocks <= 0) return true;
float2* kn = reinterpret_cast<float2*>(static_cast<uint8_t*>(scratch) + L.kn);
tp_keys_kernel<<<(unsigned) ((n_blocks + 7) / 8), 256, 0, (cudaStream_t) stream>>>(pooled, n_blocks, kn);
const cudaError_t e = cudaGetLastError();
if (e != cudaSuccess) { std::fprintf(stderr, "qsa two-pass keys: %s\n", cudaGetErrorString(e)); std::exit(1); }
return true;
}
bool qsa_block_scores_two_pass(const float* pooled, const float* dead, const float* q_idx, const int32_t* steps,
int64_t nq, int64_t nq_cap, int64_t max_blocks, const QsaShapes& s, float* scores,
void* scratch, uint64_t scratch_bytes, void* stream, int64_t active_blocks) {
if (nq <= 0) return true;
if (!qsa_two_pass_on() || s.idx_dim != IDX_DIM || s.idx_n_head != IDX_HEADS || s.idx_block != R ||
nq > nq_cap || nq_cap > 4096 || !tp_device_ok())
return false;
const two_pass::Layout L = two_pass::layout(nq_cap, max_blocks);
if (scratch == nullptr || scratch_bytes < L.total || (reinterpret_cast<uintptr_t>(scratch) & 15u) != 0) return false;
uint8_t* base = static_cast<uint8_t*>(scratch);
const float2* kn = reinterpret_cast<const float2*>(base + L.kn);
float2* qc = reinterpret_cast<float2*>(base + L.qc);
int* mode = reinterpret_cast<int*>(base + L.mode);
int* cnt = reinterpret_cast<int*>(base + L.cnt);
int* ids = reinterpret_cast<int*>(base + L.ids);
int* tiles = reinterpret_cast<int*>(base + L.tiles);
const cudaStream_t st = (cudaStream_t) stream;
const int64_t reach = active_blocks > 0 && active_blocks < max_blocks ? active_blocks : max_blocks;
static std::atomic<bool> said{false};
if (!said.exchange(true))
std::fprintf(stderr, "qsa select: two-pass scores on (STRATA_SELECT_TWO_PASS=1): calls whose last query has %lld+ "
"blocks score every block in single-pass TF32 and again in 3xTF32 where the bound reaches the "
"cut-off (the same cells as the 3xTF32 scorer)\n", (long long) qsa_two_pass_min_blocks());
const unsigned qtiles = (unsigned) ((nq + TC_QT - 1) / TC_QT);
const int64_t per = (int64_t) TC_NB * TP1_ITER;
tp_score1_kernel<<<dim3(qtiles, (unsigned) ((reach + per - 1) / per)), 128, 0, st>>>(pooled, q_idx, steps, nq,
max_blocks, reach, scores, tiles);
block_scores_tail_kernel<<<(unsigned) nq, 32, 0, st>>>(dead, q_idx, steps, max_blocks, scores);
tp_band_kernel<<<(unsigned) nq, TPB_T, (size_t) two_pass::kCandCap * 8, st>>>(scores, q_idx, steps, max_blocks, kn, qc,
mode, cnt, ids, tiles);
tp_rescore_kernel<<<(unsigned) nq, 128, 0, st>>>(pooled, q_idx, max_blocks, mode, cnt, ids, tiles, scores);
const int64_t ranges = (reach + (int64_t) TC_NB * TC_ITER - 1) / ((int64_t) TC_NB * TC_ITER);
const int bytes = (TC_QT * TC_QS + TC_NB * TC_KS) * (int) sizeof(float);
tp_full_kernel<<<dim3((unsigned) (ranges < 32 ? ranges : 32), qtiles), 128, bytes, st>>>(pooled, q_idx, steps, nq,
max_blocks, reach, tiles,
scores);
const cudaError_t e = cudaGetLastError();
if (e != cudaSuccess) { std::fprintf(stderr, "qsa two-pass scores: %s\n", cudaGetErrorString(e)); std::exit(1); }
return true;
}
void qsa_two_pass_check(const float* scores, const int32_t* ids, const float* ref_scores, const int32_t* ref_ids,
const int32_t* steps, int64_t nq, int64_t nq_cap, int64_t max_blocks, int64_t cap,
void* scratch, void* stream) {
if (nq <= 0 || scratch == nullptr) return;
const size_t mark = (size_t) ((max_blocks + 31) / 32) * 4;
if (mark > 48 * 1024) {
static std::atomic<bool> said{false};
if (!said.exchange(true)) std::fprintf(stderr, "qsa two-pass check: %lld blocks are too many to check\n", (long long) max_blocks);
return;
}
const two_pass::Layout L = two_pass::layout(nq_cap, max_blocks);
const uint8_t* base = static_cast<const uint8_t*>(scratch);
tp_check_kernel<<<(unsigned) nq, 256, mark, (cudaStream_t) stream>>>(
scores, ids, ref_scores, ref_ids, steps, max_blocks, cap, reinterpret_cast<const float2*>(base + L.kn),
reinterpret_cast<const float2*>(base + L.qc), reinterpret_cast<const int*>(base + L.mode),
reinterpret_cast<const int*>(base + L.cnt), reinterpret_cast<const int*>(base + L.ids),
reinterpret_cast<const int*>(base + L.tiles));
const cudaError_t e = cudaGetLastError();
if (e != cudaSuccess) { std::fprintf(stderr, "qsa two-pass check: %s\n", cudaGetErrorString(e)); std::exit(1); }
}
bool qsa_two_pass_stats(unsigned long long* out, bool reset, void* stream) {
const cudaStream_t st = (cudaStream_t) stream;
void* p = nullptr;
if (cudaGetSymbolAddress(&p, g_tp_stats) != cudaSuccess) { cudaGetLastError(); return false; }
const size_t bytes = sizeof(unsigned long long) * two_pass::kStatCount;
if (cudaMemcpyAsync(out, p, bytes, cudaMemcpyDeviceToHost, st) != cudaSuccess ||
(reset && cudaMemsetAsync(p, 0, bytes, st) != cudaSuccess) || cudaStreamSynchronize(st) != cudaSuccess) {
cudaGetLastError();
return false;
}
return true;
}
#else // HIP: the two-pass scorer is CUDA-only (the gfx12 WMMA scorer has another arithmetic); nothing runs
bool qsa_two_pass_on() { // false; a set switch says once that it is not used here
static two_pass::RequestStatus status(std::getenv("STRATA_SELECT_TWO_PASS"));
return status.on("HIP");
}
bool qsa_two_pass_check_on() { return false; }
int64_t qsa_two_pass_min_blocks() { return two_pass::kMinBlocksDefault; }
uint64_t qsa_two_pass_scratch_bytes(int64_t nq_cap, int64_t max_blocks) { return two_pass::layout(nq_cap, max_blocks).total; }
bool qsa_two_pass_keys(const float*, int64_t, int64_t, int64_t, void*, uint64_t, void*) { return false; }
bool qsa_block_scores_two_pass(const float*, const float*, const float*, const int32_t*, int64_t, int64_t, int64_t,
const QsaShapes&, float*, void*, uint64_t, void*, int64_t) { return false; }
void qsa_two_pass_check(const float*, const int32_t*, const float*, const int32_t*, const int32_t*, int64_t, int64_t,
int64_t, int64_t, void*, void*) {}
bool qsa_two_pass_stats(unsigned long long*, bool, void*) { return false; }
#endif
} // namespace strata::kernels
+224
View File
@@ -0,0 +1,224 @@
// STRATA_SELECT_TWO_PASS (qsa_two_pass.hpp) against today's prompt scorer, GPU, synthetic, no model: for prompt
// chunks of 256 queries at depths from 2K to 256K and several kinds of keys (normal, indexer-like, wide magnitudes,
// small integers with exact ties, near-duplicate and duplicate keys), qsa_block_topk over the two-pass row must return
// exactly the ids it returns over qsa_block_scores_tc's row, every scored-again block must hold today's score bit for
// bit, and every kept single-pass score must lie inside its bound (qsa_two_pass_check's counters). Then, for the
// larger cases, the time of each scorer (one synthetic run, not a benchmark). Needs CUDA sm_80+ (77 = skipped
// otherwise, and on HIP).
#include "strata/kernels/qsa.hpp"
#include "strata/kernels/qsa_select.hpp"
#include "strata/kernels/qsa_two_pass.hpp"
#include <cuda_runtime.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <vector>
namespace k = strata::kernels;
namespace tp = strata::kernels::two_pass;
namespace {
void ck(cudaError_t e) {
if (e != cudaSuccess) {
std::fprintf(stderr, "CUDA: %s\n", cudaGetErrorString(e));
std::exit(2);
}
}
enum Kind { kNormal, kIndexer, kWide, kSmall, kNear, kDup, kKinds };
const char* kKindName[kKinds] = {"normal", "indexer-like", "wide magnitudes", "small integers", "near-duplicate keys",
"duplicate keys"};
struct Case { int64_t ctx, queries; Kind kind; bool time; };
// stride: the score row's stride in blocks; 0 = the case's own (ctx / 4 + 2), else the engine's (--max-context 262144:
// 65,538, so qsa_block_topk takes the engine's dispatch on this card)
bool run(const Case& c, int64_t stride) {
const auto s = k::qsa_real_shapes();
const int64_t D = s.idx_dim, H = s.idx_n_head, own = c.ctx / s.idx_block + 2;
const int64_t max_blocks = stride > own ? stride : own;
const int64_t nq = c.queries, nq_cap = 256, cap = k::qsa_selection_width(k::kTopkMaxCells, s);
std::mt19937 rng(77001 + (unsigned) c.ctx + 13 * (unsigned) c.queries + 1009 * (unsigned) c.kind);
std::normal_distribution<float> nd(0.f, 1.f);
std::uniform_int_distribution<int> small(-3, 3), expo(-16, 16), coin(0, 9);
std::vector<float> pooled((size_t) (max_blocks * D)), dead((size_t) D), q((size_t) (nq * H * D));
auto unit = [&](float* v, int n, float norm) {
double ss = 0;
for (int i = 0; i < n; ++i) ss += (double) v[i] * v[i];
const float f = ss > 0 ? (float) (norm / std::sqrt(ss)) : 0.f;
for (int i = 0; i < n; ++i) v[i] *= f;
};
for (int64_t b = 0; b < max_blocks; ++b) {
float* kb = pooled.data() + b * D;
if (c.kind == kIndexer) { // the mean of 4 RMS-normed cell keys, as the indexer pools them
for (int i = 0; i < D; ++i) kb[i] = 0.f;
for (int cell = 0; cell < 4; ++cell) {
float ck_[128];
for (int i = 0; i < D; ++i) ck_[i] = nd(rng);
unit(ck_, (int) D, std::sqrt((float) D));
for (int i = 0; i < D; ++i) kb[i] += 0.25f * ck_[i];
}
} else if (c.kind == kNear && b > 0 && coin(rng) < 5) { // half the keys a hair from the one before
for (int i = 0; i < D; ++i) kb[i] = kb[i - D] * (1.0f + 1e-6f * nd(rng));
} else if (c.kind == kDup && b >= 8 && coin(rng) < 5) { // half the keys copies of an earlier one: exact ties
const int64_t src = (int64_t) (rng() % (uint32_t) b);
std::memcpy(kb, pooled.data() + src * D, (size_t) D * 4);
} else {
for (int i = 0; i < D; ++i) {
if (c.kind == kWide) kb[i] = coin(rng) == 0 ? 0.f : std::ldexp(nd(rng), expo(rng));
else if (c.kind == kSmall) kb[i] = (float) small(rng);
else kb[i] = nd(rng);
}
}
}
for (auto& v : dead) v = nd(rng);
for (int64_t i = 0; i < nq; ++i)
for (int64_t h = 0; h < H; ++h) {
float* qh = q.data() + (i * H + h) * D;
if (c.kind == kIndexer) { // RMS-normed, leaning towards a few keys (a peaked score row)
for (int j = 0; j < D; ++j) qh[j] = nd(rng);
for (int r = 0; r < 3; ++r) {
const float* kb = pooled.data() + (int64_t) (rng() % (uint32_t) (max_blocks - 2)) * D;
for (int j = 0; j < D; ++j) qh[j] += 0.5f * kb[j];
}
unit(qh, (int) D, std::sqrt((float) D));
} else {
for (int j = 0; j < D; ++j) {
if (c.kind == kWide) qh[j] = coin(rng) == 0 ? 0.f : std::ldexp(nd(rng), expo(rng));
else if (c.kind == kSmall) qh[j] = (float) small(rng);
else qh[j] = nd(rng);
}
}
}
std::vector<int32_t> steps((size_t) (nq * k::kStepCount));
for (int64_t i = 0; i < nq; ++i) {
int32_t* st = steps.data() + i * k::kStepCount;
const int64_t pos = c.ctx - nq + i;
st[k::kStepPos] = (int32_t) pos;
st[k::kStepNKv] = (int32_t) (pos + 1);
st[k::kStepNBid] = (int32_t) ((pos + 1) / s.idx_block);
st[k::kStepWidth] = (int32_t) k::qsa_selection_width(pos + 1, s);
}
const int64_t last_nbid = steps[(size_t) ((nq - 1) * k::kStepCount + k::kStepNBid)], active = last_nbid + 1;
float *dp, *dd, *dq, *ref, *two;
int32_t *dt, *ref_ids, *two_ids;
void* scratch;
const uint64_t sbytes = k::qsa_two_pass_scratch_bytes(nq_cap, max_blocks);
const size_t n_sc = (size_t) (nq * max_blocks), n_id = (size_t) (nq * cap);
ck(cudaMalloc(&dp, pooled.size() * 4)); ck(cudaMalloc(&dd, dead.size() * 4)); ck(cudaMalloc(&dq, q.size() * 4));
ck(cudaMalloc(&dt, steps.size() * 4)); ck(cudaMalloc(&ref, n_sc * 4)); ck(cudaMalloc(&two, n_sc * 4));
ck(cudaMalloc(&ref_ids, n_id * 4)); ck(cudaMalloc(&two_ids, n_id * 4)); ck(cudaMalloc(&scratch, sbytes));
ck(cudaMemcpy(dp, pooled.data(), pooled.size() * 4, cudaMemcpyHostToDevice));
ck(cudaMemcpy(dd, dead.data(), dead.size() * 4, cudaMemcpyHostToDevice));
ck(cudaMemcpy(dq, q.data(), q.size() * 4, cudaMemcpyHostToDevice));
ck(cudaMemcpy(dt, steps.data(), steps.size() * 4, cudaMemcpyHostToDevice));
ck(cudaMemset(ref, 0x7f, n_sc * 4)); ck(cudaMemset(two, 0x3e, n_sc * 4));
ck(cudaMemset(ref_ids, 0xff, n_id * 4)); ck(cudaMemset(two_ids, 0xee, n_id * 4));
if (!k::qsa_block_scores_tc(dp, dd, dq, dt, nq, max_blocks, s, ref, nullptr, active)) {
std::puts("SKIP: no tensor-core scorer on this card");
std::exit(77);
}
k::qsa_block_topk(ref, dt, nq, max_blocks, cap, s, ref_ids, nullptr, active);
unsigned long long st0[tp::kStatCount];
k::qsa_two_pass_stats(st0, true, nullptr); // zero the counters
if (!k::qsa_two_pass_keys(dp, last_nbid, nq_cap, max_blocks, scratch, sbytes, nullptr) ||
!k::qsa_block_scores_two_pass(dp, dd, dq, dt, nq, nq_cap, max_blocks, s, two, scratch, sbytes, nullptr, active)) {
std::fprintf(stderr, "FAIL: the two-pass scorer did not run (ctx %lld)\n", (long long) c.ctx);
return false;
}
k::qsa_block_topk(two, dt, nq, max_blocks, cap, s, two_ids, nullptr, active);
k::qsa_two_pass_check(two, two_ids, ref, ref_ids, dt, nq, nq_cap, max_blocks, cap, scratch, nullptr);
ck(cudaDeviceSynchronize());
unsigned long long st[tp::kStatCount];
if (!k::qsa_two_pass_stats(st, true, nullptr)) { std::fprintf(stderr, "FAIL: counters\n"); return false; }
// the ids on the host too, every entry qsa_block_topk wrote
std::vector<int32_t> a(n_id), b(n_id);
ck(cudaMemcpy(a.data(), ref_ids, n_id * 4, cudaMemcpyDeviceToHost));
ck(cudaMemcpy(b.data(), two_ids, n_id * 4, cudaMemcpyDeviceToHost));
int64_t bad_q = 0, compared = 0;
for (int64_t i = 0; i < nq; ++i) {
const int64_t n_kv = steps[(size_t) (i * k::kStepCount + k::kStepNKv)];
const int64_t w = std::min<int64_t>(n_kv, steps[(size_t) (i * k::kStepCount + k::kStepWidth)]);
compared += w;
if (!std::equal(a.begin() + i * cap, a.begin() + i * cap + w, b.begin() + i * cap)) ++bad_q;
}
const bool ok = bad_q == 0 && st[tp::kChkIdQueries] == 0 && st[tp::kChkRescoredDiff] == 0 && st[tp::kChkKeptOut] == 0 &&
st[tp::kChkQueries] == (unsigned long long) nq;
float ratio;
const uint32_t rb = (uint32_t) st[tp::kChkRatioMax];
std::memcpy(&ratio, &rb, 4);
const unsigned long long kept = st[tp::kStQueries] - st[tp::kStFull];
std::printf("%s ctx=%7lld stride=%5lld queries=%3lld %-20s: ids %lld queries differ (%lld ids compared); scored "
"again %llu (%.1f per query), %llu of %llu scored-again blocks not bitwise; flagged %llu (%llu tiles in "
"full); kept %llu, outside the bound %llu, max |S1-S3|/eps %.4f\n",
ok ? "ok " : "FAIL", (long long) c.ctx, (long long) max_blocks, (long long) nq, kKindName[c.kind],
(long long) bad_q, (long long) compared, st[tp::kStBand], kept ? (double) st[tp::kStBand] / kept : 0.0,
st[tp::kChkRescoredDiff], st[tp::kChkRescored], st[tp::kStFull], st[tp::kStFullTiles], st[tp::kChkKept],
st[tp::kChkKeptOut], (double) ratio);
if (ok && c.time && stride > 0) { // one synthetic observation of each scorer (the bench worker measures the engine)
cudaEvent_t e0, e1, e2;
ck(cudaEventCreate(&e0)); ck(cudaEventCreate(&e1)); ck(cudaEventCreate(&e2));
constexpr int N = 5;
ck(cudaEventRecord(e0));
for (int r = 0; r < N; ++r) k::qsa_block_scores_tc(dp, dd, dq, dt, nq, max_blocks, s, ref, nullptr, active);
ck(cudaEventRecord(e1));
for (int r = 0; r < N; ++r)
k::qsa_block_scores_two_pass(dp, dd, dq, dt, nq, nq_cap, max_blocks, s, two, scratch, sbytes, nullptr, active);
ck(cudaEventRecord(e2));
ck(cudaEventSynchronize(e2));
float t3 = 0, t2 = 0;
ck(cudaEventElapsedTime(&t3, e0, e1)); ck(cudaEventElapsedTime(&t2, e1, e2));
std::printf(" time per call (synthetic, one run): 3xTF32 scorer %.3f ms, two-pass %.3f ms (keys once per chunk "
"not included)\n", t3 / N, t2 / N);
k::qsa_two_pass_stats(st, true, nullptr);
cudaEventDestroy(e0); cudaEventDestroy(e1); cudaEventDestroy(e2);
}
ck(cudaFree(dp)); ck(cudaFree(dd)); ck(cudaFree(dq)); ck(cudaFree(dt)); ck(cudaFree(ref)); ck(cudaFree(two));
ck(cudaFree(ref_ids)); ck(cudaFree(two_ids)); ck(cudaFree(scratch));
return ok;
}
} // namespace
int main() {
#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)
// AMD: the two-pass scorer is CUDA-only (qsa_select.cu builds stubs under HIP), so the engine keeps today's scorer
std::puts("SKIP: the two-pass scorer is CUDA-only");
return 77;
#endif
#if defined(_WIN32)
if (std::getenv("STRATA_SELECT_TWO_PASS") == nullptr) _putenv_s("STRATA_SELECT_TWO_PASS", "1");
#else
setenv("STRATA_SELECT_TWO_PASS", "1", 0); // read once, at the first call
#endif
int devices = 0;
if (cudaGetDeviceCount(&devices) != cudaSuccess || devices == 0) {
std::puts("SKIP: no CUDA device");
return 77;
}
if (!k::qsa_two_pass_on()) {
std::puts("FAIL: STRATA_SELECT_TWO_PASS is set to something other than 1");
return 1;
}
const Case cases[] = {
{2100, 64, kNormal, false}, {4096, 256, kNormal, false}, {9000, 37, kNormal, false},
{20000, 256, kNormal, false}, {20481, 256, kSmall, false}, {32768, 256, kIndexer, false},
{40000, 256, kWide, false}, {50000, 200, kNear, false}, {70000, 256, kDup, false},
{131072, 256, kIndexer, false}, {131075, 129, kNormal, false}, {262144, 256, kIndexer, true},
{262144, 256, kNormal, true}, {200003, 256, kSmall, false}, {65536, 256, kIndexer, true},
};
bool all = true;
for (const int64_t stride : {(int64_t) 0, (int64_t) 65538})
for (const Case& c : cases) all = run(c, stride) && all;
if (!all) {
std::puts("FAIL: the two-pass selection differs from the 3xTF32 scorer's");
return 1;
}
std::puts("PASS: two-pass ids identical to the 3xTF32 scorer's, scored-again blocks bitwise, kept blocks inside the bound");
return 0;
}
+108 -2
View File
@@ -49,6 +49,8 @@
#include "strata/kernels/qsa_pattn.hpp"
#include "strata/kernels/qsa_prompt_attn.hpp"
#include "strata/kernels/qsa_select.hpp"
#include "strata/kernels/qsa_two_pass.hpp"
#include "strata/kernels/qsa_two_pass_status.hpp"
#include "strata/prefill/gemm.hpp"
#include "strata/prefill/moe_fused.hpp"
#include "strata/prefill/moe_ws.hpp"
@@ -4382,6 +4384,22 @@ bool Prefill::run_impl(const int64_t* tokens, int64_t n, int64_t pos0, std::stri
break;
}
pt.mark(kPfQsaSel, cs);
#if !defined(STRATA_GPU_METAL)
// STRATA_SELECT_TWO_PASS=1 (qsa_two_pass.hpp): the tensor-core scorer's calls whose last query has
// STRATA_SELECT_TWO_PASS_MIN blocks or more score every block in single-pass TF32 and again in 3xTF32
// only where the error bound reaches the cut-off; qsa_block_topk below returns the same cells. Its
// scratch is m.attn, which the attention below writes and nothing reads before; the key norms are
// this layer's and chunk's (the indexer appends above are complete)
static const bool tp_on = strata::kernels::qsa_two_pass_on();
static const bool tp_check = tp_on && strata::kernels::qsa_two_pass_check_on();
static const int64_t tp_min = strata::kernels::qsa_two_pass_min_blocks();
const uint64_t tp_bytes = (uint64_t) m.T * ZV * sizeof(float);
const int64_t tp_nbid = T > 0 ? (int64_t) m.steps_host[(size_t) ((T - 1) * strata::kernels::kStepCount +
strata::kernels::kStepNBid)] : 0;
const bool tp_ok = tp_on && tp_nbid + 1 >= tp_min &&
strata::kernels::qsa_two_pass_keys(st.idx_pooled, tp_nbid, m.sel_batch, m.max_blocks,
m.attn, tp_bytes, m.cs);
#endif
for (int64_t t0 = 0; t0 < T; t0 += m.sel_batch) {
const int64_t nb = std::min(m.sel_batch, T - t0);
const int32_t* steps0 = m.steps_dev + t0 * strata::kernels::kStepCount;
@@ -4391,13 +4409,66 @@ bool Prefill::run_impl(const int64_t* tokens, int64_t n, int64_t pos0, std::stri
// the scores on tensor cores (3xTF32: FP32-level, not bitwise), below sm_80 the FP32 tiled kernel with STRATA_SELECT_SIMT=1;
// STRATA_SELECT_OLD=1: the warp kernel
static const bool old_sel = std::getenv("STRATA_SELECT_OLD") != nullptr;
if (old_sel || !strata::kernels::qsa_block_scores_tc(st.idx_pooled, st.idx_dead, m.q_idx + t0 * 512,
bool tp_call = false;
#if !defined(STRATA_GPU_METAL)
tp_call = tp_ok && !old_sel && active >= tp_min &&
strata::kernels::qsa_block_scores_two_pass(st.idx_pooled, st.idx_dead, m.q_idx + t0 * 512,
steps0, nb, m.sel_batch, m.max_blocks, s,
m.sel_scores, m.attn, tp_bytes, m.cs, active);
#endif
if (!tp_call && (old_sel || !strata::kernels::qsa_block_scores_tc(st.idx_pooled, st.idx_dead, m.q_idx + t0 * 512,
steps0, nb, m.max_blocks, s, m.sel_scores,
m.cs, active))
m.cs, active)))
strata::kernels::qsa_block_scores(st.idx_pooled, st.idx_dead, m.q_idx + t0 * 512, steps0, nb,
m.max_blocks, s, m.sel_scores, m.cs, active);
strata::kernels::qsa_block_topk(m.sel_scores, steps0, nb, m.max_blocks, m.cap, s,
m.sel_ids + t0 * m.cap, m.cs, active);
#if !defined(STRATA_GPU_METAL)
// STRATA_SELECT_TWO_PASS_CHECK=1 (diagnostic, one GPU): today's scores and ids for the same
// queries, compared with the two-pass ones (counts at the prompt's end). Their buffers are the
// rest of m.attn past the two-pass scratch when it is large enough (the attention below writes
// m.attn after the comparison), else buffers of their own
if (tp_call && tp_check) {
static float* own_sc = nullptr;
static int32_t* own_ids = nullptr;
static size_t own_sc_n = 0, own_ids_n = 0;
const size_t want_sc = (size_t) m.sel_batch * (size_t) m.max_blocks, want_ids = (size_t) m.sel_batch * (size_t) m.cap;
const uint64_t at = (strata::kernels::qsa_two_pass_scratch_bytes(m.sel_batch, m.max_blocks) + 255u) & ~uint64_t(255u);
const uint64_t at_ids = at + (((uint64_t) want_sc * 4u + 255u) & ~uint64_t(255u));
float* ref_sc = nullptr;
int32_t* ref_ids = nullptr;
if (at_ids + (uint64_t) want_ids * 4u <= tp_bytes) {
ref_sc = reinterpret_cast<float*>(reinterpret_cast<uint8_t*>(m.attn) + at);
ref_ids = reinterpret_cast<int32_t*>(reinterpret_cast<uint8_t*>(m.attn) + at_ids);
} else {
if (want_sc > own_sc_n || want_ids > own_ids_n) {
cudaStreamSynchronize(m.cs);
cudaFree(own_sc);
cudaFree(own_ids);
own_sc = nullptr;
own_ids = nullptr;
own_sc_n = own_ids_n = 0;
if (cudaMalloc((void**) &own_sc, want_sc * 4) != cudaSuccess ||
cudaMalloc((void**) &own_ids, want_ids * 4) != cudaSuccess) {
err = "STRATA_SELECT_TWO_PASS_CHECK: no room for its buffers";
return false;
}
own_sc_n = want_sc;
own_ids_n = want_ids;
}
ref_sc = own_sc;
ref_ids = own_ids;
}
if (!strata::kernels::qsa_block_scores_tc(st.idx_pooled, st.idx_dead, m.q_idx + t0 * 512, steps0, nb,
m.max_blocks, s, ref_sc, m.cs, active)) {
err = "STRATA_SELECT_TWO_PASS_CHECK: the tensor-core scorer did not run";
return false;
}
strata::kernels::qsa_block_topk(ref_sc, steps0, nb, m.max_blocks, m.cap, s, ref_ids, m.cs, active);
strata::kernels::qsa_two_pass_check(m.sel_scores, m.sel_ids + t0 * m.cap, ref_sc, ref_ids, steps0, nb,
m.sel_batch, m.max_blocks, m.cap, m.attn, m.cs);
}
#endif
}
// STRATA_SEL_OVERLAP (debug, D-1's question): how much do neighbouring queries' selections share?
// Per tile of 16 queries: the union of their selected cells against the sum of their widths.
@@ -6298,6 +6369,31 @@ bool Prefill::run_impl(const int64_t* tokens, int64_t n, int64_t pos0, std::stri
(long long) kv_prefetches);
}
stats_.ms_total += ms_since(t_start);
#if !defined(STRATA_GPU_METAL)
if (strata::kernels::qsa_two_pass_on()) { // STRATA_SELECT_TWO_PASS: what the two-pass scorer did in this prompt
namespace tp = strata::kernels::two_pass;
unsigned long long c[tp::kStatCount] = {};
if (strata::kernels::qsa_two_pass_stats(c, true, m.cs) && c[tp::kStCalls] > 0) {
const unsigned long long sparse = c[tp::kStQueries], kept = sparse - c[tp::kStFull];
std::fprintf(stderr, "strata prefill: two-pass QSA select: %llu calls, %llu queries (%llu blocks) in single-pass TF32, "
"%llu blocks again in 3xTF32 (%.1f per query, max %llu), %llu queries flagged (%llu "
"16-query tiles in full), %llu selecting every cell; candidates %.0f per query (max %llu)\n",
c[tp::kStCalls], sparse, c[tp::kStBlocks], c[tp::kStBand], kept ? (double) c[tp::kStBand] / kept : 0.0,
c[tp::kStBandMax], c[tp::kStFull], c[tp::kStFullTiles], c[tp::kStIdentity],
kept ? (double) c[tp::kStCand] / kept : 0.0, c[tp::kStCandMax]);
if (c[tp::kChkQueries] > 0) {
const uint32_t rb = (uint32_t) c[tp::kChkRatioMax];
float ratio = 0.0f;
std::memcpy(&ratio, &rb, 4);
std::fprintf(stderr, "strata prefill: two-pass check: %llu queries, %llu with ids not the 3xTF32 scorer's (%llu ids); "
"scored again: %llu of %llu blocks not the 3xTF32 score bit for bit; kept: %llu of %llu "
"single-pass scores outside the bound, the largest |S1 - S3| / bound %.4g\n",
c[tp::kChkQueries], c[tp::kChkIdQueries], c[tp::kChkIdEntries], c[tp::kChkRescoredDiff],
c[tp::kChkRescored], c[tp::kChkKeptOut], c[tp::kChkKept], (double) ratio);
}
}
}
#endif
{
static const bool f16_range = std::getenv("STRATA_F16_RANGE") != nullptr;
float xm = 0.0f, wy[2] = {0.0f, 0.0f}; unsigned long long xo = 0, wyo[2] = {0, 0};
@@ -7800,7 +7896,12 @@ bool Prefill::reserve_uploads(int64_t T) {
}
bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) {
#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)
(void) strata::kernels::qsa_two_pass_on(); // HIP: a set STRATA_SELECT_TWO_PASS says once it is not used
#endif
#if defined(STRATA_GPU_METAL)
static strata::kernels::two_pass::RequestStatus tp_status(std::getenv("STRATA_SELECT_TWO_PASS"));
(void) tp_status.on("Metal");
{
const auto t0 = std::chrono::steady_clock::now();
// DFlash2: the window writes the taps of the prompt rows at positions >= tap_first_ and hands them to on_taps after each chunk
@@ -7839,6 +7940,11 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e
}
}
if (lm_prepare_chain(n, why) > 0) {
#if !defined(STRATA_USE_HIP) && !defined(STRATA_HIP_GFX906)
// STRATA_SELECT_TWO_PASS is in run_impl's selection only, not in this walk: say so once
static strata::kernels::two_pass::RequestStatus tp_status(std::getenv("STRATA_SELECT_TWO_PASS"));
(void) tp_status.on("the layer-major prompt path");
#endif
bool ok = run_layer_major(tokens, n, pos0, err);
std::string drain_err; // a layer split: the later stages' last super-chunk
if (!drain_pipeline(drain_err) && ok) {
+3
View File
@@ -37,6 +37,7 @@
#include "strata/kernels/qsa_decode_attn.hpp"
#include "strata/kernels/qsa_prompt_attn.hpp"
#include "strata/kernels/qsa_select.hpp"
#include "strata/kernels/qsa_two_pass_status.hpp"
#include "strata/prefill/gemm.hpp"
#include "strata/prefill/moe_fused.hpp"
#include "strata/prefill/moe_fused_iq.hpp"
@@ -5196,6 +5197,8 @@ bool Prefill::drain_pipeline(std::string& err) {
}
bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) {
static strata::kernels::two_pass::RequestStatus tp_status(std::getenv("STRATA_SELECT_TWO_PASS"));
(void) tp_status.on("SYCL"); // the two-pass scorer is CUDA-only: say once that a set switch is not used
const bool body_ok = run_impl(tokens, n, pos0, err);
std::string drain_err;
@@ -0,0 +1,80 @@
// STRATA_SELECT_TWO_PASS's request and its line where the two-pass scorer cannot run (qsa_two_pass_status.hpp).
// GPU-free: the parse, quiet when unset or supported, one line per process where unsupported.
#include "strata/kernels/qsa_two_pass_status.hpp"
#include <cstdio>
#include <cstdlib>
#include <string>
namespace tp = strata::kernels::two_pass;
static void check(bool ok, const char* why) {
if (!ok) {
std::fprintf(stderr, "FAIL: %s\n", why);
std::exit(1);
}
}
static std::string output(FILE* log) {
check(std::fflush(log) == 0 && std::fseek(log, 0, SEEK_SET) == 0, "rewind the log");
std::string text;
char buf[512];
while (const size_t n = std::fread(buf, 1, sizeof(buf), log)) text.append(buf, n);
check(!std::ferror(log), "read the log");
return text;
}
static std::string line(const char* where) {
return std::string("strata: STRATA_SELECT_TWO_PASS=1 is not used on ") + where +
": the two-pass scorer runs only in the CUDA prompt path's tensor-core selection (sm_80+); the selection is "
"unchanged\n";
}
int main(int argc, char** argv) {
check(argc == 2, "pass a scratch log path (no GPU or model needed)");
FILE* log = std::fopen(argv[1], "w+");
check(log != nullptr, "open the log");
// exactly "1" is on, as the scorer parsed it before; unset or anything else is off and prints nothing anywhere
check(tp::requested_from("1"), "1: on");
const char* off[] = {nullptr, "", "0", "01", "1x", "true", "1 ", "2"};
for (const char* flag : off) {
check(!tp::requested_from(flag), "not exactly 1: off");
tp::RequestStatus status(flag);
check(!status.requested(), "not requested");
check(!status.on("HIP", log), "off stays off where unsupported");
check(!status.on(nullptr, log), "off stays off where supported");
}
check(output(log).empty(), "no line without the switch");
// a supported path returns the request and prints nothing (the engine's per-segment line reports what ran)
tp::RequestStatus cuda("1");
check(cuda.on(nullptr, log) && cuda.on(nullptr, log), "supported: on");
check(output(log).empty(), "supported: no line");
// each unsupported path the engine names: off, one line, never repeated
const char* where[] = {"HIP", "Metal", "SYCL", "this CUDA card or with STRATA_QSA_WARP",
"the layer-major prompt path"};
std::string expected;
for (const char* w : where) {
tp::RequestStatus status("1");
check(!status.on(w, log), "unsupported: off");
expected += line(w);
check(output(log) == expected, "unsupported: one line naming the path");
check(!status.on(w, log) && !status.on(w, log), "unsupported again: off");
check(output(log) == expected, "unsupported: the line is printed once");
}
// a layer split can mix cards: a supported query neither uses up the line nor is turned off by an unsupported one
tp::RequestStatus mixed("1");
check(mixed.on(nullptr, log), "supported card first");
check(!mixed.on("this CUDA card or with STRATA_QSA_WARP", log), "then an unsupported card");
check(mixed.on(nullptr, log), "the supported card stays on");
expected += line("this CUDA card or with STRATA_QSA_WARP");
check(output(log) == expected, "the unsupported card's line once");
check(std::fclose(log) == 0, "close the log");
check(std::remove(argv[1]) == 0, "remove the log");
std::puts("qsa_two_pass_status_test: OK");
return 0;
}
+442
View File
@@ -0,0 +1,442 @@
// tests/prefill/qsa_two_pass_test.cpp - STRATA_SELECT_TWO_PASS without a GPU (qsa_two_pass.hpp).
//
// 1. TF32: the host copy of cvt.rna.tf32.f32 rounds to nearest, ties away from zero.
// 2. The bound: for random and adversarial indexer queries and keys, the single-pass score S1 and the 3xTF32 score S3
// are computed from the same TF32 operands as the kernels, with each MMA modelled two ways - exact then rounded to
// nearest (the most accurate hardware), and aligned to the largest addend with every addend and the result
// truncated to 24 bits (no guard bits, the least accurate model) - and |S1 - S3| must stay within eps. The exact
// cross term |qh.kl + ql.kh| must stay within its Cauchy-Schwarz part.
// 3. The cut-off rule: rows of 3xTF32 scores, single-pass scores anywhere inside [S3 - eps, S3 + eps] (pushed across
// the threshold where possible), ties at the threshold, every tail weight, short and long rows (the sampled
// candidate threshold), the identity: a host copy of the band kernel picks the blocks to score again, and the
// top-k of the resulting row (block_topk_kernel's rule) must be the top-k of the 3xTF32 row, id for id.
// Exits non-zero on the first failure. No GPU, no model.
#include "strata/kernels/qsa_two_pass.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <random>
#include <vector>
namespace tp = strata::kernels::two_pass;
namespace {
int g_fail = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
std::fprintf(stderr, __VA_ARGS__); \
std::fprintf(stderr, "\n"); \
++g_fail; \
if (g_fail > 20) std::exit(1); \
} \
} while (0)
float up(double x) { // the smallest float >= x (x finite and in range)
float f = (float) x;
if ((double) f < x) f = std::nextafter(f, std::numeric_limits<float>::infinity());
return f;
}
float down(double x) {
float f = (float) x;
if ((double) f > x) f = std::nextafter(f, -std::numeric_limits<float>::infinity());
return f;
}
// Shewchuk's exact summation (Python's math.fsum): the sum of doubles, correctly rounded
double fsum(const std::vector<double>& xs) {
std::vector<double> p;
for (double x : xs) {
size_t i = 0;
for (double y : p) {
if (std::fabs(x) < std::fabs(y)) std::swap(x, y);
const double hi = x + y, lo = y - (hi - x);
if (lo != 0.0) p[i++] = lo;
x = hi;
}
p.resize(i);
p.push_back(x);
}
double s = 0.0;
for (size_t i = p.size(); i-- > 0;) s += p[i]; // the partials are non-overlapping: this is the rounded sum
return s;
}
// ---- 1. TF32
void test_tf32() {
auto bits = [](float f) { uint32_t b; std::memcpy(&b, &f, 4); return b; };
CHECK(tp::tf32_rna_host(1.0f) == 1.0f, "1.0");
CHECK(tp::tf32_rna_host(1.0f + 0x1p-11f) == 1.0f + 0x1p-10f, "a tie rounds away from zero");
CHECK(tp::tf32_rna_host(-(1.0f + 0x1p-11f)) == -(1.0f + 0x1p-10f), "a negative tie rounds away from zero");
CHECK(tp::tf32_rna_host(1.0f + 0x1p-12f) == 1.0f, "below the tie rounds down");
CHECK((bits(tp::tf32_rna_host(3.14159265f)) & 0x1fffu) == 0u, "the low 13 bits are zero");
std::mt19937 rng(7);
std::normal_distribution<float> nd;
for (int i = 0; i < 100000; ++i) {
const float x = nd(rng) * std::ldexp(1.0f, (int) (rng() % 40) - 20);
const float h = tp::tf32_rna_host(x);
CHECK(std::fabs((double) x - (double) h) <= std::ldexp(std::fabs((double) x), -11), "TF32 error %g", (double) x);
}
}
// ---- 2. the bound
struct Op { float h, l; }; // tf32(x), tf32(x - tf32(x)): the kernels' split
Op split(float x) {
const float h = tp::tf32_rna_host(x);
return {h, tp::tf32_rna_host(x - h)};
}
// one MMA: the accumulator c plus 8 exact products
float mma_rn(float c, const double* p) {
std::vector<double> t(p, p + 8);
t.push_back(c);
return (float) fsum(t);
}
float trunc_to(double v, int e) { // toward zero to a multiple of 2^e
const double q = std::ldexp(1.0, e);
return (float) (std::trunc(v / q) * q);
}
float mma_trunc(float c, const double* p) {
double t[9];
for (int i = 0; i < 8; ++i) t[i] = p[i];
t[8] = c;
int emax = std::numeric_limits<int>::min();
for (double v : t)
if (v != 0.0) { int e; std::frexp(v, &e); emax = std::max(emax, e); }
if (emax == std::numeric_limits<int>::min()) return 0.0f;
std::vector<double> a;
for (double v : t) { // every addend kept to 24 bits below the largest one's leading bit, toward zero
const double q = std::ldexp(1.0, emax - 24);
a.push_back(std::trunc(v / q) * q);
}
const double s = fsum(a);
if (s == 0.0) return 0.0f;
int e;
std::frexp(s, &e);
return trunc_to(s, e - 24); // the result truncated to FP32's 24 bits
}
struct Pair { std::vector<float> q, k; }; // q: 4 heads x 128, k: 128
// The scores the kernels compute, per model: S1 (qh*kh, 16 MMAs per head) and S3 (ql*kh, qh*kl, qh*kh per step).
void scores(const Pair& x, bool trunc, float& s1, float& s3, double cross[4], double hn[4][2], double kn[2]) {
std::vector<Op> k(128);
for (int i = 0; i < 128; ++i) k[i] = split(x.k[i]);
double kh2 = 0, kl2 = 0;
for (int i = 0; i < 128; ++i) { kh2 += (double) k[i].h * k[i].h; kl2 += (double) k[i].l * k[i].l; }
kn[0] = std::sqrt(kh2);
kn[1] = std::sqrt(kl2);
s1 = s3 = 0.0f;
for (int h = 0; h < 4; ++h) {
std::vector<Op> q(128);
for (int i = 0; i < 128; ++i) q[i] = split(x.q[h * 128 + i]);
double qh2 = 0, ql2 = 0;
for (int i = 0; i < 128; ++i) { qh2 += (double) q[i].h * q[i].h; ql2 += (double) q[i].l * q[i].l; }
hn[h][0] = std::sqrt(qh2);
hn[h][1] = std::sqrt(ql2);
std::vector<double> d;
for (int i = 0; i < 128; ++i) { d.push_back((double) q[i].h * k[i].l); d.push_back((double) q[i].l * k[i].h); }
cross[h] = fsum(d);
float c1 = 0.0f, c3 = 0.0f;
for (int st = 0; st < 16; ++st) {
double p_hh[8], p_lh[8], p_hl[8];
for (int j = 0; j < 8; ++j) {
const int i = 8 * st + j;
p_hh[j] = (double) q[i].h * k[i].h;
p_lh[j] = (double) q[i].l * k[i].h;
p_hl[j] = (double) q[i].h * k[i].l;
}
c1 = trunc ? mma_trunc(c1, p_hh) : mma_rn(c1, p_hh);
c3 = trunc ? mma_trunc(c3, p_lh) : mma_rn(c3, p_lh); // the kernel's order: al*bh, ah*bl, ah*bh
c3 = trunc ? mma_trunc(c3, p_hl) : mma_rn(c3, p_hl);
c3 = trunc ? mma_trunc(c3, p_hh) : mma_rn(c3, p_hh);
}
s1 += c1 > 0.0f ? c1 : 0.0f;
s3 += c3 > 0.0f ? c3 : 0.0f;
}
}
// eps as the band kernel forms it (norms rounded up, the coefficients and eps rounded up)
float eps_of(const double hn[4][2], const double kn[2]) {
double a = 0, c = 0;
for (int h = 0; h < 4; ++h) { a += hn[h][0]; c += hn[h][1]; }
a = up(a) * (1 + 1e-12);
c = up(c) * (1 + 1e-12);
double P, Q;
tp::coefficients(a, c, P, Q);
const float Pf = up(P), Qf = up(Q), kh = up(kn[0] * (1 + 1e-12)), kl = up(kn[1] * (1 + 1e-12));
return up((double) Pf * kl + (double) Qf * kh + (double) tp::kAbs);
}
void test_bound() {
std::mt19937 rng(20261024);
std::normal_distribution<float> nd;
std::uniform_int_distribution<int> small(-3, 3), expo(-12, 12), coin(0, 9);
// the worst rounding: mantissas just below a TF32 tie, so |x - tf32(x)| is nearly 2^-11 |x|
auto worst = [&](float sign) {
uint32_t b = 0x3f800000u | ((rng() & 0x3ffu) << 13) | 0x0fffu; // ...0 1111 1111 1111 in the dropped bits
float f;
std::memcpy(&f, &b, 4);
return sign * std::ldexp(f, expo(rng) / 4);
};
const char* names[] = {"normal", "wide magnitudes and zeros", "small integers", "aligned worst rounding (q = k)",
"worst rounding, signs of k"};
for (int mode = 0; mode < 5; ++mode) {
double worst_ratio[2] = {0, 0}, worst_cross = 0;
const int N = mode >= 3 ? 1500 : 3000;
for (int n = 0; n < N; ++n) {
Pair x;
x.q.resize(512);
x.k.resize(128);
for (int i = 0; i < 128; ++i) {
float v;
if (mode == 0) v = nd(rng);
else if (mode == 1) v = coin(rng) == 0 ? 0.0f : std::ldexp(nd(rng), expo(rng));
else if (mode == 2) v = (float) small(rng);
else v = worst(nd(rng) > 0 ? 1.0f : -1.0f);
x.k[i] = v;
}
for (int h = 0; h < 4; ++h)
for (int i = 0; i < 128; ++i) {
float v;
if (mode == 0) v = nd(rng);
else if (mode == 1) v = coin(rng) == 0 ? 0.0f : std::ldexp(nd(rng), expo(rng));
else if (mode == 2) v = (float) small(rng);
else if (mode == 3) v = x.k[i]; // every head aligned with k, lo parts too: Cauchy-Schwarz is tight
else v = worst(x.k[i] > 0 ? 1.0f : -1.0f); // independent worst roundings, positive dot products
x.q[h * 128 + i] = v;
}
for (int model = 0; model < 2; ++model) {
float s1, s3;
double cross[4], hn[4][2], kn[2];
scores(x, model == 1, s1, s3, cross, hn, kn);
const float e = eps_of(hn, kn);
const double d = std::fabs((double) s1 - (double) s3);
CHECK(d <= (double) e, "%s, %s model: |S1 - S3| = %.9g > eps %.9g", names[mode],
model ? "truncating" : "rounding", d, (double) e);
worst_ratio[model] = std::max(worst_ratio[model], d / (double) e);
if (model == 0)
for (int h = 0; h < 4; ++h) {
const double cb = hn[h][0] * kn[1] + hn[h][1] * kn[0];
CHECK(std::fabs(cross[h]) <= cb * (1 + 1e-12) + 1e-300, "%s: cross term %.9g > %.9g", names[mode],
std::fabs(cross[h]), cb);
if (cb > 0) worst_cross = std::max(worst_cross, std::fabs(cross[h]) / cb);
}
}
}
std::printf("bound, %-36s: largest |S1 - S3| / eps %.4f (rounding MMA), %.4f (truncating MMA); "
"cross term / its bound %.4f\n", names[mode], worst_ratio[0], worst_ratio[1], worst_cross);
}
}
// ---- 3. the cut-off rule
constexpr int R = 4;
uint32_t key(float s) { return tp::order_key_host(s); }
// the largest key k with (sum of weights of entries with key >= k) >= width (block_topk_kernel's threshold)
uint32_t kth(std::vector<std::pair<uint32_t, int>> kw, int64_t width) {
std::sort(kw.begin(), kw.end(), [](auto& a, auto& b) { return a.first > b.first; });
int64_t c = 0;
for (auto& [k, w] : kw) {
c += w;
if (c >= width) return k;
}
return 0u; // fewer cells than the width: the radix scan ends at digit 0 every pass
}
// block_topk_kernel's ids: cells of blocks above the threshold, and the first (width - above) tied cells by index
std::vector<int32_t> topk(const std::vector<float>& sc, int64_t n_kv, int64_t n_bid, int64_t width) {
std::vector<int32_t> out;
if (n_kv <= width) {
for (int64_t j = 0; j < n_kv; ++j) out.push_back((int32_t) j);
return out;
}
auto weight = [&](int64_t b) { return b < n_bid ? R : (int) (n_kv - n_bid * R); };
std::vector<std::pair<uint32_t, int>> kw;
for (int64_t b = 0; b <= n_bid; ++b)
if (weight(b)) kw.push_back({key(sc[b]), weight(b)});
const uint32_t thr = kth(kw, width);
int64_t above = 0;
for (int64_t b = 0; b <= n_bid; ++b)
if (weight(b) && key(sc[b]) > thr) above += weight(b);
int64_t eq_left = width - above;
for (int64_t b = 0; b <= n_bid; ++b) {
const int w = weight(b);
if (!w) continue;
const uint32_t k = key(sc[b]);
if (k > thr) for (int c = 0; c < w; ++c) out.push_back((int32_t) (b * R + c));
else if (k == thr) for (int c = 0; c < w && eq_left > 0; ++c, --eq_left) out.push_back((int32_t) (b * R + c));
}
return out;
}
struct Band { int mode = 0; std::vector<int64_t> again; int64_t cand = 0; };
// the band kernel on the host (tp_band_kernel, qsa_select.cu): the same steps and constants
Band band(const std::vector<float>& s1, const std::vector<float>& eps, int64_t n_kv, int64_t n_bid, int64_t width) {
Band r;
if (n_kv <= width) return r;
const int64_t nb = n_bid + 1;
const int w_tail = (int) (n_kv - n_bid * R);
auto interval = [&](int64_t b, float& lo, float& hi, float& e) {
e = eps[b];
lo = down((double) s1[b] - (double) e);
hi = up((double) s1[b] + (double) e);
};
float F = -std::numeric_limits<float>::infinity();
if (nb > tp::kCandCap) {
int64_t stride = tp::kSampleStride;
while ((nb + stride - 1) / stride > tp::kCandCap) stride *= 2;
const int64_t ns = (nb + stride - 1) / stride;
std::vector<std::pair<uint32_t, int>> kw;
for (int64_t i = 0; i < ns; ++i) {
const int64_t b = i * stride;
uint32_t k = 0u;
if (b < n_bid) { float lo, hi, e; interval(b, lo, hi, e); k = key(lo); }
else if (w_tail > 0) k = key(s1[b]);
kw.push_back({k, 1});
}
const int64_t ks = 2 * ((width + R * stride - 1) / (R * stride)) + 16;
const uint32_t fk = kth(kw, std::min(ks, ns));
if (fk != 0u) F = tp::key_float_host(fk);
}
std::vector<int64_t> cand;
int64_t wf = 0;
float E = 0.0f;
for (int64_t b = 0; b < nb; ++b) {
float lo, hi, e = 0.0f;
int w;
if (b < n_bid) { w = R; interval(b, lo, hi, e); }
else { w = w_tail; if (!w) continue; lo = hi = s1[b]; }
if (lo >= F) wf += w;
if (hi >= F) { cand.push_back(b); E = std::max(E, e); }
}
r.cand = (int64_t) cand.size();
if ((int64_t) cand.size() > tp::kCandCap || wf < width) { r.mode = 2; return r; }
std::vector<std::pair<uint32_t, int>> kw;
for (int64_t b : cand) kw.push_back({key(s1[b]), b < n_bid ? R : w_tail});
const float T1 = tp::key_float_host(kth(kw, width));
const float lo_b = std::max(down((double) T1 - (double) E), F), hi_b = up((double) T1 + (double) E);
int res[8] = {};
for (int64_t b : cand) {
if (b >= n_bid) continue;
float lo, hi, e;
interval(b, lo, hi, e);
if (hi >= lo_b && lo <= hi_b) { r.again.push_back(b); ++res[b & 7]; }
}
for (int x : res) if (x > tp::kResCap) { r.mode = 2; return r; }
r.mode = 1;
return r;
}
void test_rule() {
std::mt19937 rng(424242);
std::normal_distribution<double> nd;
std::uniform_real_distribution<double> ud(0.0, 1.0);
const int64_t sizes[] = {300, 512, 600, 1500, 4000, 5119, 5120, 5121, 9000, 20000, 65000};
const char* kinds[] = {"continuous", "small integers (ties)", "a plateau at the threshold", "eps wide against the spread",
"heavy tail", "every score equal"};
int64_t rows = 0, full = 0, again = 0, cand = 0;
for (int kind = 0; kind < 6; ++kind) {
const int64_t rows0 = rows, full0 = full, again0 = again;
for (int64_t n_bid : sizes) {
for (int tail = 0; tail < 4; ++tail) {
for (int rep = 0; rep < (n_bid > 20000 ? 2 : 4); ++rep) {
const int64_t n_kv = n_bid * R + tail, width = std::min<int64_t>(n_kv, 2048 + R - 1);
std::vector<float> s3((size_t) n_bid + 1), s1((size_t) n_bid + 1), eps((size_t) n_bid + 1);
// kind 0: continuous; 1: small integers (ties); 2: a plateau at the threshold; 3: eps wide
// against the spread; 4: heavy tail; 5: every score equal
for (int64_t b = 0; b < n_bid; ++b) {
double v;
if (kind == 1) v = (double) (int) (ud(rng) * 40);
else if (kind == 2) v = ud(rng) < 0.3 ? 7.0 : 7.0 + nd(rng);
else if (kind == 4) v = std::exp(2.0 * nd(rng));
else if (kind == 5) v = 3.0;
else v = std::max(0.0, 2.0 + nd(rng));
s3[b] = (float) v;
const double scale = kind == 3 ? 0.3 : (kind == 1 ? 0.6 : 0.002);
eps[b] = (float) (scale * (0.5 + ud(rng)));
}
s3[n_bid] = tail ? 1e9f + (float) ud(rng) : (float) ud(rng); // the tail: exact, +1e9 with cells
eps[n_bid] = 0.0f;
// the 3xTF32 threshold, to push single-pass scores across it
const std::vector<int32_t> ref = topk(s3, n_kv, n_bid, width);
std::vector<std::pair<uint32_t, int>> kw;
for (int64_t b = 0; b <= n_bid; ++b) {
const int w = b < n_bid ? R : tail;
if (w) kw.push_back({key(s3[b]), w});
}
const float T = n_kv > width ? tp::key_float_host(kth(kw, width)) : 0.0f;
for (int64_t b = 0; b < n_bid; ++b) {
// S1 inside [S3 - eps, S3 + eps]: towards and across the threshold where it can, else random
const double e = eps[b] * (1.0 - 1e-6);
double d;
if (rep % 2 == 0) d = (s3[b] >= T ? -e : e);
else d = (2.0 * ud(rng) - 1.0) * e;
s1[b] = (float) ((double) s3[b] + d);
if (std::fabs((double) s1[b] - (double) s3[b]) > eps[b]) s1[b] = s3[b];
}
s1[n_bid] = s3[n_bid];
const Band bd = band(s1, eps, n_kv, n_bid, width);
std::vector<float> row = s1;
if (bd.mode == 2) row = s3;
for (int64_t b : bd.again) row[b] = s3[b];
const std::vector<int32_t> got = topk(row, n_kv, n_bid, width);
CHECK(got == ref, "kind %d, %lld blocks, tail %d, rep %d: ids differ (mode %d, %zu scored again)", kind,
(long long) n_bid, tail, rep, bd.mode, bd.again.size());
++rows;
full += bd.mode == 2;
again += (int64_t) bd.again.size();
cand += bd.cand;
}
}
}
std::printf("rule, %-28s: %3lld rows, %3lld scored again in full, %.1f blocks scored again per row\n", kinds[kind],
(long long) (rows - rows0), (long long) (full - full0),
rows > rows0 ? (double) (again - again0) / (double) (rows - rows0) : 0.0);
}
std::printf("rule: %lld rows, ids identical in every one; %lld scored again in full (the list or the bound could not "
"settle them), %.1f blocks scored again and %.0f candidates per row\n", (long long) rows, (long long) full,
rows ? (double) again / rows : 0.0, rows ? (double) cand / rows : 0.0);
// the guard bites: keep the single-pass score of one block whose interval reaches the threshold, and the ids move
{
const int64_t n_bid = 1000, n_kv = n_bid * R, width = 2048 + R - 1;
std::vector<float> s3((size_t) n_bid + 1), s1((size_t) n_bid + 1), eps((size_t) n_bid + 1, 0.01f);
for (int64_t b = 0; b < n_bid; ++b) s3[b] = 1.0f + 0.001f * (float) b; // distinct, rising
s3[n_bid] = 0.0f;
eps[n_bid] = 0.0f;
const std::vector<int32_t> ref = topk(s3, n_kv, n_bid, width);
// the 513th block from the top (block 487) sits at the threshold; its single-pass score 0.009 lower
for (int64_t b = 0; b <= n_bid; ++b) s1[b] = s3[b];
s1[487] = s3[487] - 0.009f;
s1[486] = s3[486] + 0.009f;
const Band bd = band(s1, eps, n_kv, n_bid, width);
CHECK(bd.mode == 1 && std::find(bd.again.begin(), bd.again.end(), 487) != bd.again.end(),
"block 487 must be scored again (mode %d)", bd.mode);
std::vector<float> row = s1;
for (int64_t b : bd.again) row[b] = s3[b];
CHECK(topk(row, n_kv, n_bid, width) == ref, "the guarded row must select today's ids");
CHECK(topk(s1, n_kv, n_bid, width) != ref, "without the re-score the ids must differ (the test bites)");
}
}
} // namespace
int main() {
test_tf32();
test_bound();
test_rule();
if (g_fail) {
std::fprintf(stderr, "FAILED: %d checks\n", g_fail);
return 1;
}
std::puts("PASS: qsa_two_pass_test (TF32 split, the bound under two MMA models, the cut-off rule)");
return 0;
}