Oclgrind core — Simulation Engine
LLVM IR interpreter, OpenCL memory model, and plugin-driven instrumentation hub
💡 What is this & why does it exist?
Think of the core module as a flight simulator for GPU kernels. Just as a flight simulator recreates every control surface, weather system, and instrument reading so a pilot can train without risking an aircraft, the core module recreates every work-item thread, memory bank, and barrier synchronisation so a developer can test OpenCL kernels without needing actual GPU hardware — and with full visibility into what every instruction does.
At its heart sits an LLVM IR interpreter (WorkItem) that steps through compiled kernel bitcode instruction-by-instruction. Surrounding it are a faithful memory model with four address spaces, a barrier-aware work-group scheduler, and a plugin dispatch bus that fires callbacks on every memory access, instruction, and synchronisation event — enabling race detectors, uninitialised-read checkers, and custom analysis passes to observe execution without modifying the engine.
Table of Contents
Architecture Overview
The diagram below shows how the major classes compose into a simulation pipeline — from OpenCL source compilation through kernel dispatch to per-work-item execution.
flowchart TD
subgraph Compilation
SRC["OpenCL Source"] --> PRG["Program\n(Clang → LLVM IR)"]
PRG --> KRN["Kernel\n(argument binding)"]
end
subgraph Dispatch
QUE["Queue\n(command scheduling)"] --> KI["KernelInvocation\n(NDRange orchestrator)"]
KRN --> KI
end
subgraph Execution
KI --> WG["WorkGroup\n(barrier sync)"]
WG --> WI["WorkItem\n(IR interpreter)"]
end
subgraph MemoryModel["Memory Model"]
GLB["Global Memory\nAddrSpace 1"]
LOC["Local Memory\nAddrSpace 3"]
PRI["Private Memory\nAddrSpace 0"]
CON["Constant Memory\nAddrSpace 2"]
end
WI -->|load/store| GLB
WI -->|load/store| PRI
WG -->|async_copy| LOC
WI -->|read| CON
subgraph Instrumentation
CTX["Context\n(plugin hub)"]
PLG["Plugin (abstract)\n→ RaceDetector\n→ UninitChecker\n→ InstCounter"]
end
WI -.->|notifyInstruction| CTX
WG -.->|notifyBarrier| CTX
CTX -.->|dispatch| PLG
Key Classes
| Class / Struct | File | Role |
|---|---|---|
Context | Context.h/.cpp | Central hub — owns global memory, LLVM context, plugin registry; dispatches all notify* callbacks to registered plugins |
Context::Message | Context.h | Stream-style structured logging with indent control and work-item context injection |
Memory | Memory.h/.cpp | Typed address-space memory with buffer allocation, load/store, atomic ops, and address validation |
Memory::Buffer | Memory.h | Individual allocation unit — size, flags, raw data pointer |
WorkItem | WorkItem.h/.cpp | LLVM IR interpreter — steps one instruction at a time, maintains SSA value map, private memory, and call stack |
InterpreterCache | WorkItem.h | Per-kernel cache for builtin function look-ups, constant pre-computation, and SSA value IDs |
WorkGroup | WorkGroup.h/.cpp | Owns local memory, manages barrier synchronisation, async copies, and work-item scheduling within a group |
KernelInvocation | KernelInvocation.h/.cpp | NDRange orchestrator — partitions global work into work-groups, distributes to worker threads |
Kernel | Kernel.h/.cpp | Argument binding & metadata — wraps an llvm::Function with OpenCL-specific qualifiers and type info |
Program | Program.h/.cpp | Compilation pipeline — invokes Clang to compile OpenCL C → LLVM IR, applies optimisation passes, creates kernels |
Plugin | Plugin.h/.cpp | Abstract observer base — virtual hooks for instruction, memory, barrier, and lifecycle events |
Queue | Queue.h/.cpp | Command queue — enqueue/execute model for buffer read/write/copy/fill, kernel dispatch, map/unmap |
Command family | Queue.h | Polymorphic command structs (KernelCommand, BufferCommand, CopyCommand, FillBufferCommand, etc.) |
TypedValue | common.h | Size-tagged value container — supports float/int/pointer get/set, used throughout the SSA value map |
MemoryPool | common.h | Arena allocator for fast per-work-item allocation of instruction results without per-value free() |
Kernel Execution Flow
This sequence diagram traces the lifecycle of a single kernel dispatch — from enqueue through work-group barrier synchronisation to completion.
sequenceDiagram
participant Host
participant Q as Queue
participant KI as KernelInvocation
participant WG as WorkGroup
participant WI as WorkItem
participant Mem as Memory
participant Ctx as Context
participant Plg as Plugin
Host->>Q: enqueue(KernelCommand)
Q->>KI: KernelInvocation::run(context, kernel, dims)
KI->>WG: new WorkGroup(wgid)
WG->>WI: new WorkItem(lid)
loop Each instruction
WI->>WI: step() → execute(instruction)
WI->>Mem: load / store / atomic
WI->>Ctx: notifyInstructionExecuted()
Ctx->>Plg: instructionExecuted()
end
WI->>WG: notifyBarrier(fence)
Note over WG: All items reach barrier
WG->>WG: clearBarrier()
loop Resume after barrier
WI->>WI: step()
end
WI->>WG: notifyFinished()
WG->>Ctx: notifyWorkGroupComplete()
Ctx->>Plg: workGroupComplete()
KI->>Ctx: notifyKernelEnd()
Ctx->>Plg: kernelEnd()
Cross-Class Interactions
| Caller | Callee | Mechanism | Description |
|---|---|---|---|
WorkItem | Context | notifyInstructionExecuted() | Fires after every interpreted LLVM instruction with the result value |
WorkItem | Memory | load() / store() / atomic() | All data movement goes through the Memory abstraction matching the target address space |
WorkGroup | Context | notifyWorkGroupBarrier() | Signals barrier events so plugins can check for divergent barriers or deadlocks |
Context | Plugin | Virtual dispatch loop | Iterates m_plugins list, calling the matching virtual method on each registered plugin |
KernelInvocation | WorkGroup | Constructor + getNextWorkItem() | Creates work-groups, drives the round-robin scheduler that steps work-items |
Queue | KernelInvocation | KernelInvocation::run() | The queue's executeKernel() delegates to the static factory that builds and runs the invocation |
Program | Kernel | createKernel() | Extracts the named llvm::Function, wraps it in a Kernel with metadata |
Memory | Context | notifyMemoryLoad/Store() | Every read and write triggers a plugin callback through the context for instrumentation |
Deep-Dive Focus Pages
Directory Map
Configuration & Environment Variables
| Variable | Type | Default | Description |
|---|---|---|---|
OCLGRIND_INST_COUNTS | bool | 0 | Enable per-instruction execution counting at kernel end |
OCLGRIND_DATA_RACES | bool | 0 | Activate the data-race detection plugin |
OCLGRIND_UNINITIALIZED | bool | 0 | Detect reads from uninitialised memory |
OCLGRIND_INTERACTIVE | bool | 0 | Enable interactive GDB-style debugger for stepping through kernels |
OCLGRIND_PLUGINS | string | "" | Colon-separated list of shared-library plugin paths to load at start-up |
OCLGRIND_NUM_THREADS | int | 1 | Number of worker threads for parallel work-group execution |
OCLGRIND_QUICK | bool | 0 | Skip detailed memory-access checks for faster simulation |
OCLGRIND_BUILD_OPTIONS | string | "" | Extra Clang flags appended when compiling OpenCL programs |
OCLGRIND_PCH_DIR | path | install | Directory containing pre-compiled OpenCL header (.pch) |
OCLGRIND_DUMP_SPIR | bool | 0 | Dump SPIR bitcode to disk after compilation for debugging |
OCLGRIND_DISABLE_PCH | bool | 0 | Force full header compilation instead of using PCH (slower but useful for modified headers) |
OCLGRIND_MAX_ERRORS | int | 1000 | Cap on the number of error messages before suppression |
OCLGRIND_GLOBAL_MEM_SIZE | int (MB) | 1024 | Maximum total global memory available to allocations |
OCLGRIND_LOCAL_MEM_SIZE | int (KB) | 32 | Maximum local memory per work-group |
Code Examples
Plugin Observer Interface
The Plugin base class defines virtual hooks that any analysis pass can override. The Context iterates registered plugins and calls these on every simulation event:
class Plugin
{
public:
Plugin(const Context* context);
virtual ~Plugin();
virtual void instructionExecuted(const WorkItem* workItem,
const llvm::Instruction* instruction,
const TypedValue& result) {}
virtual void memoryLoad(const Memory* memory, const WorkItem* workItem,
size_t address, size_t size) {}
virtual void memoryStore(const Memory* memory, const WorkItem* workItem,
size_t address, size_t size,
const uint8_t* storeData) {}
virtual void memoryAtomicLoad(const Memory* memory, const WorkItem* workItem,
AtomicOp op, size_t address, size_t size) {}
virtual void workGroupBarrier(const WorkGroup* workGroup, uint32_t flags) {}
virtual void kernelBegin(const KernelInvocation* kernelInvocation) {}
virtual void kernelEnd(const KernelInvocation* kernelInvocation) {}
virtual bool isThreadSafe() const;
protected:
const Context* m_context;
};
TypedValue — Size-Tagged Data Container
Every SSA value produced by the interpreter is wrapped in a TypedValue. The struct provides typed accessors for the raw byte buffer:
struct TypedValue
{
unsigned size; // bytes per element
unsigned num; // number of vector elements (1 for scalar)
unsigned char* data; // raw byte storage
double getFloat(unsigned index = 0) const;
size_t getPointer(unsigned index = 0) const;
int64_t getSInt(unsigned index = 0) const;
uint64_t getUInt(unsigned index = 0) const;
void setFloat(double value, unsigned index = 0);
void setPointer(size_t value, unsigned index = 0);
void setSInt(int64_t value, unsigned index = 0);
void setUInt(uint64_t value, unsigned index = 0);
struct TypedValue clone() const;
};
Address Space & Atomic Enumerations
The four OpenCL address spaces and supported atomic operations are defined as enums in common.h:
enum AddressSpace
{
AddrSpacePrivate = 0, // per-work-item scratch
AddrSpaceGlobal = 1, // shared across all work-items
AddrSpaceConstant = 2, // read-only global
AddrSpaceLocal = 3, // shared within a work-group
};
enum AtomicOp
{
AtomicAdd, AtomicAnd, AtomicCmpXchg,
AtomicDec, AtomicInc, AtomicMax,
AtomicMin, AtomicOr, AtomicSub,
AtomicXchg, AtomicXor,
};