open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-backend-apple/examples/mpp_gemm_bench.rs
Claude 1c10b364a5 jouleclaw-backend-apple: 84 clippy warnings -> 10
#414 left this crate as the reason the `-D warnings` gate covers
loader-gguf on default features only: clippy reports it through the path
dependency, so gating the `fabric-meter` configuration needs it clean too.

I described its warnings in #414 as "unused in THAT configuration and
needed in others, so they want cfg work rather than deletion". That was
wrong on two counts, and measuring it directly settled both:

- the crate has NO `[features]` at all, only a macOS-conditional
  dependency block, so there is one configuration and the unused items
  are unconditionally unused;
- there are 84 of them, not 43. The 43 was what leaked through
  loader-gguf's lint, not the crate's own count.

82 of the 84 are in mps_matmul.rs.

41 came from clippy's own suggestions (manual_is_multiple_of 23,
unnecessary_cast 8, needless_option_as_deref 3, excessive_precision,
doc_lazy_continuation, double_parens, chunks_exact_to_as_chunks).

The other 42 -- 27 `unused_unsafe` and 15 `unused_imports` -- carry no
machine-applicable suggestion at all. Neither `clippy --fix` nor
`cargo fix` touches them, with or without `--all-targets`. They are
plain code, not macro expansions, so they were fixed line-guided from
rustc's own line numbers:

- `|| unsafe { .. }` -> `|| { .. }` where objc2 0.6 made the call safe.
  Dropping a block rustc has already proved redundant cannot change
  behaviour.
- `use objc2_metal::{MTLComputePipelineState as _, MTLLibrary as _}` ->
  just the name the function needs. The unused one is a trait brought in
  as `_`; the type itself is referenced fully qualified on the lines
  above, which is why it reads as used at a glance.

Checked before deleting, because this is the shape that broke #372: each
unused import was matched against every other mention of the name in its
file and against the file's six `cfg(not(target_os = "macos"))` branches.
None appears in one, and objc2-metal is a macOS-only dependency, so no
other configuration can want them.

Verified: 0 errors, 37 backend-apple tests (32 pass, 5 ignored), and
loader-gguf still builds and its fabric-meter `process_calibration`
passes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-22 06:55:59 -04:00

