Skip to content

Commit 0c9db70

Browse files
eaglstunclaude
andcommitted
Phase M4: matmul closeout -- sync tax measured + documented, offset-0 semantics verified, honest docs
Task 1 (per-call sync overhead): measured and decomposed, NOT optimized away -- torch exposes no safe handle to its MPS queue/stream, so the private-queue + sync design stays, now with the cost precisely documented (MPS_STATUS.md 11.3). - BNB_MPS_PROFILE=1 now decomposes each native matmul call into sched (commit -> GPU start), gpu (kernel), and done (GPU end -> wait return), on the mach_absolute_time timebase GPUStartTime uses. Steady-state fp16: the fixed tax is ~0.15ms/call (~0.02 encode + ~0.07 sched + ~0.07 done); the pre-call torch.mps.synchronize() itself is ~1us on an idle queue. That tax is ~52% of a 4096x4096 gemv call and ~10% of an M=512 gemm. - Queue sharing rejected with evidence: torch 2.12.1 exposes no stream handle from Python; libtorch's at::mps::getCurrentMPSStream()/MPSStream internals are reachable only via dlsym'd C++ mangled symbols + header-derived ivar offsets (commandQueue()/queue() are inline, commit()/flush() private, encodes must run on torch's private _serialQueue) -- an unguardable ABI trap. Sharing only the queue would not even remove the pre-sync (command buffers execute in commit order; torch batches into an uncommitted buffer). Future directions (libtorch-linked extension, torch.mps.compile_shader) recorded with their costs in 11.3. - New test_sync_discipline_interleave_stress pins the discipline: dependent torch writes interleaved with native matmuls on the same buffers, parity every iteration. Verified to have teeth: fails 30/30 with the pre-sync no-op'd, passes 100% with it. Task 2 (offset-0 input copy): verified on this build, clone stays. - A view's data_ptr() IS base + storage_offset*itemsize (raw arithmetic); objc-probing such an interior pointer SIGSEGVs uncatchably, so casting it would be silent corruption or a crash -- the clone in _ensure_native_buffer is load-bearing. - Better than NEXT_MATMUL_PLAN 5 feared: untyped_storage().data_ptr() DOES recover the base MTLBuffer (passes the contract check), so offset binding is possible; not implemented because steady-state matmul inputs are offset-0 (the clone ~never fires) and it would touch every ABI entry point for no measured benefit. Recipe recorded in 11.4; semantics pinned by test_view_data_ptr_is_base_plus_offset. Task 3 (docs): - MPS_STATUS.md: M2/M3 short notes consolidated into a proper 11 (fused gemv, native gemm, the M4 sync/offset findings); op matrix rows for gemv/gemm updated to native; header records the M4 harness result (328 passed + the known unrelated lion strict-xfail XPASS). - docs/apple_silicon/README.md: gemv_4bit -> native (fused, 3.4-6.2x); gemm_4bit -> native for fp16/fp32 with the honest caveats (bf16 falls back -- MPSMatrixMultiplication has no bf16 on macOS 26.4.1; large-M ~ parity with F.linear); Known limitations rewritten around the real remaining costs. - README.md accelerator table, mps QLoRA 4-bit: 🐢 -> ✅ with a footnote. Justification: "slow implementation" is no longer accurate (inference gemv is fused native Metal at 3.4-6.2x; gemm is native fp16/fp32), but a bare ✅ would overclaim, so the footnote carries the bf16-fallback and large-M-parity caveats inline in the README. - "mis-cast" -> "miscast" (typos hook); _typos.toml learns the mach timebase field `numer` so the hook stops rewriting it to `number`. Parity gate: BNB_MPS_REQUIRE_NATIVE=1 pytest tests/test_mps_parity.py -> 328 passed; the only failure is the pre-existing, unrelated lion strict-xfail XPASS that fails identically on clean HEAD (separate optimizer track). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bdd3d9e commit 0c9db70

7 files changed

Lines changed: 323 additions & 92 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,18 @@ bitsandbytes has the following minimum requirements for all platforms:
182182
<td>⬜ Metal <br><code>mps</code></td>
183183
<td>Apple M1+</td>
184184
<td>❌</td>
185-
<td>🐢</td>
185+
<td>✅ <sup>1</sup></td>
186186
<td>❌</td>
187187
</tr>
188188
</tbody>
189189
</table>
190190

191+
<small><sup>1</sup> On <code>mps</code>, 4-bit matmul runs on native Metal kernels: a fused gemv for
192+
inference (M=1) and an <code>MPSMatrixMultiplication</code>-backed GEMM for fp16/fp32 batches. bf16
193+
batched matmul and other unsupported shapes use a slower dequantize+matmul fallback, and large-batch
194+
GEMM performs on par with dequantize+matmul. Details:
195+
<a href="./docs/apple_silicon/README.md">docs/apple_silicon</a>.</small>
196+
191197
## :book: Documentation
192198

193199
- [Official Documentation](https://huggingface.co/docs/bitsandbytes/main)

_typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extend-ignore-re = [
1515
extend-ignore-identifiers-re = [
1616
".*arange.*",
1717
".*ARANGE.*",
18+
"numer", # mach_timebase_info_data_t.numer (csrc/mps_ops.mm)
1819
]
1920

2021
[type.py.extend-words]

csrc/mps_ops.mm

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <cstdint>
66
#include <dlfcn.h>
77
#include <libgen.h>
8+
#include <mach/mach_time.h>
89
#include <string>
910

1011
// Native Metal dispatch layer for the bitsandbytes MPS backend.
@@ -133,6 +134,17 @@
133134
}
134135
}
135136

137+
// Host time in seconds on the mach_absolute_time timebase -- the same clock
138+
// MTLCommandBuffer's GPUStartTime/GPUEndTime report, so the two are directly comparable.
139+
// Used only by the BNB_MPS_PROFILE probe.
140+
static double host_time_s() {
141+
static mach_timebase_info_data_t tb = {0, 0};
142+
if (tb.denom == 0) {
143+
mach_timebase_info(&tb);
144+
}
145+
return (double)mach_absolute_time() * tb.numer / tb.denom / 1e9;
146+
}
147+
136148
// One thread per block: cap the threadgroup and dispatch a non-uniform grid, then block on
137149
// completion. dispatchThreads is supported on all Apple Silicon GPUs.
138150
static void dispatch_per_block(id<MTLComputeCommandEncoder> enc, id<MTLComputePipelineState> pso, int64_t num_blocks) {
@@ -264,16 +276,26 @@ static void dispatch_per_block(id<MTLComputeCommandEncoder> enc, id<MTLComputePi
264276
// One SIMD-group per output element n.
265277
[enc dispatchThreadgroups:MTLSizeMake((NSUInteger)N, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)];
266278
[enc endEncoding];
267-
[cb commit];
268-
[cb waitUntilCompleted];
269279

270-
// Kernel-only timing probe (BNB_MPS_PROFILE=1): wall-clock around this call includes
271-
// the cross-queue sync + encode + blocking wait; GPUStartTime/GPUEndTime isolates the
272-
// kernel itself for bandwidth accounting.
280+
// Timing probe (BNB_MPS_PROFILE=1): decomposes the blocking call into
281+
// sched = commit -> GPU start (driver/queue scheduling latency)
282+
// gpu = kernel execution (GPUStartTime..GPUEndTime)
283+
// done = GPU end -> waitUntilCompleted return (completion delivery)
284+
// Wall-clock around the whole ctypes call additionally includes encode (above) and
285+
// the caller's torch.mps.synchronize().
273286
static const bool profile = getenv("BNB_MPS_PROFILE") != nullptr;
287+
const double t_commit = profile ? host_time_s() : 0.0;
288+
[cb commit];
289+
[cb waitUntilCompleted];
274290
if (profile) {
291+
const double t_done = host_time_s();
292+
const double sched_ms = ([cb GPUStartTime] - t_commit) * 1000.0;
275293
const double gpu_ms = ([cb GPUEndTime] - [cb GPUStartTime]) * 1000.0;
276-
NSLog(@"bnb_mps_gemv_4bit %@ N=%lld K=%lld gpu=%.3fms", name, (long long)N, (long long)K, gpu_ms);
294+
const double done_ms = (t_done - [cb GPUEndTime]) * 1000.0;
295+
NSLog(
296+
@"bnb_mps_gemv_4bit %@ N=%lld K=%lld sched=%.3fms gpu=%.3fms done=%.3fms", name, (long long)N,
297+
(long long)K, sched_ms, gpu_ms, done_ms
298+
);
277299
}
278300
}
279301
}
@@ -427,15 +449,18 @@ static void dispatch_per_block(id<MTLComputeCommandEncoder> enc, id<MTLComputePi
427449
[enc endEncoding];
428450
}
429451

452+
static const bool profile = getenv("BNB_MPS_PROFILE") != nullptr;
453+
const double t_commit = profile ? host_time_s() : 0.0;
430454
[cb commit];
431455
[cb waitUntilCompleted];
432-
433-
static const bool profile = getenv("BNB_MPS_PROFILE") != nullptr;
434456
if (profile) {
457+
const double t_done = host_time_s();
458+
const double sched_ms = ([cb GPUStartTime] - t_commit) * 1000.0;
435459
const double gpu_ms = ([cb GPUEndTime] - [cb GPUStartTime]) * 1000.0;
460+
const double done_ms = (t_done - [cb GPUEndTime]) * 1000.0;
436461
NSLog(
437-
@"bnb_mps_gemm_4bit dtype=%lld M=%lld N=%lld K=%lld bias=%d gpu=%.3fms", (long long)dtype_flag,
438-
(long long)M, (long long)N, (long long)K, bias ? 1 : 0, gpu_ms
462+
@"bnb_mps_gemm_4bit dtype=%lld M=%lld N=%lld K=%lld bias=%d sched=%.3fms gpu=%.3fms done=%.3fms",
463+
(long long)dtype_flag, (long long)M, (long long)N, (long long)K, bias ? 1 : 0, sched_ms, gpu_ms, done_ms
439464
);
440465
}
441466
}

0 commit comments

Comments
 (0)