Chapter 10: Debugging¶
Overview¶
Debugging GPU code is challenging. This chapter covers:
- Validation layers for error detection
- Debug naming and labels
- Profiling and performance analysis
- Common debugging techniques
What you'll learn:
- Setting up validation layers
- Using GPU debugging tools
- Identifying performance bottlenecks
Validation Layers¶
Vulkan has minimal runtime error checking for performance. Validation layers add comprehensive checking during development.
Enabling Validation¶
const char* layers[] = {
"VK_LAYER_KHRONOS_validation"
};
VkInstanceCreateInfo create_info = {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.enabledLayerCount = 1,
.ppEnabledLayerNames = layers,
.enabledExtensionCount = 1,
.ppEnabledExtensionNames = (const char*[]){
VK_EXT_DEBUG_UTILS_EXTENSION_NAME
}
};
Debug Callback¶
Receive validation messages:
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(
VkDebugUtilsMessageSeverityFlagBitsEXT severity,
VkDebugUtilsMessageTypeFlagsEXT type,
const VkDebugUtilsMessengerCallbackDataEXT* data,
void* user_data)
{
const char* severity_str = "";
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
severity_str = "ERROR";
else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
severity_str = "WARNING";
else if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT)
severity_str = "INFO";
fprintf(stderr, "[Vulkan %s] %s\n", severity_str, data->pMessage);
return VK_FALSE; // Don't abort
}
// Create messenger
VkDebugUtilsMessengerCreateInfoEXT messenger_info = {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.pfnUserCallback = debug_callback
};
VkDebugUtilsMessengerEXT messenger;
// Need to load function pointer
PFN_vkCreateDebugUtilsMessengerEXT func =
(PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(
instance, "vkCreateDebugUtilsMessengerEXT");
func(instance, &messenger_info, NULL, &messenger);
Debug Object Names¶
Name your objects for clearer error messages:
VkDebugUtilsObjectNameInfoEXT name_info = {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT,
.objectType = VK_OBJECT_TYPE_BUFFER,
.objectHandle = (uint64_t)input_buffer,
.pObjectName = "Input Data Buffer"
};
PFN_vkSetDebugUtilsObjectNameEXT setName =
(PFN_vkSetDebugUtilsObjectNameEXT)vkGetDeviceProcAddr(
device, "vkSetDebugUtilsObjectNameEXT");
setName(device, &name_info);
Now errors reference "Input Data Buffer" instead of "VkBuffer 0x7f3a2b1c":
Debug Labels¶
Mark regions in command buffers:
// Helper function
void cmd_begin_label(VkCommandBuffer cmd, const char* name,
float r, float g, float b) {
VkDebugUtilsLabelEXT label = {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT,
.pLabelName = name,
.color = {r, g, b, 1.0f}
};
PFN_vkCmdBeginDebugUtilsLabelEXT func = ...;
func(cmd, &label);
}
void cmd_end_label(VkCommandBuffer cmd) {
PFN_vkCmdEndDebugUtilsLabelEXT func = ...;
func(cmd);
}
// Usage
cmd_begin_label(cmd, "Data Processing", 0.0f, 1.0f, 0.0f);
vkCmdBindPipeline(cmd, ...);
vkCmdDispatch(cmd, 256, 1, 1);
cmd_end_label(cmd);
cmd_begin_label(cmd, "Post-Processing", 1.0f, 0.0f, 0.0f);
// ...
cmd_end_label(cmd);
These labels appear in debugging tools like RenderDoc.
GPU Timestamps¶
Measure GPU execution time:
// Create query pool
VkQueryPoolCreateInfo query_info = {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP,
.queryCount = 2
};
VkQueryPool query_pool;
vkCreateQueryPool(device, &query_info, NULL, &query_pool);
// Record timestamps
vkCmdResetQueryPool(cmd, query_pool, 0, 2);
vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
query_pool, 0);
vkCmdDispatch(cmd, 256, 1, 1);
vkCmdWriteTimestamp(cmd, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
query_pool, 1);
// After execution, read results
uint64_t timestamps[2];
vkGetQueryPoolResults(device, query_pool, 0, 2,
sizeof(timestamps), timestamps,
sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
float nano_per_tick = props.limits.timestampPeriod;
float ms = (timestamps[1] - timestamps[0]) * nano_per_tick / 1e6;
printf("Dispatch took %.3f ms\n", ms);
Pipeline Statistics¶
Query execution statistics:
VkQueryPoolCreateInfo query_info = {
.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_PIPELINE_STATISTICS,
.queryCount = 1,
.pipelineStatistics = VK_QUERY_PIPELINE_STATISTIC_COMPUTE_SHADER_INVOCATIONS_BIT
};
// Wrap dispatch in query
vkCmdBeginQuery(cmd, query_pool, 0, 0);
vkCmdDispatch(cmd, 256, 1, 1);
vkCmdEndQuery(cmd, query_pool, 0);
// Read results
uint64_t invocations;
vkGetQueryPoolResults(device, query_pool, 0, 1,
sizeof(invocations), &invocations,
sizeof(uint64_t), VK_QUERY_RESULT_64_BIT);
printf("Shader invocations: %lu\n", invocations);
Debugging Tools¶
RenderDoc¶
Free, open-source GPU debugger:
- Launch your app through RenderDoc
- Capture a frame
- Inspect:
- Command buffer contents
- Buffer data at each stage
- Shader debugging (step through execution)
NVIDIA Nsight¶
For NVIDIA GPUs: - Nsight Graphics: Frame debugging - Nsight Compute: Kernel profiling - Nsight Systems: System-wide profiling
AMD RGP/RGD¶
For AMD GPUs: - Radeon GPU Profiler (RGP): Performance analysis - Radeon GPU Detective (RGD): Crash debugging
Intel GPA¶
For Intel GPUs: - Graphics Performance Analyzers - Metrics and trace analysis
Common Validation Errors¶
Synchronization Hazard¶
SYNC-HAZARD-WRITE-AFTER-READ: vkCmdDispatch(): Hazard WRITE_AFTER_READ
for Buffer "Output Buffer". Prior access by vkCmdDispatch() with usage READ.
Fix: Add a barrier between dispatches.
Missing Descriptor¶
Fix: Update all required descriptor bindings.
Invalid Layout¶
VUID-VkWriteDescriptorSet-descriptorType-00327: descriptorType is
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER but pBufferInfo[0].buffer was
created with VK_BUFFER_USAGE_STORAGE_BUFFER_BIT.
Fix: Match buffer usage to descriptor type.
Printf Debugging¶
Some drivers support printf from shaders:
#extension GL_EXT_debug_printf : enable
void main() {
debugPrintfEXT("Invocation %u: value = %f\n",
gl_GlobalInvocationID.x, my_value);
}
Enable with:
Performance
Printf severely impacts performance. Use only for debugging specific issues.
Running the Example¶
Output from an Apple M3 Pro, abridged:
Validation layers: ENABLED
Debug utilities: AVAILABLE
=== Creating Named Resources ===
Named: CommandPool, Buffers, Memory
=== Setting Up GPU Timestamps ===
Timestamp period: 1.00 ns
=== Executing with Timestamps ===
=== Timing Results ===
CPU wall time: 4.245 ms
GPU total time: 0.416 ms
First dispatch: 0.264 ms
Barrier: 0.006 ms
Second dispatch: 0.145 ms
Throughput: 18.79 GB/s
=== Verification ===
Expected: input * 4 (doubled twice)
[0] 0.0 -> 0.0 (expected 0.0) [ok]
[1] 1.0 -> 4.0 (expected 4.0) [ok]
...
All 1048576 values verified correct
=== Common Debugging Scenarios ===
1. Validation layer messages appear above if there are issues
2. Object names help identify resources in error messages
3. Debug labels help track command buffer execution
4. Timestamps pinpoint performance bottlenecks
Chapter 10 completed!
A clean run prints no validation output
The sample does not deliberately trigger an error. If the validation layer
has nothing to report you will see no [Vulkan ERROR] lines at all — that
is the expected result. Note also the gap between CPU wall time (4.2 ms)
and GPU time (0.4 ms): most of the wall clock is submission and fence
wait, which is exactly the kind of thing timestamps expose.
Best Practices¶
During Development¶
- ✅ Always enable validation layers
- ✅ Name all objects
- ✅ Use debug labels for command sections
- ✅ Check return values
For Release¶
- ❌ Disable validation layers (performance)
- ✅ Keep error handling
- ✅ Add telemetry for crash reporting
Performance Debugging Checklist¶
- Are dispatches too small? Increase work per dispatch
- Too many barriers? Batch independent work
- Memory bandwidth bound? Use shared memory
- Occupancy limited? Reduce register usage
- Pipeline switching overhead? Batch by pipeline
Exercises¶
-
Custom Callback: Log validation messages to a file with timestamps.
-
Benchmark Suite: Create automated performance tests with timestamp queries.
-
Error Injection: Intentionally trigger different validation errors to learn the messages.
Congratulations!¶
You've completed the VKCompute tutorial series! You now understand:
- Vulkan initialization and device setup
- Buffer management and memory types
- Descriptor sets and resource binding
- Pipeline creation and management
- GPU synchronization
- Push constants and specialization
- Subgroup operations
- Debugging and profiling
Where to Go Next¶
- Vulkan Specification: The authoritative reference
- Vulkan Guide: Practical patterns and best practices
- GPU Gems: Advanced compute techniques
- Vendor Documentation: NVIDIA, AMD, Intel optimization guides
Happy computing! 🚀