1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//! Attention pooling over EVERY feature, zeros included — the window-free
//! masked encoder's pool.
//!
//! [`super::scatter_pool`] re-associates the same three sums over a cell's `K`
//! context slots. Once nothing scales with `K · H`, the slots stop earning
//! their keep: feeding all `D` features costs one `[N, D]` product either way,
//! so the context window — and the shortlist that had to choose it — goes.
//!
//! ```text
//! rq_d = ρ q [D] one matvec per step
//! s_nd = a_nd · rq_d / √H [N, D] no gather at all
//! attn_nd = softmax_d(s_nd + mask) [N, D] over the whole gene axis
//! pool_nh = (attn_nd · a_nd) ρ [N, H] one gemm
//! ```
//!
//! This is NOT the same model as the indexed path. A zero-count gene carries a
//! below-mean gate rather than no gate at all, and it takes part in the
//! softmax, so the encoder reads what a cell does not express as well as what
//! it does — the set the decoder was already scoring. With the visible mask
//! hiding everything outside a cell's support the two pools agree exactly,
//! which is what ties this module to [`super::scatter_pool`].
use crateFeatureEmbedding;
use ;
/// `s_nd = gate · rq · scale`, with `-1e9` added where `visible == 0`; no mask
/// when `visible_nd` is `None` (every gene visible).
///
/// `rq_d` is `[D]` (see [`super::scatter_pool::query_over_features`]), broadcast
/// across the rows; `scale` is the caller's `1/√H`, applied to the `[D]` vector
/// rather than to the `[N, D]` product.
/// `pool_nh = (attn · gate) ρ`, through the feature side.
///
/// [`super::scatter_pool::pool_by_scatter`]'s scatter has nothing left to do
/// here: a gene appears once per row, so the `[N, D]` weight matrix IS the
/// product `attn · gate`.