|
9 | 9 | "time" |
10 | 10 | "unsafe" |
11 | 11 |
|
| 12 | + "github.com/tmc/apple/foundation" |
12 | 13 | "github.com/tmc/apple/metal" |
13 | 14 | ) |
14 | 15 |
|
@@ -204,3 +205,86 @@ func TestHopTaxMicrobench(t *testing.T) { |
204 | 205 | t.Fatal("dependent chain measured no GPU time — the bench is broken") |
205 | 206 | } |
206 | 207 | } |
| 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