Estoy tratando de usar MTLSharedEvent junto con MTLSharedEventListener para sincronizar el cálculo entre GPU y CPU, como en el ejemplo proporcionado por Apple ( https://developer.apple.com/documentation/metal/synchronization/synchronizing_events_ between_a_gpu_and_the_cpu ). Básicamente, lo que quiero lograr es dividir el trabajo en 3 partes ejecutadas en orden, así:
Mi problema es que el bloque eventListener siempre se llama antes de que se programe la ejecución del búfer de comandos, lo que hace que mi tarea de CPU se ejecute primero en orden.
Para simplificar el caso, usemos comandos simples que llenen MTLBuffer con ciertos valores (mi caso de uso original es más complicado, ya que usa codificadores de cómputo con sombreadores personalizados, pero se comporta de la misma manera):
let device = MTLCreateSystemDefaultDevice()! let queue = device.makeCommandQueue()! let event = device.makeSharedEvent()! let dispatchQueue = DispatchQueue(label: "myqueue") let eventListener = MTLSharedEventListener(dispatchQueue: dispatchQueue) let metalBuffer = device.makeBuffer(length: 2048, options: MTLResourceOptions.storageModeShared)! let buffer = queue.makeCommandBuffer()! NSLog("Start - signaled value: \(event.signaledValue)") event.notify(eventListener, atValue: 1) { event, value in // CPU work let pointer = metalBuffer.contents().assumingMemoryBound(to: UInt8.self) for i in 0..<512 { (pointer + i).pointee = (pointer + i).pointee + 1; } NSLog("Event notification - signaled value: \(value), buffer status: \(buffer.status.rawValue)") event.signaledValue = 2 } // GPU work part 1 let encoder1 = buffer.makeBlitCommandEncoder()! encoder1.fill(buffer: metalBuffer, range: .init(0...127), value: 22) encoder1.endEncoding() // signal with 1 to start CPU task buffer.encodeSignalEvent(event, value: 1) // wait for value >= 2 to proceed buffer.encodeWaitForEvent(event, value: 2) // GPU work part 2 let encoder2 = buffer.makeBlitCommandEncoder()! encoder2.fill(buffer: metalBuffer, range: .init(128...511), value: 255) encoder2.endEncoding() buffer.addScheduledHandler { buffer in NSLog("Buffer scheduled - signaled value: \(event.signaledValue)") } buffer.addCompletedHandler { buffer in NSLog("Buffer completed - signaled value: \(event.signaledValue)") } buffer.commit() buffer.waitUntilCompleted()Producción:
2022-01-09 23:46:08.774 Sync[76882:3531755] Metal GPU Frame Capture Enabled 2022-01-09 23:46:08.805 Sync[76882:3531755] Start - signaled value: 0 2022-01-09 23:46:08.808 Sync[76882:3531764] Event notification - signaled value: 1, buffer status: 2 (Commited) 2022-01-09 23:46:08.809 Sync[76882:3531763] Buffer scheduled - signaled value: 2 2022-01-09 23:46:08.809 Sync[76882:3531763] Buffer completed - signaled value: 2 Como puede ver, eventListener registra el estado del búfer como .commited . ¿Qué pasa aquí? ¿Me estoy perdiendo de algo?
Sistema: macOS 12.0.1, Apple M1 Pro, Xcode 13.2.1
Está perfectamente bien que el búfer de comando esté comprometido. De hecho, si no se comprometiera, nunca podrá notify el bloqueo.
GPU y CPU se ejecutan en paralelo. Entonces, cuando usa MTLEvent , no deja de ejecutar el código de la CPU (en realidad, todo el código Swift). Simplemente le dice a la GPU en qué orden ejecutar el código de la GPU.
Entonces, ¿qué está pasando en tu caso?
commit() . Antes, la GPU en realidad no hace nada. Acaba de programar el comando para que se ejecute en la GPU, pero no lo ejecuta.MTLEvent . Realiza la parte 1, luego codifica el valor 1 para el evento, realiza el bloque de notificación, codifica el valor 2 , realiza el segundo bloque de GPU. Pero nuevamente, todo el trabajo real de la GPU comienza solo cuando llamas a commit() en el búfer de comando. Es por eso que el búfer ya está comprometido en el bloque de notificación. Porque se realiza después de commit() .