|
| 1 | +# Micro-benchmark: fused quantized_matmul (mx::quantized_matmul) vs the |
| 2 | +# current quantized_dense path (dequantize_defn + Nx.dot), on GPU. |
| 3 | +# No Bumblebee / model download needed. Single-token (batch=1) decode-shaped. |
| 4 | +# |
| 5 | +# mix run bench/qmm_microbench.exs |
| 6 | +alias Emily.Quantization |
| 7 | +alias Emily.Quantization.Layers |
| 8 | +alias Emily.QuantizedWeight |
| 9 | + |
| 10 | +Nx.default_backend(Emily.Backend) |
| 11 | + |
| 12 | +native = [compiler: Emily.Compiler, native: true] |
| 13 | + |
| 14 | +dtype = :bf16 |
| 15 | +group_size = 64 |
| 16 | +bits = 4 |
| 17 | +warmup = 100 |
| 18 | +iters = 2000 |
| 19 | + |
| 20 | +# Qwen3-0.6B-shaped projections. Weight is [out, in] (transpose: true, the |
| 21 | +# from_dense default); activation is [1, in] (one decode token). |
| 22 | +shapes = [ |
| 23 | + {"q_proj [2048,1024]", 2048, 1024}, |
| 24 | + {"kv_proj [1024,1024]", 1024, 1024}, |
| 25 | + {"o_proj [1024,2048]", 1024, 2048}, |
| 26 | + {"mlp_up [3072,1024]", 3072, 1024}, |
| 27 | + {"mlp_dn [1024,3072]", 1024, 3072} |
| 28 | +] |
| 29 | + |
| 30 | +# Force a full worker sync on the result (native :sync already blocks on |
| 31 | +# mx::eval, but realizing the bytes is belt-and-suspenders). |
| 32 | +sync = fn t -> Nx.to_binary(t) end |
| 33 | + |
| 34 | +time_fn = fn compiled, x, qw -> |
| 35 | + Enum.each(1..warmup, fn _ -> compiled.(x, qw) end) |
| 36 | + sync.(compiled.(x, qw)) |
| 37 | + t0 = System.monotonic_time(:microsecond) |
| 38 | + Enum.each(1..iters, fn _ -> compiled.(x, qw) end) |
| 39 | + sync.(compiled.(x, qw)) |
| 40 | + t1 = System.monotonic_time(:microsecond) |
| 41 | + iters / ((t1 - t0) / 1_000_000) |
| 42 | +end |
| 43 | + |
| 44 | +IO.puts("dtype=#{dtype} group_size=#{group_size} bits=#{bits} warmup=#{warmup} iters=#{iters}\n") |
| 45 | +IO.puts(" before = old quantized_dense (dequantize_defn + Nx.dot)") |
| 46 | +IO.puts(" after = quantized_dense now (#197: fused mx::quantized_matmul)\n") |
| 47 | + |
| 48 | +IO.puts(String.pad_trailing("shape", 22) <> " before(it/s) after(it/s) speedup maxΔ") |
| 49 | +IO.puts(String.duplicate("-", 68)) |
| 50 | + |
| 51 | +for {label, out_f, in_f} <- shapes do |
| 52 | + {w, _} = Nx.Random.normal(Nx.Random.key(0), shape: {out_f, in_f}, type: dtype) |
| 53 | + qw = QuantizedWeight.from_dense(w, group_size: group_size, bits: bits) |
| 54 | + {x, _} = Nx.Random.normal(Nx.Random.key(1), shape: {1, in_f}, type: dtype) |
| 55 | + |
| 56 | + # Pass the QuantizedWeight (an Nx.Container) as a jit ARGUMENT, not a |
| 57 | + # closure — its tensors become Expr params and its keep-metadata |
| 58 | + # (group_size/bits/transpose/mode) stays available at trace time. This |
| 59 | + # mirrors how Bumblebee threads quantized model params into the forward. |
| 60 | + # |
| 61 | + # `before` = the old layer body (dequantize the full bf16 weight, then |
| 62 | + # dense Nx.dot). `after` = the shipped layer, which now lowers to the |
| 63 | + # fused mx::quantized_matmul kernel. |
| 64 | + before = fn x, qw -> Nx.dot(x, Nx.transpose(Quantization.dequantize_defn(qw))) end |
| 65 | + after_fn = fn x, qw -> Layers.quantized_dense(x, qw) end |
| 66 | + |
| 67 | + before_compiled = Nx.Defn.jit(before, native) |
| 68 | + after_compiled = Nx.Defn.jit(after_fn, native) |
| 69 | + |
| 70 | + # correctness: after (fused) vs before (dequant), same math up to fp reorder |
| 71 | + b = before_compiled.(x, qw) |
| 72 | + a = after_compiled.(x, qw) |
| 73 | + max_delta = Nx.subtract(b, a) |> Nx.abs() |> Nx.reduce_max() |> Nx.to_number() |
| 74 | + |
| 75 | + before_rate = time_fn.(before_compiled, x, qw) |
| 76 | + after_rate = time_fn.(after_compiled, x, qw) |
| 77 | + |
| 78 | + IO.puts( |
| 79 | + String.pad_trailing(label, 22) <> |
| 80 | + " " <> |
| 81 | + String.pad_trailing(:erlang.float_to_binary(before_rate, decimals: 0), 12) <> |
| 82 | + " " <> |
| 83 | + String.pad_trailing(:erlang.float_to_binary(after_rate, decimals: 0), 11) <> |
| 84 | + " " <> |
| 85 | + String.pad_trailing( |
| 86 | + :erlang.float_to_binary(after_rate / before_rate, decimals: 2) <> "x", |
| 87 | + 7 |
| 88 | + ) <> |
| 89 | + " " <> :erlang.float_to_binary(max_delta, decimals: 4) |
| 90 | + ) |
| 91 | +end |
0 commit comments