1use 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 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 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 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 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}