Skip to content

Introduction to Vulkan Compute

What is Vulkan?

Vulkan is a modern, low-level graphics and compute API developed by the Khronos Group. Unlike higher-level APIs such as OpenGL or CUDA, which manage much of the GPU state for you, Vulkan gives you explicit control over the GPU, resulting in better performance but requiring more code.

Graphics vs Compute

Vulkan supports two main types of workloads:

Aspect Graphics Compute
Purpose Rendering images General-purpose processing
Pipeline Complex (vertex, fragment, etc.) Simple (single stage)
Output Framebuffer/images Buffers/images
Use cases Games, visualization ML, physics, image processing

This series focuses entirely on Compute — using the GPU as a massively parallel processor.

The Vulkan Execution Model

1. Host and Device

flowchart TB
    subgraph HOST["HOST (CPU)"]
        direction LR
        App["Application<br/>(Your C)"] --> Lib["Vulkan<br/>Library"] --> Drv["Driver"]
    end

    subgraph DEVICE["DEVICE (GPU)"]
        direction TB
        subgraph CUs["Compute Units"]
            direction LR
            CU1["CU"]
            CU2["CU"]
            CU3["CU"]
            CU4["CU"]
            CU5["..."]
        end
        MEM["GPU Memory"]
    end

    HOST --> DEVICE
  • Host: Your CPU and system memory where your C code runs
  • Device: The GPU with its own memory and compute units

2. Queues and Commands

The GPU doesn't execute code directly. Instead, you:

  1. Record commands into a command buffer
  2. Submit the command buffer to a queue
  3. The GPU processes commands asynchronously
// Record commands
vkBeginCommandBuffer(cmd, &begin_info);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdDispatch(cmd, group_count_x, 1, 1);
vkEndCommandBuffer(cmd);

// Submit to queue
vkQueueSubmit(queue, 1, &submit_info, fence);

// Wait for completion
vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);

3. Workgroups and Invocations

Compute shaders run in a hierarchical structure. Dispatches, workgroups, and invocations each have 3D coordinates (x, y, z) — this series only ever uses the x dimension, so you'll see indices like (1,0,0) and gl_GlobalInvocationID.x throughout:

flowchart TD
    D["Dispatch (vkCmdDispatch)"]
    D --> WG0["Workgroup [0,0,0]"]
    D --> WG1["Workgroup [1,0,0]"]
    D --> WGN["Workgroup [N,0,0]"]

    WG0 --> I0["Invocation (0,0,0)"]
    WG0 --> I1["Invocation (1,0,0)"]
    WG0 --> I2["Invocation (2,0,0)"]
    WG0 --> I3["... up to local_size"]
  • Invocation: A single execution of your shader (like a thread)
  • Workgroup: A group of invocations that can share memory and synchronize
  • Dispatch: The total number of workgroups to launch

Compute Shaders (GLSL)

Shaders are written in GLSL and compiled to SPIR-V:

#version 450

// Workgroup size: 256 threads
layout(local_size_x = 256) in;

// Input/output buffers
layout(set = 0, binding = 0) readonly buffer Input {
    float data[];
} input_buf;

layout(set = 0, binding = 1) writeonly buffer Output {
    float data[];
} output_buf;

void main() {
    uint idx = gl_GlobalInvocationID.x;
    output_buf.data[idx] = input_buf.data[idx] * 2.0;
}

Key concepts:

  • local_size_x: Threads per workgroup
  • gl_GlobalInvocationID: Unique index for this invocation
  • layout(set, binding): Where to find resources

The Vulkan Workflow

Every Vulkan compute program follows this pattern:

flowchart TD
    A["1. Initialize<br/>Create instance, device, queue"]
    B["2. Setup Resources<br/>Allocate buffers · Create descriptor sets · Load/create pipeline"]
    C["3. Execute<br/>Record command buffer · Submit to queue · Wait for completion"]
    D["4. Cleanup<br/>Destroy all objects"]

    A --> B --> C --> D

Error Handling

Vulkan functions return VkResult. Always check for success:

VkResult result = vkCreateBuffer(device, &info, NULL, &buffer);
if (result != VK_SUCCESS) {
    fprintf(stderr, "Failed to create buffer: %d\n", result);
    return -1;
}

Validation Layers

Vulkan has minimal error checking by default for performance. Enable validation layers during development:

const char* layers[] = {"VK_LAYER_KHRONOS_validation"};
VkInstanceCreateInfo create_info = {
    // ...
    .enabledLayerCount = 1,
    .ppEnabledLayerNames = layers
};

Validation layers catch:

  • Invalid API usage
  • Memory leaks
  • Synchronization errors
  • Best practice violations

What's Next?

Now that you understand the concepts, let's write code! Head to Chapter 01 to create your first Vulkan program.

Don't worry

Vulkan has a lot of concepts, but you'll learn them incrementally. Each chapter introduces just a few new ideas, building on what you've already learned.