t4a_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 even within one call.
90 let seed = I::cast_from(n_iter);
91 let mut line = Vector::<I, N>::empty();
92 let lanes = line.vector_size();
93 #[unroll]
94 for lane in 0..lanes {
95 line.insert(lane, seed + I::cast_from(lane));
96 }
97
98 // Each pass writes the *next* window of the buffer, not the same one
99 // again. A window written repeatedly would stay resident in cache after
100 // the first pass, and every working set below the cache would report
101 // cache bandwidth instead of what a kernel of that size moves; coming
102 // back to a window only after a whole buffer of traffic keeps it cold.
103 //
104 // It also keeps the addresses moving. A window small enough that every
105 // thread writes a single line would otherwise be loop-invariant, and the
106 // compiler is free to sink such a store out of the loop and perform it
107 // once, leaving the probe reporting a bandwidth the hardware never moved.
108 let mut start = 0;
109
110 for _ in 0..n_iter {
111 for step in 0..steps {
112 // Coalesced spreads one step's addresses across adjacent threads,
113 // which is only fast where those threads share a real plane. A
114 // CPU worker has no such neighbour, so it instead gets a run of
115 // `steps` lines entirely its own.
116 let base = if blocked {
117 ABSOLUTE_POS * steps + step
118 } else {
119 ABSOLUTE_POS + (step * stride)
120 };
121
122 if base < window {
123 let mut idx = start + base;
124 if idx >= len {
125 idx -= len;
126 }
127
128 output[idx] = line;
129 }
130 }
131
132 start += window;
133 if start >= len {
134 start -= len;
135 }
136 }
137}