Oclgrind core — Simulation Engine

LLVM IR interpreter, OpenCL memory model, and plugin-driven instrumentation hub

21
Source Files
~12k
Lines of C++
10
Key Classes
4
Address Spaces
11
Atomic Ops
C++14 LLVM IR Simulation Core Stable OpenCL 1.2 / 2.0

💡 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 / StructFileRole
ContextContext.h/.cppCentral hub — owns global memory, LLVM context, plugin registry; dispatches all notify* callbacks to registered plugins
Context::MessageContext.hStream-style structured logging with indent control and work-item context injection
MemoryMemory.h/.cppTyped address-space memory with buffer allocation, load/store, atomic ops, and address validation
Memory::BufferMemory.hIndividual allocation unit — size, flags, raw data pointer
WorkItemWorkItem.h/.cppLLVM IR interpreter — steps one instruction at a time, maintains SSA value map, private memory, and call stack
InterpreterCacheWorkItem.hPer-kernel cache for builtin function look-ups, constant pre-computation, and SSA value IDs
WorkGroupWorkGroup.h/.cppOwns local memory, manages barrier synchronisation, async copies, and work-item scheduling within a group
KernelInvocationKernelInvocation.h/.cppNDRange orchestrator — partitions global work into work-groups, distributes to worker threads
KernelKernel.h/.cppArgument binding & metadata — wraps an llvm::Function with OpenCL-specific qualifiers and type info
ProgramProgram.h/.cppCompilation pipeline — invokes Clang to compile OpenCL C → LLVM IR, applies optimisation passes, creates kernels
PluginPlugin.h/.cppAbstract observer base — virtual hooks for instruction, memory, barrier, and lifecycle events
QueueQueue.h/.cppCommand queue — enqueue/execute model for buffer read/write/copy/fill, kernel dispatch, map/unmap
Command familyQueue.hPolymorphic command structs (KernelCommand, BufferCommand, CopyCommand, FillBufferCommand, etc.)
TypedValuecommon.hSize-tagged value container — supports float/int/pointer get/set, used throughout the SSA value map
MemoryPoolcommon.hArena 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

CallerCalleeMechanismDescription
WorkItemContextnotifyInstructionExecuted()Fires after every interpreted LLVM instruction with the result value
WorkItemMemoryload() / store() / atomic()All data movement goes through the Memory abstraction matching the target address space
WorkGroupContextnotifyWorkGroupBarrier()Signals barrier events so plugins can check for divergent barriers or deadlocks
ContextPluginVirtual dispatch loopIterates m_plugins list, calling the matching virtual method on each registered plugin
KernelInvocationWorkGroupConstructor + getNextWorkItem()Creates work-groups, drives the round-robin scheduler that steps work-items
QueueKernelInvocationKernelInvocation::run()The queue's executeKernel() delegates to the static factory that builds and runs the invocation
ProgramKernelcreateKernel()Extracts the named llvm::Function, wraps it in a Kernel with metadata
MemoryContextnotifyMemoryLoad/Store()Every read and write triggers a plugin callback through the context for instrumentation

Deep-Dive Focus Pages

Directory Map

src/core/ ├── common.h Shared types: Size3, TypedValue, enums, MemoryPool ├── common.cpp Utility functions, constant resolution, GEP helpers ├── Context.h Context class + Message inner class ├── Context.cpp Plugin loading, notify* dispatch implementation ├── Memory.h Memory class + Buffer struct ├── Memory.cpp Buffer alloc/dealloc, load/store, atomic ops ├── WorkItem.h WorkItem class + InterpreterCache + BuiltinFunction ├── WorkItem.cpp LLVM IR instruction dispatch (45+ opcodes) ├── WorkItemBuiltins.cpp OpenCL built-in function implementations ├── WorkGroup.h WorkGroup class, AsyncCopy, Barrier structs ├── WorkGroup.cpp Barrier synchronisation, async copy execution ├── KernelInvocation.h KernelInvocation class ├── KernelInvocation.cpp NDRange partition, multi-threaded worker dispatch ├── Kernel.h Kernel class — argument metadata wrapper ├── Kernel.cpp Argument binding, qualifier queries ├── Program.h Program class — compilation pipeline ├── Program.cpp Clang invocation, bitcode I/O, dead-code pruning ├── Plugin.h Plugin abstract base class ├── Plugin.cpp Default isThreadSafe() implementation ├── Queue.h Queue + Command hierarchy (13 command types) ├── Queue.cpp Command enqueue/execute, buffer ops └── gen_opencl-c.h.cmake CMake template for generating opencl-c.h header

Configuration & Environment Variables

VariableTypeDefaultDescription
OCLGRIND_INST_COUNTSbool0Enable per-instruction execution counting at kernel end
OCLGRIND_DATA_RACESbool0Activate the data-race detection plugin
OCLGRIND_UNINITIALIZEDbool0Detect reads from uninitialised memory
OCLGRIND_INTERACTIVEbool0Enable interactive GDB-style debugger for stepping through kernels
OCLGRIND_PLUGINSstring""Colon-separated list of shared-library plugin paths to load at start-up
OCLGRIND_NUM_THREADSint1Number of worker threads for parallel work-group execution
OCLGRIND_QUICKbool0Skip detailed memory-access checks for faster simulation
OCLGRIND_BUILD_OPTIONSstring""Extra Clang flags appended when compiling OpenCL programs
OCLGRIND_PCH_DIRpathinstallDirectory containing pre-compiled OpenCL header (.pch)
OCLGRIND_DUMP_SPIRbool0Dump SPIR bitcode to disk after compilation for debugging
OCLGRIND_DISABLE_PCHbool0Force full header compilation instead of using PCH (slower but useful for modified headers)
OCLGRIND_MAX_ERRORSint1000Cap on the number of error messages before suppression
OCLGRIND_GLOBAL_MEM_SIZEint (MB)1024Maximum total global memory available to allocations
OCLGRIND_LOCAL_MEM_SIZEint (KB)32Maximum 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,
};