import Metal // ══════════════════════════════════════════════════════════════════ // TRUE Batch Generation - Using batch Metal kernels // Expected: 8-15x speedup for batch inference // ══════════════════════════════════════════════════════════════════ extension E4BModel { /// TRUE batch forward pass - process multiple tokens with batch kernels /// This achieves real parallelism, not sequential processing public func forwardBatchTrue( tokenIds: [Int], positions: [Int], context: BatchContext ) throws -> [[Float]] { guard tokenIds.count == positions.count else { return [] } let batchSize = tokenIds.count guard batchSize <= context.maxBatchSize else { return [] } if batchSize == 0 { return [] } if batchSize == 1 { return [try forwardOptimized(tokenId: tokenIds[0], position: positions[0])] } // ── Phase 1: Embedding Lookup (FIXED: Use batch kernel) ── // Debug: Check embedWeight parameters BEFORE batch embedding print("BEFORE batch embedding:") print(" hiddenSize=\(hiddenSize)") print(" embedWeight.groupSize=\(embedWeight.groupSize)") print(" embedWeight.weight.length=\(embedWeight.weight.length)") print(" embedWeight.scales.length=\(embedWeight.scales.length)") print(" embedWeight.biases.length=\(embedWeight.biases.length)") print(" embedWeight.inDim=\(embedWeight.inDim)") print(" embedWeight.outDim=\(embedWeight.outDim)") print(" vocabSize=\(vocabSize)") print(" batchSize=\(batchSize)") print(" embedScale=\(embedScale) (should be ~50.6 for hiddenSize=2560)") print(" tokenIds=\(tokenIds)") // Prepare tokenIds array for Metal let tokenIdsBuffer = engine.device.makeBuffer( bytes: tokenIds.map { UInt32($0) }, length: batchSize * 4, options: .storageModeShared )! // Use batch embedding kernel let embedCmdBuf = engine.commandQueue.makeCommandBuffer()! let pso = try engine.pipeline(named: embedScale != 1.0 ? "dequantize_row_batch_scaled" : "dequantize_row_batch") let enc = embedCmdBuf.makeComputeCommandEncoder()! enc.setComputePipelineState(pso) enc.setBuffer(embedWeight.weight, offset: 0, index: 0) enc.setBuffer(embedWeight.scales, offset: 0, index: 1) enc.setBuffer(embedWeight.biases, offset: 0, index: 2) enc.setBuffer(tokenIdsBuffer, offset: 0, index: 3) enc.setBuffer(context.batchInputBuffer, offset: 0, index: 4) var nCols = UInt32(hiddenSize) var batchSz = UInt32(batchSize) var groupSz = UInt32(embedWeight.groupSize) enc.setBytes(&nCols, length: 4, index: 5) enc.setBytes(&batchSz, length: 4, index: 6) enc.setBytes(&groupSz, length: 4, index: 7) if embedScale != 1.0 { var scale = embedScale enc.setBytes(&scale, length: 4, index: 8) } // Calculate threadgroup size (2D grid: batchSize × hiddenSize) let threadsPerThreadgroup = MTLSize(width: 32, height: 8, depth: 1) let gridSize = MTLSize(width: batchSize, height: hiddenSize, depth: 1) enc.dispatchThreads(gridSize, threadsPerThreadgroup: threadsPerThreadgroup) enc.endEncoding() embedCmdBuf.commit() embedCmdBuf.waitUntilCompleted() // ── Phase 2: Layer Processing with BATCH KERNELS ── let layerCmdBuf = engine.commandQueue.makeCommandBuffer()! // Create batch temps for layer processing let batchTemps = try temps.createBatchBuffers( device: engine.device, batchSize: batchSize, hiddenSize: hiddenSize, nHeads: layers[0].config.nHeads, headDim: layers[0].config.headDim, intermediateSize: layers[0].config.intermediateSize ) // Process all 42 layers with batch kernels for layerIdx in 0..( start: outputPtr + i * vocabSize, count: vocabSize )) results.append(logits) } return results } }