使用 MTLSharedEvent 在单个命令缓冲区内同步 CPU 和 GPU 之间的工作

Synchronize work between CPU and GPU within single command buffer using MTLSharedEvent

我正在尝试使用 MTLSharedEventMTLSharedEventListener 来同步 GPU 和 CPU 之间的计算,如 Apple 提供的示例 (https://developer.apple.com/documentation/metal/synchronization/synchronizing_events_between_a_gpu_and_the_cpu)。基本上我想要实现的是将工作分成 3 个部分按顺序执行,如下所示:

  1. GPU 计算部分 1
  2. CPU 计算基于 GPU 计算第 1 部分的结果
  3. CPU 计算后的 GPU 计算第 2 部分

我的问题是 eventListener 块总是在命令缓冲区被安排执行之前调用,这使得我的 CPU 任务按顺序首先执行。

为了简化案例,让我们使用简单的命令填充 MTLBuffer 某些值(我的原始用例更复杂,因为使用带有自定义着色器的计算编码器,但行为相同):

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()

输出:

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

如您所见,eventListener 将缓冲区状态记录为 .commited。 这是怎么回事?我错过了什么吗?

系统:macOS 12.0.1,Apple M1 Pro,Xcode13.2.1

提交命令缓冲区完全没问题。事实上,如果它不被提交,你将永远不会到达 notify 块。

GPU 和 CPU 并行运行。因此,当您使用 MTLEvent 时,您不会停止执行 CPU 代码(实际上是所有 Swift 代码)。您只需告诉 GPU 以什么顺序执行 GPU 代码。

那么你的情况是怎样的:

  1. 您的所有代码都在单个 CPU 线程中运行,没有任何中断。
  2. GPU 仅在您调用 commit() 时才开始执行命令缓冲区命令。在此之前,GPU 实际上什么都不做。您刚刚安排了要在 GPU 上执行的命令,但没有执行它们。
  3. 当 GPU 执行命令时,它会检查您的 MTLEvent。它执行第 1 部分,然后将值 1 编码为事件,执行通知块,对值 2 进行编码,执行第二个 GPU 块。

但是,只有当您在命令缓冲区上调用 commit() 时,所有实际的 GPU 工作才开始。这就是缓冲区已经在通知块中提交的原因。因为是在commit().

之后执行的