---
title: Matrix Two-Pass Forward Scan Algorithm
url: https://www.emergentmind.com/topics/matrix-two-pass-forward-scan-algorithm
type: topic
---

# Matrix Two-Pass Forward Scan Algorithm

“Matrix Two-Pass Forward Scan Algorithm” is an *Editor’s term* for a class of scan methods in which prefix-sum computation is reformulated as matrix multiplication on fixed-size tiles and then extended to larger inputs by a second forward propagation stage over tile, block, or grid summaries. In the relevant literature, the phrase is not used as a formal algorithm title. Its closest explicit realizations are the tensor-core scan formulation in “Accelerating Reduction and Scan Using Tensor Core Units” [1811.09736], the Ascend cube-unit scan schemes in “Parallel Scan on Ascend AI Accelerators” [2505.15112], and the segmented-scan construction in “Segmented Operations using Matrix Multiplications” [2506.23906]. Across these works, the common structure is a local matrix-based inclusive scan followed by a forward carry, correction, or fix-up stage, which is the conventional substance of a two-pass or multi-pass forward scan.

## 1. Definition and scope

The underlying problem is the classic prefix-sum problem. One formulation defines scan on a vector \(A = [a_1, a_2, \ldots, a_n]\) as
\[
[a_1,\; a_1+a_2,\; \ldots,\; \sum_{i=1}^n a_i].
\]
This is an inclusive scan. In segmented form, the input consists of a value vector \(\mathbf{x}\) and a boolean flag vector \(\mathbf{f}\), where \(f(i)=1\) means that position \(i\) starts a segment and \(f(0)=1\). The segmented inclusive scan is defined by
\[
y(i)=
\begin{cases}
x(i), & \text{if } i=0 \text{ or } f(i)=1,\\
y(i-1)+x(i), & \text{otherwise}.
\end{cases}
\]
In both cases, “forward” refers to the ordinary left-to-right dependency structure: each output depends on all prior inputs in the linear order [1811.09736] [2506.23906].

The phrase “matrix two-pass forward scan” most naturally refers to prefix-sum rather than to a literal two-dimensional raster scan. In the tensor-core and Ascend papers, the scan is a one-dimensional inclusive prefix sum over a linear array, but the array is packed into square matrices so that local prefixes can be computed by matrix multiplications. At larger scopes, the algorithm becomes hierarchical: local scans are computed first, partial totals are scanned second, and the resulting offsets are propagated back into local results. This suggests that the term denotes a family of matrix-engine scan schemes rather than a single standardized algorithm [1811.09736] [2505.15112].

A distinct usage appears in connected component labeling, where a two-pass forward scan over a 2-D binary image assigns provisional labels on a first raster pass and canonical labels on a second pass. That usage is domain-specific and tied to 8-connectivity, union-find equivalence maintenance, and final relabeling, rather than to the prefix-sum primitive [1606.05973].

## 2. Matrix encoding of forward scan on a tile

The most explicit matrix formulation appears in the NVIDIA tensor-core work. For exposition it assumes a \(16\times16\) tile, so a vector \(V\) of 256 elements is stored in a matrix \(A\) by
\[
a_{i,j} = V \left[ 16(j-1) + i \right].
\]
Two structured matrices then encode the scan. The upper-triangular matrix \(U\), with ones on and above the diagonal, computes row-wise inclusive scans:
\[
RowScan(A) = A \cdot U.
\]
The strict lower-triangular matrix \(L\), with ones below the diagonal and zeros on the diagonal, computes an exclusive column scan:
\[
ExclusiveColumnScan(A) = L \cdot A.
\]
From this, the inter-row carry matrix \(G\) is formed by reducing the rows of \(L\cdot A\) and replicating the result across columns:
\[
G_{j,i} = \sum_{k=1}^{j-1} \sum_{i=1}^{16} A_{k,i}.
\]
The complete tile scan is then
\[
Scan(V) = L \cdot A \cdot \underline{\mathbf{1}} + A \cdot U = G + A \cdot U,
\]
where \(\underline{\mathbf{1}}\) is the all-ones matrix. Read back in linear order, this yields the inclusive prefix sums of the original 256-element vector [1811.09736].

