Skip to content

Commit 452ed73

Browse files
committed
perf(engine/metal): ICB go/no-go instrument — phase 3 CLOSED by rule (#341)
ICB commands within one executeCommandsInBuffer are concurrent-typed (no ordering inside an execute), so a dependent per-layer chain recorded as an ICB must issue one execute per stage. TestHopTaxICBMicrobench measures that shape: a DEPENDENT execute on a serial encoder costs 9.76us/hop on this hardware — 2.4x the 4.13us barrier hop the carried concurrent encoder already pays — while the ICB concurrency ceiling for independent bundles is 0.52us/dispatch (which is what the dense lane correctly uses ICBs for). Pre-registered rule fires: a stage-ICB MoE decode would REGRESS the 26B, and the host-encode elimination ICBs classically buy is already hidden by submit-ahead (0.17 ms/token host/sync gap). The stale router-readback premise is retired a different way than expected: the prerequisites all landed, and the measurement says the destination is not worth the trip. This also explains the dense/E-family lanes sharing the same ~110us/layer floor despite their ICB path — dependent executes pay the same drain. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent bc2488d commit 452ed73

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

go/engine/metal/hop_tax_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010
"unsafe"
1111

12+
"github.com/tmc/apple/foundation"
1213
"github.com/tmc/apple/metal"
1314
)
1415

@@ -204,3 +205,86 @@ func TestHopTaxMicrobench(t *testing.T) {
204205
t.Fatal("dependent chain measured no GPU time — the bench is broken")
205206
}
206207
}
208+
209+
// TestHopTaxICBMicrobench is the phase-3 go/no-go instrument (#341): ICB
210+
// commands within one executeCommandsInBuffer are CONCURRENT-typed (no
211+
// ordering inside an execute), so a dependent per-layer chain recorded as an
212+
// ICB must issue one execute per stage — the decisive constant is therefore
213+
// the cost of a DEPENDENT execute on a serial encoder, against the carried
214+
// concurrent encoder's 4.13µs barrier hop (the shape the decode already runs).
215+
//
216+
// Pre-registered rule: dependent-execute ≥ the barrier hop ⇒ a stage-ICB MoE
217+
// decode buys nothing GPU-side (and submit-ahead already hides the host
218+
// encode) ⇒ phase 3 closes by rule. Only a dramatically cheaper execute
219+
// (< ~2µs) earns the build.
220+
func TestHopTaxICBMicrobench(t *testing.T) {
221+
requireNativeRuntime(t)
222+
const dModel = 2816
223+
const n = 512
224+
eps := float32(1e-6)
225+
226+
rmsPSO, err := pipelineForICB(rmsKernelBF16(dModel))
227+
if err != nil {
228+
t.Skipf("ICB-capable rms pipeline unavailable: %v", err)
229+
}
230+
rmsTG := rmsThreadgroup(dModel, rmsPSO)
231+
232+
w := toBF16Bytes(syntheticFloat32(dModel, 7))
233+
wBuf := device.NewBufferWithBytesLengthOptions(unsafe.Pointer(&w[0]), uint(len(w)), metal.MTLResourceStorageModeShared)
234+
ping := device.NewBufferWithLengthOptions(uint(dModel*bf16Size), metal.MTLResourceStorageModeShared)
235+
pong := device.NewBufferWithLengthOptions(uint(dModel*bf16Size), metal.MTLResourceStorageModeShared)
236+
indepOut := device.NewBufferWithLengthOptions(uint(n*dModel*bf16Size), metal.MTLResourceStorageModeShared)
237+
238+
icbDesc := metal.NewMTLIndirectCommandBufferDescriptor()
239+
icbDesc.SetCommandTypes(metal.MTLIndirectCommandTypeConcurrentDispatch)
240+
icbDesc.SetInheritBuffers(false)
241+
icbDesc.SetInheritPipelineState(false)
242+
icbDesc.SetMaxKernelBufferBindCount(8)
243+
244+
// dependent chain: command i reads the previous command's output (ping↔pong),
245+
// replayed as n SEPARATE executes so the serial encoder orders them.
246+
depICB := device.NewIndirectCommandBufferWithDescriptorMaxCommandCountOptions(icbDesc, uint(n), metal.MTLResourceStorageModeShared)
247+
a, b := ping, pong
248+
for i := 0; i < n; i++ {
249+
cmd := indirectComputeCommandAtIndexFast(depICB, uint(i))
250+
emitRMSNorm(fastICBSink{cmd}, rmsPSO, a, wBuf, b, 0, dModel, eps, rmsTG)
251+
a, b = b, a
252+
}
253+
// independent: n commands with disjoint outputs, ONE execute (the ICB's
254+
// internal concurrency ceiling).
255+
indepICB := device.NewIndirectCommandBufferWithDescriptorMaxCommandCountOptions(icbDesc, uint(n), metal.MTLResourceStorageModeShared)
256+
for i := 0; i < n; i++ {
257+
cmd := indirectComputeCommandAtIndexFast(indepICB, uint(i))
258+
emitRMSNormAt(fastICBSink{cmd}, rmsPSO, ping, wBuf, indepOut, 0, 0, uint(i*dModel*bf16Size), dModel, eps, rmsTG)
259+
}
260+
261+
resident := []metal.MTLResource{wBuf, ping, pong, indepOut}
262+
residentIDs := resourceIDsForFastUse(nil, resident)
263+
empty := hopTaxTime(t, func(cb metal.MTLCommandBufferObject) {})
264+
265+
depExec := hopTaxTime(t, func(cb metal.MTLCommandBufferObject) {
266+
enc := computeCommandEncoderFast(cb)
267+
useResourcesIDsFast(enc, resident, residentIDs, metal.MTLResourceUsageRead|metal.MTLResourceUsageWrite)
268+
for i := 0; i < n; i++ {
269+
executeCommandsInBufferWithRangeFast(enc, depICB, foundation.NSRange{Location: uint(i), Length: 1})
270+
}
271+
endEncodingFast(enc)
272+
})
273+
oneExec := hopTaxTime(t, func(cb metal.MTLCommandBufferObject) {
274+
enc := computeCommandEncoderFast(cb)
275+
useResourcesIDsFast(enc, resident, residentIDs, metal.MTLResourceUsageRead|metal.MTLResourceUsageWrite)
276+
executeCommandsInBufferWithRangeFast(enc, indepICB, foundation.NSRange{Location: 0, Length: uint(n)})
277+
endEncodingFast(enc)
278+
})
279+
280+
per := func(d time.Duration, count int) float64 {
281+
net := d - empty
282+
if net < 0 {
283+
net = 0
284+
}
285+
return float64(net.Nanoseconds()) / 1e3 / float64(count)
286+
}
287+
t.Logf("ICB rms %d-wide, n=%d:", dModel, n)
288+
t.Logf(" dependent, one execute per hop (serial enc): %7.2f µs/hop — compare the 4.13µs barrier hop", per(depExec, n))
289+
t.Logf(" independent, ONE execute of %d commands: %7.2f µs/dispatch — the ICB concurrency ceiling", n, per(oneExec, n))
290+
}

0 commit comments

Comments
 (0)