Skip to main content

patch_bench/
patch_bench.rs

1//! Paired compiler qualification on resident attention, GEMM and convolution kernels.
2//! Usage: patch_bench BASELINE_LIB CANDIDATE_LIB [pairs=30] [default|cu|wgp]
3use hrx::{
4    Buffer, Constants, GraphExec, Result, Stream,
5    loom::{Compiler, CompilerOptions, ProcessorMode, ReportMode, Specialization},
6};
7use std::{path::Path, time::Instant};
8
9fn half(value: f32) -> u16 {
10    let bits = value.to_bits();
11    let sign = ((bits >> 16) & 0x8000) as u16;
12    let exponent = ((bits >> 23) & 255) as i32 - 127 + 15;
13    if exponent <= 0 {
14        return sign;
15    }
16    assert!(exponent < 31, "fixture value overflows fp16");
17    let mantissa = bits & 0x7fffff;
18    let rounded = mantissa + 0xfff + ((mantissa >> 13) & 1);
19    sign | (((exponent as u32) << 10) + (rounded >> 13)) as u16
20}
21fn float(value: u16) -> f64 {
22    let sign = if value & 0x8000 == 0 { 1.0 } else { -1.0 };
23    let exponent = (value >> 10) & 31;
24    let mantissa = f64::from(value & 1023);
25    match exponent {
26        0 => sign * mantissa * 2f64.powi(-24),
27        31 => f64::NAN,
28        _ => sign * (1.0 + mantissa / 1024.0) * 2f64.powi(i32::from(exponent) - 15),
29    }
30}
31fn bf16(value: f32) -> f32 {
32    let bits = value.to_bits();
33    f32::from_bits((bits + 0x7fff + ((bits >> 16) & 1)) & 0xffff0000)
34}
35fn halves(values: impl IntoIterator<Item = f32>) -> Vec<u8> {
36    values
37        .into_iter()
38        .flat_map(|value| half(value).to_le_bytes())
39        .collect()
40}
41fn floats(values: impl IntoIterator<Item = f32>) -> Vec<u8> {
42    values.into_iter().flat_map(f32::to_le_bytes).collect()
43}
44struct Case {
45    name: String,
46    source: &'static str,
47    spec: Specialization,
48    grid: [u32; 3],
49    block: [u32; 3],
50    indices: Vec<u32>,
51    data: Vec<Vec<u8>>,
52    output: usize,
53    expected: Vec<f64>,
54    tolerance: f64,
55}
56fn attention(tokens: usize, repack: bool) -> Case {
57    let capacity = tokens + 32;
58    let symbol = if repack {
59        "krea2_attention_query32"
60    } else {
61        "krea2_attention_gqa_lds_f16_wmma"
62    };
63    let prefix = if repack {
64        "krea2.attention_query32"
65    } else {
66        "krea2.attention_gqa_lds_f16_wmma"
67    };
68    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
69    for (key, value) in [
70        ("q_stride", 512),
71        ("kv_stride", 128),
72        ("out_stride", 512),
73        ("tokens", tokens),
74        ("token_capacity", capacity),
75    ] {
76        spec.set_config(format!("{prefix}.{key}"), value.to_string());
77    }
78    spec.set_config(format!("{prefix}.scale"), "0.08838834764831845");
79    let query = |row: usize, head: usize| ((row + head * 3) % 9) as f32 * 0.0625 - 0.25;
80    let key = |row: usize| (row % 7) as f32 * 0.0625 - 0.1875;
81    let value = |row: usize, channel: usize| {
82        (row % 7) as f32 * 0.125 - 0.375 + (channel % 8) as f32 * 0.0625
83    };
84    let q = halves((0..capacity * 512).map(|i| {
85        if i / 512 < tokens {
86            query(i / 512, (i % 512) / 128)
87        } else {
88            0.0
89        }
90    }));
91    let k = halves((0..capacity * 128).map(|i| if i / 128 < tokens { key(i / 128) } else { 0.0 }));
92    let v = halves((0..capacity * 128).map(|i| {
93        let (row, channel) = if repack {
94            (i % capacity, i / capacity)
95        } else {
96            (i / 128, i % 128)
97        };
98        if row < tokens {
99            value(row, channel)
100        } else {
101            0.0
102        }
103    }));
104    // Independent f64 softmax oracle. Inputs are constant across the 128 Q/K
105    // channels, so their exact dot product is 128 times the scalar product.
106    let mut expected = Vec::with_capacity(tokens * 512);
107    for row in 0..tokens {
108        for head in 0..4 {
109            let mut numerator = 0.0;
110            let mut denominator = 0.0;
111            for token in 0..tokens {
112                let score = (128.0 * f64::from(query(row, head)) * f64::from(key(token))
113                    / 128f64.sqrt())
114                .exp();
115                numerator += score * f64::from(value(token, 0));
116                denominator += score;
117            }
118            expected.extend(
119                (0..128).map(|channel| numerator / denominator + (channel % 8) as f64 * 0.0625),
120            );
121        }
122    }
123    Case {
124        name: format!("{symbol}-{tokens}"),
125        source: if repack {
126            include_str!("../native/qualification/attention_query32.loom")
127        } else {
128            include_str!("../native/qualification/attention_gqa_lds_f16_wmma.loom")
129        },
130        spec,
131        grid: [(tokens / if repack { 32 } else { 16 }) as u32, 1, 1],
132        block: [if repack { 256 } else { 128 }, 1, 1],
133        indices: vec![tokens as u32],
134        data: vec![q, k, v, vec![0; tokens * 512 * 2]],
135        output: 3,
136        expected,
137        tolerance: 0.001,
138    }
139}
140fn gemm(large: bool, integer: bool) -> Case {
141    let (m, n, k) = if large {
142        (256usize, 1024usize, 1024usize)
143    } else {
144        (256, 512, 256)
145    };
146    let symbol = if integer {
147        "krea2_gemm_i8_256"
148    } else {
149        "h3_gemm_f16_fast_256b"
150    };
151    let prefix = if integer {
152        "krea2.gemm_i8_256"
153    } else {
154        "h3.gemm_f16_fast_256b"
155    };
156    let mut spec = Specialization::new(symbol).with_report(ReportMode::Summary);
157    for (name, value) in [
158        ("k_size", k),
159        ("n_size", n),
160        ("k_stride", k),
161        ("m_group", 1),
162    ] {
163        spec.set_config(format!("{prefix}.{name}"), value.to_string());
164    }
165    let a = |row: usize, col: usize| ((row * 3 + col * 5 + row * col) % 11) as i32 - 5;
166    let w = |row: usize, col: usize| ((row * 7 + col * 3 + row * col) % 13) as i32 - 6;
167    let bias = |col: usize| (col % 7) as f32 * 0.03125 - 0.09375;
168    // Evaluate the full dot product once per distinct row-pattern pair. Pattern
169    // repetition in the authored inputs is independent of the compiled program.
170    let mut products = [[0f32; 13]; 11];
171    for (row, values) in products.iter_mut().enumerate() {
172        for (col, product) in values.iter_mut().enumerate() {
173            let sum: i32 = (0..k).map(|i| a(row, i) * w(col, i)).sum();
174            *product = sum as f32 / 64.0;
175        }
176    }
177    let expected = (0..m * n)
178        .map(|i| {
179            let dot = products[(i / n) % 11][(i % n) % 13];
180            float(half(if integer {
181                bf16(dot)
182            } else {
183                dot + bias(i % n)
184            }))
185        })
186        .collect();
187    let data = if integer {
188        vec![
189            (0..m * k).map(|i| a(i / k, i % k) as i8 as u8).collect(),
190            (0..n * k).map(|i| w(i / k, i % k) as i8 as u8).collect(),
191            floats(vec![0.125; n]),
192            floats(vec![0.125; m]),
193            vec![0; m * n * 2],
194        ]
195    } else {
196        vec![
197            halves((0..m * k).map(|i| a(i / k, i % k) as f32 * 0.125)),
198            halves((0..n * k).map(|i| w(i / k, i % k) as f32 * 0.125)),
199            vec![0; m * n * 2],
200            floats((0..n).map(bias)),
201        ]
202    };
203    Case {
204        name: format!("{symbol}-{m}x{n}x{k}"),
205        source: if integer {
206            include_str!("../native/qualification/gemm_i8_256.loom")
207        } else {
208            include_str!("../native/qualification/gemm_f16_fast_256b.loom")
209        },
210        spec,
211        grid: [
212            (n / if integer { 128 } else { 256 }) as u32,
213            (m / if integer { 256 } else { 128 }) as u32,
214            1,
215        ],
216        block: [256, 1, 1],
217        indices: vec![m as u32],
218        data,
219        output: if integer { 4 } else { 2 },
220        expected,
221        // Permit subnormal residuals from the floating-point matrix accumulator.
222        // Integer GEMM remains exact; the absolute floor is one fp16 minimum normal.
223        tolerance: if integer { 0.0 } else { 2f64.powi(-14) },
224    }
225}
226fn conv(height: usize, cin: usize, n: usize) -> Case {
227    let symbol = "arcface_conv3x3_f16_wmma_bnprelu";
228    let mut spec = Specialization::new(symbol).with_report(ReportMode::Details);
229    for (key, value) in [
230        ("height", height),
231        ("width", height),
232        ("stride", 1),
233        ("cin_pad", cin),
234        ("cin_stride", cin),
235        ("k_size", 9 * cin),
236        ("n_size", n),
237    ] {
238        spec.set_config(
239            format!("arcface.conv3x3_f16_wmma_bnprelu.{key}"),
240            value.to_string(),
241        );
242    }
243    let m = height * height;
244    // Constant power-of-two operands: exact mathematical convolution, including
245    // zero padding at borders. Bias is zero and PReLU slope is one.
246    let expected = (0..m)
247        .flat_map(|p| {
248            let y = p / height;
249            let x = p % height;
250            let rows = if y == 0 || y + 1 == height { 2 } else { 3 };
251            let cols = if x == 0 || x + 1 == height { 2 } else { 3 };
252            std::iter::repeat_n((rows * cols * cin) as f64 / 64.0, n)
253        })
254        .collect();
255    Case {
256        name: format!("arcface_conv-{height}x{height}x{cin}x{n}"),
257        source: include_str!("../native/qualification/arcface_conv3x3_f16_wmma_bnprelu.loom"),
258        spec,
259        grid: [(n / 64) as u32, m.div_ceil(64) as u32, 1],
260        block: [256, 1, 1],
261        indices: vec![m as u32],
262        data: vec![
263            halves(std::iter::repeat_n(0.125, m * cin)),
264            halves(std::iter::repeat_n(0.125, n * 9 * cin)),
265            floats(std::iter::repeat_n(0.0, 9 * n)),
266            vec![0; m * n * 2],
267            floats(std::iter::repeat_n(1.0, n)),
268        ],
269        output: 3,
270        expected,
271        tolerance: 0.0,
272    }
273}
274struct Arm {
275    graph: GraphExec,
276    buffers: std::sync::Arc<Vec<Buffer>>,
277    identity: serde_json::Value,
278}
279fn prepare(
280    stream: &mut Stream,
281    compiler: &Compiler,
282    case: &Case,
283    shared: Option<std::sync::Arc<Vec<Buffer>>>,
284) -> Result<Arm> {
285    let artifact = compiler.module(case.source).compile(&case.spec)?;
286    let kernel = unsafe { stream.load_artifact(&artifact) }?;
287    let constants = Constants::indices(&kernel, &case.indices)?;
288    let buffers = if let Some(buffers) = shared {
289        buffers
290    } else {
291        std::sync::Arc::new(
292            case.data
293                .iter()
294                .map(|data| {
295                    let buffer = stream.allocate(data.len())?;
296                    stream.upload_blocking(buffer.binding(), data)?;
297                    Ok(buffer)
298                })
299                .collect::<Result<Vec<_>>>()?,
300        )
301    };
302    let bindings: Vec<_> = buffers.iter().map(Buffer::binding).collect();
303    let mut graph = stream.graph()?;
304    let mut prior = None;
305    for _ in 0..32 {
306        prior = Some(unsafe {
307            graph.dispatch(
308                prior.as_slice(),
309                &kernel,
310                case.grid,
311                case.block,
312                &constants,
313                &bindings,
314            )
315        }?);
316    }
317    let identity = serde_json::json!({"compiler":artifact.compiler_identity(), "artifact":hrx::bundle::digest(artifact.bytes()),
318        "report":artifact.report().map(|report| report.json())});
319    Ok(Arm {
320        graph: graph.finish()?,
321        buffers,
322        identity,
323    })
324}
325fn check(stream: &mut Stream, arm: &Arm, case: &Case) -> Result<f64> {
326    let mut bytes = vec![0; case.expected.len() * 2];
327    stream.read_blocking(arm.buffers[case.output].binding(), &mut bytes)?;
328    let mut maximum = 0f64;
329    for (index, (value, expected)) in bytes
330        .as_chunks::<2>()
331        .0
332        .iter()
333        .zip(&case.expected)
334        .enumerate()
335    {
336        let value = float(u16::from_le_bytes(*value));
337        let error = (value - expected).abs();
338        if !value.is_finite() || error > case.tolerance {
339            return Err(hrx::Error::Message(format!(
340                "{} output {index}: {value} vs {expected}, error {error}, limit {}",
341                case.name, case.tolerance
342            )));
343        }
344        maximum = maximum.max(error);
345    }
346    Ok(maximum)
347}
348fn main() -> Result<()> {
349    let args: Vec<_> = std::env::args().collect();
350    if args.len() < 3 || args.len() > 5 {
351        return Err(hrx::Error::Message(
352            "usage: patch_bench BASELINE_LIB CANDIDATE_LIB [pairs=30] [default|cu|wgp]".into(),
353        ));
354    }
355    let pairs = args
356        .get(3)
357        .map(|s| s.parse::<usize>())
358        .transpose()
359        .map_err(|e| hrx::Error::Message(e.to_string()))?
360        .unwrap_or(30);
361    if pairs < 30 {
362        return Err(hrx::Error::Message("at least 30 pairs are required".into()));
363    }
364    let mode = match args.get(4).map(String::as_str).unwrap_or("default") {
365        "default" => ProcessorMode::Default,
366        "cu" => ProcessorMode::ComputeUnit,
367        "wgp" => ProcessorMode::WorkgroupProcessor,
368        _ => {
369            return Err(hrx::Error::Message(
370                "mode must be default, cu or wgp".into(),
371            ));
372        }
373    };
374    let mut stream = Stream::open()?;
375    let mut options = CompilerOptions::for_target(stream.target());
376    options.processor_mode = mode;
377    let compilers = [
378        Compiler::with_options(Some(Path::new(&args[1])), options.clone())?,
379        Compiler::with_options(Some(Path::new(&args[2])), options)?,
380    ];
381    for case in [
382        attention(128, false),
383        attention(512, false),
384        gemm(false, false),
385        gemm(true, false),
386        gemm(false, true),
387        gemm(true, true),
388        conv(56, 64, 64),
389        conv(14, 256, 256),
390        conv(7, 512, 512),
391    ] {
392        let baseline = prepare(&mut stream, &compilers[0], &case, None)?;
393        let candidate = prepare(
394            &mut stream,
395            &compilers[1],
396            &case,
397            Some(baseline.buffers.clone()),
398        )?;
399        let mut arms = [baseline, candidate];
400        for arm in &mut arms {
401            for _ in 0..5 {
402                stream.launch(&mut arm.graph)?;
403                stream.synchronize()?;
404            }
405            check(&mut stream, arm, &case)?;
406        }
407        let errors = [
408            check(&mut stream, &arms[0], &case)?,
409            check(&mut stream, &arms[1], &case)?,
410        ];
411        println!(
412            "{}",
413            serde_json::json!({"case":case.name,"mode":format!("{mode:?}"),"baseline":arms[0].identity,"candidate":arms[1].identity,"max_absolute_error":errors,"tolerance":case.tolerance,"replays":32})
414        );
415        for pair in 0..pairs {
416            let mut ns = [0.0; 2];
417            for index in if pair % 2 == 0 { [0, 1] } else { [1, 0] } {
418                let start = Instant::now();
419                stream.launch(&mut arms[index].graph)?;
420                stream.synchronize()?;
421                ns[index] = start.elapsed().as_secs_f64() * 1e9 / 32.0;
422            }
423            println!(
424                "{}",
425                serde_json::json!({"case":case.name,"pair":pair,"baseline_ns":ns[0],"candidate_ns":ns[1]})
426            );
427        }
428        for arm in &mut arms {
429            stream.launch(&mut arm.graph)?;
430            stream.synchronize()?;
431            check(&mut stream, arm, &case)?;
432        }
433    }
434    Ok(())
435}