This decomposition separates two dependency types. Right multiplication by \(U\) handles intra-row forward dependencies, while left multiplication by \(L\), followed by multiplication with the all-ones matrix, handles inter-row carry propagation. Exclusive scan appears internally even though the externally visible result is inclusive segmented scan. The same conceptual structure reappears in the Ascend work, which defines \(U_s\) as the upper-triangular all-ones matrix, \(L_s^{-}\) as the strictly lower-triangular all-ones matrix, and \(1_s\) as the all-ones square matrix, and writes
\[
scan(z) = A_s\ @\ U_s + L^{-}_s\ @\ A_s\ @\ 1_s.
\]
Its implementation decomposes this identity as
\[
C_1 = A_s\ @\ 1_s,
\]
\[
C_2 = A_s\ @\ U_s,
\]
\[
C_2 = C_2 + L_s^{-}\ @\ C_1.
\]
Here \(A_s\) is the row-major \(s\times s\) view of a tile \(z\) of length \(s^2\), padded with zeroes if necessary [2505.15112].

## 3. Two-pass structure across warp, block, and grid levels

At the warp level, the tensor-core formulation is a local matrix realization of scan rather than a canonical two-pass algorithm. For \(Scan_{256N}\), the pseudocode is
\[
AU \gets A \cdot U + S,
\]
\[
LA \gets L \cdot A + \underline{\mathbf{0}},
\]
\[
R \gets LA \cdot \underline{\mathbf{1}} + AU,
\]
\[
S \gets Broadcast(R[255]).
\]
For a single 256-element tile, this is a three-matrix-multiply tile scan. For multiple tiles in one warp, it becomes a forward streaming scan over 256-element chunks: each tile consumes the prefix total from the previous tile via the broadcast matrix \(S\) and emits a new carry from the last output element \(R[255]\) [1811.09736].

The block level is the clearest instance of a matrix two-pass forward scan in the conventional hierarchical sense. The tensor-core paper states that the algorithm “first computes the segmented scan using the warp primitives … stores the reduced values into a partials list … performs a scan on the partial list … and adds the values to the intermediate results to get the output.” The pseudocode gathers the last-row elements of per-warp tiles into a matrix
\[
E \gets LoadTile(sout[240 \ldots 4096], stride=256),
\]
scans those totals by
\[
prtls \gets LastColumnScan_{16}(E),
\]
and applies the fix-up by
\[
val \gets sout[256\cdot warpIdx + it] + prtls[warpIdx].
\]
This is the classic decomposition into local scan, scan of block totals, and uniform add or fix-up [1811.09736].

The grid level is explicitly multi-pass. The same paper describes a device-wide “scan-then-propagate strategy” with three kernel calls: a first kernel computing segmented scans and block partials, a second kernel scanning the partial values, and a third kernel uniformly adding the scanned block offsets to corresponding segments. The Ascend paper presents an analogous hierarchy in `MCScan`, which “consists of two phases separated by a global synchronization barrier across all cores (blocks).” In Phase I, cube units compute tile-local scans and vector units compute block reductions. In Phase II, vector units compute or use scanned block totals and propagate the carries through the local tile scans. The paper states that `MCScan` is similar to the Scan-Scan-Add paradigm, but differs in that it performs partial re-computation of block-level reductions during its first phase [2505.15112].

## 4. Segmented scan and speculative correction

The MMV-RAM paper generalizes the matrix-scan idea into a theoretical framework with a scalar unit, a vector computing unit, and a matrix multiplication unit. Its segmented scan algorithm is not named as a “two-pass forward scan,” but each recursion level has a structure that is naturally interpreted as a speculative local forward pass followed by a correction or propagation pass. The main algorithm `SegScan` is composed of `BlockSegScan`, `Recurse`, and `UpdateFirstSegment` [2506.23906].

`BlockSegScan` begins with speculative unsegmented scans inside each block of size \(s\):
\[
\bar{x} \gets \mathrm{matmul}(x, U_s),
\]
\[
\bar{f} \gets \mathrm{matmul}(f, U_s),
\]
\[
x \gets \texttt{revertspecs}(\bar{x},\bar{f};s).
\]
The matrix multiplication with \(U_s\) computes blockwise inclusive scans of values and of flags, while `revertspecs` removes contributions that crossed true segment boundaries. The recursive stage then contracts the problem by gathering block-end values and block-level flag summaries, recursively segmented-scans those summaries, scatters the resulting carries back, and invokes `UpdateFirstSegment` so that only the first segment of each block receives inter-block correction. This suggests a two-pass forward interpretation at each level: first compute speculative local scans and summaries, then propagate recursively derived prefix information back into the blocks [2506.23906].

The paper gives both asymptotic upper and lower bounds. Its segmented scan runs in
\[
O(\log_s(n))
\]
steps, with work
\[
O\left(M\!\left(\frac{n}{s}\right)+nB\left(s+\frac{B}{s}\right)\right),
\]
and it proves that any polynomial-work algorithm that uses only the vector unit requires
\[
\Omega\left(\frac{\log_2(n)}{\log_2\log_2(n)}\right)
\]
steps. In this framing, matrix multiplication is not merely an implementation trick; it changes the depth complexity of segmented scan under the MMV-RAM model [2506.23906].

