Skip to main content

cubecl_std/throughput/runners/
memory_write.rs

1use cubecl::prelude::*;
2use cubecl_core as cubecl;
3use cubecl_runtime::throughput::{KernelConfig, MemorySpec, ThroughputError, ThroughputKey};
4
5use crate::throughput::{
6    LaunchConfig,
7    memory_probe::{self, MemoryProbe},
8    verify::verify,
9};
10
11/// Builds the write-only streaming kernel, moving `working_set` bytes per
12/// pass, all of them written.
13///
14/// This is [`memory_direct`](super::memory_direct) with the load removed. The
15/// copy kernel moves a line in and a line back out, and counts both
16/// directions in `ops_count`, so what it reports is total traffic across the
17/// memory interface. That is the right ceiling for a kernel that also reads
18/// what it writes, and the wrong one for a kernel that only writes: an RNG
19/// fill, a memset, a broadcast. Those legitimately exceed the copy figure,
20/// because half of the copy's traffic is a direction they never use.
21///
22/// Reported `ops_count` is the write count alone.
23pub fn build_kernel(
24    client: &Client,
25    key: ThroughputKey,
26    config: LaunchConfig,
27    spec: MemorySpec,
28) -> Result<KernelConfig, ThroughputError> {
29    let client = client.clone();
30    let dtype = key.dtype();
31
32    let line_bytes = config.vector_size * dtype.size();
33    let probe = MemoryProbe::new(&client, config, line_bytes, spec);
34
35    let [out_handle] = memory_probe::reserve(&client, [probe.buffer_bytes])?;
36
37    let (verifier, written) = (client.clone(), out_handle.clone());
38    let sample = Box::new(move |iterations: usize| {
39        let start = cubecl_common::profile::Instant::now();
40        unsafe {
41            memory_write_throughput::launch_unchecked(
42                &client,
43                CubeCount::Static(probe.cube_count as u32, 1, 1),
44                config.cube_dim,
45                config.vector_size,
46                BufferArg::from_raw_parts(out_handle.clone(), probe.pool_lines),
47                probe.window_lines,
48                iterations,
49                probe.blocked,
50                dtype,
51            )
52        };
53        // A failure is not this sync's to report: `verify` asked the output.
54        let _ = cubecl_core::future::block_on(client.sync());
55        start.elapsed()
56    });
57    verify(&verifier, &sample, &written)?;
58
59    // Writes only, no `2 *`. That factor is the whole difference from the copy.
60    let ops_count = probe.window_lines * config.vector_size;
61
62    Ok(KernelConfig {
63        sample,
64        ops_count,
65        min_iterations: probe.min_iterations(),
66    })
67}
68
69#[cube(launch_unchecked)]
70pub fn memory_write_throughput<I: Numeric, N: Size>(
71    output: &mut [Vector<I, N>],
72    window: usize,
73    n_iter: usize,
74    #[comptime] blocked: bool,
75    #[define(I)] _dtype: ElemType,
76) {
77    let len = output.len();
78    let stride = CUBE_DIM as usize * CUBE_COUNT;
79
80    // From `window` alone rather than from `window - ABSOLUTE_POS`, which
81    // underflows for a thread past the end of a window smaller than the launch.
82    // High threads get one step too many and the bounds check drops it.
83    let steps = window.div_ceil(stride).max(1);
84
85    // Read once, write everywhere: the reverse of `memory_read`, which reads
86    // everywhere and writes once. `n_iter` is a scalar the kernel compiler
87    // sees only at launch time, so it cannot fold the stores into a single
88    // known-pattern fill, and the per-lane offset keeps the written line from
89    // being a uniform value within one line; each store then offsets it by its
90    // own index, so no two neighbouring lines repeat (see the store).
91    let seed = I::cast_from(n_iter);
92    let mut line = Vector::<I, N>::empty();
93    let lanes = line.vector_size();
94    #[unroll]
95    for lane in 0..lanes {
96        line.insert(lane, seed + I::cast_from(lane));
97    }
98
99    // Each pass writes the *next* window of the buffer, not the same one
100    // again. A window written repeatedly would stay resident in cache after
101    // the first pass, and every working set below the cache would report
102    // cache bandwidth instead of what a kernel of that size moves; coming
103    // back to a window only after a whole buffer of traffic keeps it cold.
104    //
105    // It also keeps the addresses moving. A window small enough that every
106    // thread writes a single line would otherwise be loop-invariant, and the
107    // compiler is free to sink such a store out of the loop and perform it
108    // once, leaving the probe reporting a bandwidth the hardware never moved.
109    let mut start = 0;
110    // The cubes each cube has moved along the launch, once per walk of the
111    // buffer, so a line comes back only after a whole buffer of traffic (see
112    // `memory_probe::moved_pos`).
113    let mut moved = 0usize;
114
115    for _ in 0..n_iter {
116        let pos = memory_probe::moved_pos(moved);
117        for step in 0..steps {
118            // Coalesced spreads one step's addresses across adjacent threads,
119            // which is only fast where those threads share a real plane. A
120            // CPU worker has no such neighbour, so it instead gets a run of
121            // `steps` lines entirely its own.
122            let base = if blocked {
123                ABSOLUTE_POS * steps + step
124            } else {
125                pos + (step * stride)
126            };
127
128            if base < window {
129                let mut idx = start + base;
130                if idx >= len {
131                    idx -= len;
132                }
133
134                // Every line holds its own value. Lines that all repeat one
135                // value do not all reach memory: an RTX PRO 6000 reported
136                // 3.8 TB/s writing them on a 1.8 TB/s bus, and 1.65 TB/s once
137                // each line differed. A multiplicative hash of the index
138                // spreads neighbouring lines apart at the cost of one integer
139                // multiply, which the store's latency hides.
140                let scrambled = u32::cast_from(idx) * 2654435761u32;
141                output[idx] = line + Vector::cast_from(I::cast_from(scrambled));
142            }
143        }
144
145        start += window;
146        if start >= len {
147            start -= len;
148            moved = memory_probe::moved_further(moved);
149        }
150    }
151}