Vulkan 计算着色器缓存和屏障
Vullkan compute shader caches and barriers
我正在尝试了解整个 L1/L2 冲洗的工作原理。假设我有一个像这样的计算着色器
layout(std430, set = 0, binding = 2) buffer Particles{
Particle particles[];
};
layout(std430, set = 0, binding = 4) buffer Constraints{
Constraint constraints[];
};
void main(){
const uint gID = gl_GlobalInvocationID.x;
for (int pass=0;pass<GAUSS_SEIDEL_PASSES;pass++){
// first query the constraint, which contains particle_id_1 and particle_id_1
const Constraint c = constraints[gID*GAUSS_SEIDEL_PASSES+pass];
// read newest positions
vec3 position1 = particles[c.particle_id_1].position;
vec3 position2 = particles[c.particle_id_2].position;
// modify position1 and position2
position1 += something;
position2 -= something;
// update positions
particles[c.particle_id_1].position = position1;
particles[c.particle_id_2].position = position2;
// in the next iteration, different constraints may use the updated positions
}
}
据我了解,最初所有数据都驻留在 L2 中。当我读取 particles[c.particle_id_1].position
时,我将一些数据从 L2 复制到 L1(或直接复制到寄存器)。
然后在 position1 += something
中修改 L1(或寄存器)。最后在 particles[c.particle_id_2].position = position1
中,我将数据从 L1(或寄存器)刷新回 L2,对吗?因此,如果我有第二个计算着色器,我想在这个之后 运行,并且第二个着色器将读取粒子的位置,我不需要同步 Particles
。只设置一个执行障碍就足够了,没有内存障碍
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask, // here I put VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
VkPipelineStageFlags dstStageMask, // here I put VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
VkDependencyFlags dependencyFlags, // here nothing
uint32_t memoryBarrierCount, // here 0
const VkMemoryBarrier* pMemoryBarriers, // nullptr
uint32_t bufferMemoryBarrierCount, // 0
const VkBufferMemoryBarrier* pBufferMemoryBarriers, // nullptr
uint32_t imageMemoryBarrierCount, // 0
const VkImageMemoryBarrier* pImageMemoryBarriers); // nullptr
依赖于实现。据我们所知,一个设备可能根本没有缓存,或者在未来它可能是一些量子魔法 bs。
着色器赋值操作不代表任何东西。 Vulkan 规范中没有任何地方提到“L1”或“L2”。这是一个不存在的概念。
完全摆脱缓存的东西,以及随之而来的所有精神包袱。
这里重要的是,当您阅读某些内容时,该内容需要对阅读代理“可见”(无论您使用何种设备,以及它可能具有何种晦涩的内存架构)。如果它不是“可见”,那么您可能正在阅读垃圾。
当你写东西时,这不会自动发生。写入对任何人都“不可见”。
首先,您将写入内容放入内存依赖项的 src*
部分(例如,通过管道屏障)。这将使您的文章“可从”获得。
然后将 reader 放入 dst*
,这将获取所有“可从”引用的写入,并使它们“对”第二个同步范围“可见”。
如果您真的想将其硬塞进缓存系统概念中,请不要将其视为缓存级别。将其视为单独的缓存。有些东西已经在某些缓存中并不意味着它在消费者需要的特定缓存中。
Vulkan 的内存模型不关心“缓存”作为缓存。它的模型建立在可用性和可见性的概念之上。如果 command/stage A 与 command/stage B 具有执行依赖性,则 GPU command/stage A 生成的值对 GPU command/stage B 是“可用的”。GPU command/stage A 对 GPU command/stage B 是“可见的”,如果 command/stage A 与 command/stage B 关于所讨论的特定内存和 A 写入它的访问模式具有内存依赖性B 将访问它。
如果某个值 不可用且对 command/stage 不可见,则尝试访问它会产生未定义的行为。
可用性和可见性的实现会涉及清除缓存等。但就 Vulkan 内存模型而言,这是它不关心的实现细节。您也不应该:了解 Vulkan 内存模型并编写在其中运行的代码。
您的管道屏障会产生执行依赖性,但不会产生内存依赖性。因此,CS 进程在屏障之前写入的值对于之后的 CS 进程是可用的,但对他们来说不可见。您需要具有内存依赖性才能建立可见性。
但是,如果你想要一个 GPU 级别的理解......这一切都取决于 GPU。 GPU 是否有缓存层次结构,L1/L2 拆分?也许有人会,也许不会。
无论如何这有点无关紧要,因为仅仅将值写入内存中的地址 不 等同于“刷新”该内存周围的适当缓存。即使使用 coherent
限定符也只会导致在同一调度调用中执行的计算着色器操作刷新。不能保证会影响以后 调度调用。
我正在尝试了解整个 L1/L2 冲洗的工作原理。假设我有一个像这样的计算着色器
layout(std430, set = 0, binding = 2) buffer Particles{
Particle particles[];
};
layout(std430, set = 0, binding = 4) buffer Constraints{
Constraint constraints[];
};
void main(){
const uint gID = gl_GlobalInvocationID.x;
for (int pass=0;pass<GAUSS_SEIDEL_PASSES;pass++){
// first query the constraint, which contains particle_id_1 and particle_id_1
const Constraint c = constraints[gID*GAUSS_SEIDEL_PASSES+pass];
// read newest positions
vec3 position1 = particles[c.particle_id_1].position;
vec3 position2 = particles[c.particle_id_2].position;
// modify position1 and position2
position1 += something;
position2 -= something;
// update positions
particles[c.particle_id_1].position = position1;
particles[c.particle_id_2].position = position2;
// in the next iteration, different constraints may use the updated positions
}
}
据我了解,最初所有数据都驻留在 L2 中。当我读取 particles[c.particle_id_1].position
时,我将一些数据从 L2 复制到 L1(或直接复制到寄存器)。
然后在 position1 += something
中修改 L1(或寄存器)。最后在 particles[c.particle_id_2].position = position1
中,我将数据从 L1(或寄存器)刷新回 L2,对吗?因此,如果我有第二个计算着色器,我想在这个之后 运行,并且第二个着色器将读取粒子的位置,我不需要同步 Particles
。只设置一个执行障碍就足够了,没有内存障碍
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask, // here I put VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
VkPipelineStageFlags dstStageMask, // here I put VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
VkDependencyFlags dependencyFlags, // here nothing
uint32_t memoryBarrierCount, // here 0
const VkMemoryBarrier* pMemoryBarriers, // nullptr
uint32_t bufferMemoryBarrierCount, // 0
const VkBufferMemoryBarrier* pBufferMemoryBarriers, // nullptr
uint32_t imageMemoryBarrierCount, // 0
const VkImageMemoryBarrier* pImageMemoryBarriers); // nullptr
依赖于实现。据我们所知,一个设备可能根本没有缓存,或者在未来它可能是一些量子魔法 bs。
着色器赋值操作不代表任何东西。 Vulkan 规范中没有任何地方提到“L1”或“L2”。这是一个不存在的概念。
完全摆脱缓存的东西,以及随之而来的所有精神包袱。
这里重要的是,当您阅读某些内容时,该内容需要对阅读代理“可见”(无论您使用何种设备,以及它可能具有何种晦涩的内存架构)。如果它不是“可见”,那么您可能正在阅读垃圾。
当你写东西时,这不会自动发生。写入对任何人都“不可见”。
首先,您将写入内容放入内存依赖项的 src*
部分(例如,通过管道屏障)。这将使您的文章“可从”获得。
然后将 reader 放入 dst*
,这将获取所有“可从”引用的写入,并使它们“对”第二个同步范围“可见”。
如果您真的想将其硬塞进缓存系统概念中,请不要将其视为缓存级别。将其视为单独的缓存。有些东西已经在某些缓存中并不意味着它在消费者需要的特定缓存中。
Vulkan 的内存模型不关心“缓存”作为缓存。它的模型建立在可用性和可见性的概念之上。如果 command/stage A 与 command/stage B 具有执行依赖性,则 GPU command/stage A 生成的值对 GPU command/stage B 是“可用的”。GPU command/stage A 对 GPU command/stage B 是“可见的”,如果 command/stage A 与 command/stage B 关于所讨论的特定内存和 A 写入它的访问模式具有内存依赖性B 将访问它。
如果某个值 不可用且对 command/stage 不可见,则尝试访问它会产生未定义的行为。
可用性和可见性的实现会涉及清除缓存等。但就 Vulkan 内存模型而言,这是它不关心的实现细节。您也不应该:了解 Vulkan 内存模型并编写在其中运行的代码。
您的管道屏障会产生执行依赖性,但不会产生内存依赖性。因此,CS 进程在屏障之前写入的值对于之后的 CS 进程是可用的,但对他们来说不可见。您需要具有内存依赖性才能建立可见性。
但是,如果你想要一个 GPU 级别的理解......这一切都取决于 GPU。 GPU 是否有缓存层次结构,L1/L2 拆分?也许有人会,也许不会。
无论如何这有点无关紧要,因为仅仅将值写入内存中的地址 不 等同于“刷新”该内存周围的适当缓存。即使使用 coherent
限定符也只会导致在同一调度调用中执行的计算着色器操作刷新。不能保证会影响以后 调度调用。