## 5. Hardware mapping and implementation constraints

The tensor-core realization is tightly coupled to the NVIDIA WMMA programming model. NVIDIA Tensor Cores on V100 expose a warp-level API in which each Tensor Core performs
\[
D = A\cdot B + C
\]
on \(4\times4\times4\) internal arrays, while programmer-visible WMMA shapes are \(\langle 16,16,16\rangle\), \(\langle 32,8,16\rangle\), and \(\langle 8,32,16\rangle\). The scan paper uses the \(16\times16\times16\) shape. Inputs \(A\) and \(B\) must be half precision, while accumulators \(C\) and outputs \(D\) can be half or single precision. One warp cooperatively performs one WMMA operation on one \(16\times16\) tile; a thread block contains multiple warps; shared memory holds intermediate tile outputs and per-warp partial totals; and grid-level scan is orchestrated by multiple kernels [1811.09736].

The implementation is strongly shaped by memory-layout and API constraints. Tiles are loaded into WMMA fragments using `LoadTile`, structured matrices \(U\), \(L\), and \(\underline{\mathbf{1}}\) must be materialized as fragments, and block-level fix-up requires gathering the last row of each tile in shared memory by a strided load into the matrix \(E\). The paper also lists three practical API limitations: loads and stores only at fragment granularity; fragments can only be loaded from or stored to global or shared memory, not constant memory; and `matrix_a`, `matrix_b`, and `accumulator` fragments have different opaque internal layouts, with no cast API. The authors mitigate these constraints by exploiting knowledge of fragment layout and by constructing matrices such as the upper-triangular matrix directly in fragment registers [1811.09736].

The Ascend implementation is similarly hardware-specific, though with different units and memory hierarchy. It uses the cube unit for matrix multiplication, the vector unit for vector operations, and scratchpads `L0A`, `L0B`, `L0C`, `L1`, and `UB`. In the single-core algorithms `ScanU` and `ScanUL1`, the cube and vector units are pipelined: the cube unit computes local tile scans, and the vector unit applies the running carry `partial`. In `MCScan`, a global synchronization point `SyncAll` separates the local-scan phase from the propagation phase. Experimental settings include \(s \in \{32,64,128\}\), with the paper noting that larger \(s\) improves performance and that \(s=128\) maximizes utilization of the level-0 scratchpad memories of the cube unit [2505.15112].

## 6. Performance regimes, applications, and limitations

The tensor-core paper reports that its scan algorithm achieves \(89\%-98\%\) of peak memory copy bandwidth, is up to \(3\times\) faster than state-of-the-art methods for small segment sizes, and decreases power consumption by up to \(16\%\) for scan. For segmented scan specifically, it reports a theoretical peak of \(225\) billion half-precision elements per second and measured throughput of \(89\%-97\%\) of ideal throughput for segment sizes between 16 and \(2^{19}\). Compared with Thrust’s `inclusive_scan_by_key`, the TCU implementation is up to \(3\times\) faster for small segment sizes; for full scan, the unoptimized grid-level implementation is comparable to CUB and faster than Thrust [1811.09736].

The Ascend paper reports single-core speedups ranging from \(5\times\) to \(9.6\times\) compared to vector-only implementations for sufficiently large input lengths, a multi-core scan reaching up to \(37.5\%\) of theoretical memory bandwidth, and a radix sort offering up to \(3.3\times\) speedup over the baseline. It also uses scan as a building block for sorting, tensor masking, top-\(k\), top-\(p\) sampling, weighted sampling, split, and compress. For top-\(p\), it states that the algorithm executes 17 scans for each batch: 16 scans for radix sort and one additional scan required by the algorithm [2505.15112].

The strongest performance regime is small regular segment sizes and tile sizes that match the native matrix engine. The tensor-core paper states that regular segmented scan is the natural fit, and that irregular segmented scan would need padding or masking. It also notes precision limitations, since Tensor Core inputs are half precision, and potential underutilization for very large segment sizes if only a few blocks are launched. The Ascend work similarly assumes row-major \(s\times s\) tiles and zero padding of incomplete final tiles. More generally, these algorithms are formulated for summation, not for an arbitrary monoid interface; conceptually they rely on associativity, but operationally they are implemented as floating-point or integer sums supported by the matrix engines [1811.09736] [2505.15112] [2506.23906].

Source: https://www.emergentmind.com/topics/matrix-two-pass-forward-scan-algorithm