118 lines
6.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! M5 GPU Neural-Accelerator GEMM via Metal-4 Metal-Performance-
//! Primitives `mpp::tensor_ops::matmul2d`, using the canonical
//! recipe from liuliu/example_matmul_metal4. WORKING: bf16 in,
//! f32 accum, standard C[M,N]=A[M,K]·B[K,N] (no transpose).
//!
//! The unblock was K = `dynamic_length_v<int>` in the descriptor
//! — NOT `dynamic_extent`/0, which silently caps the K-sum at
//! one ~32-wide tile (the bug that defeated every earlier
//! attempt). Plain `slice()` (origin shift), single `run()`
//! that loops K internally, `execution_simdgroups<4>`,
//! descriptor tile (64, 32). Constraints: M%64, N%32 (N must be
//! ≥128 — N=64 mis-tiles), K%32.
//!
//! VERIFIED CORRECT (max_abs 0.0) at all shapes except N=64.
//! On M5 Max: 24.6 TFLOP/s at 2048×2048×4096 vs the bf16
//! simdgroup path's ~4.8 TFLOP/s — ~5×, both bit-correct.
#[cfg(target_os = "macos")]
fn main() {
use std::time::Instant;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSString;
use objc2_metal::{
MTLBuffer, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue,
MTLComputeCommandEncoder, MTLComputePipelineState, MTLCreateSystemDefaultDevice,
MTLDevice, MTLLibrary, MTLResourceOptions, MTLSize,
};
fn f32_to_bf16(v: f32) -> u16 { let b = v.to_bits(); ((b + (((b >> 16) & 1) + 0x7fff)) >> 16) as u16 }
fn bf16_to_f32(h: u16) -> f32 { f32::from_bits((h as u32) << 16) }
const KERNEL: &str = r#"
#include <metal_stdlib>
#include <MetalPerformancePrimitives/MetalPerformancePrimitives.h>
using namespace metal;
using namespace mpp::tensor_ops;
kernel void gemm_bf16_mpp(device bfloat* A_buf [[buffer(0)]], // [K,M]
device bfloat* B_buf [[buffer(1)]], // [N,K]
device float* C_buf [[buffer(2)]], // [M,N]
constant uint& M [[buffer(3)]],
constant uint& N [[buffer(4)]],
constant uint& K [[buffer(5)]],
uint2 tgid [[threadgroup_position_in_grid]]) {
auto A = tensor<device bfloat, dextents<int32_t,2>, tensor_inline>(A_buf, dextents<int32_t,2>(K, M));
auto B = tensor<device bfloat, dextents<int32_t,2>, tensor_inline>(B_buf, dextents<int32_t,2>(N, K));
auto C = tensor<device float, dextents<int32_t,2>, tensor_inline>(C_buf, dextents<int32_t,2>(M, N));
constexpr auto desc = matmul2d_descriptor(64, 32, dynamic_length_v<int>, false, false, false,
matmul2d_descriptor::mode::multiply);
matmul2d<desc, execution_simdgroups<4>> mm;
auto mA = A.slice(0, tgid.y * 64);
auto mB = B.slice(tgid.x * 32, 0);
auto mC = C.slice(tgid.x * 32, tgid.y * 64);
mm.run(mA, mB, mC);
}
"#;
let device = MTLCreateSystemDefaultDevice().expect("no device");
println!("device: {}", device.name());
let queue = device.newCommandQueue().unwrap();
let lib = match device.newLibraryWithSource_options_error(&NSString::from_str(KERNEL), None) {
Ok(l) => l, Err(e) => { println!("COMPILE FAIL:\n{}", e.localizedDescription()); return; }
};
let func = lib.newFunctionWithName(&NSString::from_str("gemm_bf16_mpp")).unwrap();
let pipe: Retained<ProtocolObject<dyn MTLComputePipelineState>> =
device.newComputePipelineStateWithFunction_error(&func).unwrap();
// M%64==0, N%32==0, K%32==0.
let shapes = [(256usize, 256usize, 512usize), (512, 512, 2048), (1024, 1024, 4096), (2048, 2048, 4096)];
for &(m, n, k) in &shapes {
let a_bf: Vec<u16> = (0..k * m).map(|i| f32_to_bf16((((i % 17) as f32) - 8.0) * 0.05)).collect();
let b_bf: Vec<u16> = (0..n * k).map(|i| f32_to_bf16((((i % 13) as f32) - 6.0) * 0.04)).collect();
let mkb = |d: &[u16]| unsafe { device.newBufferWithBytes_length_options(
std::ptr::NonNull::new(d.as_ptr() as *mut _).unwrap(), std::mem::size_of_val(d),
MTLResourceOptions::StorageModeShared).unwrap() };
let buf_a = mkb(&a_bf);
let buf_b = mkb(&b_bf);
let buf_c = device.newBufferWithLength_options(m * n * 4, MTLResourceOptions::StorageModeShared).unwrap();
let cp = MTLBuffer::contents(&*buf_c).as_ptr() as *mut f32;
let dispatch = || unsafe {
std::ptr::write_bytes(cp, 0, m * n);
let cb = queue.commandBuffer().unwrap();
let enc = cb.computeCommandEncoder().unwrap();
enc.setComputePipelineState(&pipe);
enc.setBuffer_offset_atIndex(Some(&buf_a), 0, 0);
enc.setBuffer_offset_atIndex(Some(&buf_b), 0, 1);
enc.setBuffer_offset_atIndex(Some(&buf_c), 0, 2);
for (s, &v) in [m as u32, n as u32, k as u32].iter().enumerate() {
enc.setBytes_length_atIndex(std::ptr::NonNull::new(&v as *const u32 as *mut _).unwrap(), 4, 3 + s);
}
enc.dispatchThreadgroups_threadsPerThreadgroup(
MTLSize { width: n / 32, height: m / 64, depth: 1 },
MTLSize { width: 128, height: 1, depth: 1 });
enc.endEncoding();
cb.commit();
cb.waitUntilCompleted();
};
dispatch();
let c: Vec<f32> = (0..m * n).map(|i| unsafe { *cp.add(i) }).collect();
let mut max_abs = 0f32; let mut max_mag = 0f32; let mut wrong = 0usize;
// FULL-output check: every row, every column (not a corner).
for mm_ in 0..m { for nn in 0..n {
let mut acc = 0f32;
for l in 0..k { acc += bf16_to_f32(a_bf[mm_ * k + l]) * bf16_to_f32(b_bf[l * n + nn]); }
let d = (c[mm_ * n + nn] - acc).abs();
max_abs = max_abs.max(d);
max_mag = max_mag.max(acc.abs());
if d > 1e-2 { wrong += 1; }
}}
let iters = 50;
for _ in 0..5 { dispatch(); }
let t0 = Instant::now();
for _ in 0..iters { dispatch(); }
let ms = t0.elapsed().as_secs_f64() * 1e3 / iters as f64;
let gflop = 2.0 * m as f64 * n as f64 * k as f64 / 1e9;
println!(" m={m:<4} n={n:<4} k={k:<4} {ms:6.3} ms {:8.1} GFLOP/s max_abs={max_abs:.2e} WRONG={wrong}/{} (mag≈{max_mag:.2e})",
gflop / (ms / 1e3), m*n);
}
}
#[cfg(not(target_os = "macos"))]
fn main() {}