NumLeanOpenCL is an experimental Lean 4 package for running Float32 array computations on OpenCL devices while keeping a Lean-facing API and specification model.
The project currently focuses on an OpenCL-backed OpenCLFloat32Array, a small kernel-expression DSL, automatic compilation of selected Lean expressions to OpenCL kernels, runtime tests, and profiling tools for understanding queue scheduling and kernel execution.
- Lean 4 via
elan, using the version inlean-toolchain. - Lake.
- OpenCL headers and an OpenCL runtime/ICD.
libOpenCL.soavailable at the path configured inlakefile.lean.
On Linux, this usually means installing your GPU vendor OpenCL runtime or a CPU runtime such as POCL, plus OpenCL development headers.
Build the default executable:
lake buildRun the runtime tests:
lake testThe current mapFinIdx implementation has a known sorry warning because @[implemented_by] does not currently work with the required autoParam setup. The tests are expected to pass despite that warning.
Initialize OpenCL once before using the runtime-backed operations:
import NumLeanOpenCL
open NumLean
def main : IO Unit := do
NumLean.initOpenCLContext
let xs := OpenCLFloat32Array.ofArray #[Float32.ofBits 0x3f800000, Float32.ofBits 0x40000000]
let ys := xs.scal (Float32.ofBits 0x40000000)
IO.println ys.toArrayPrefer OpenCLFloat32Array.ofArray for input creation. It performs one bulk upload from a Lean Array Float32 into an OpenCL buffer. Do not build large OpenCL arrays with repeated push; that creates many tiny operations and makes profiling/scheduling data misleading.
The profiling API is in NumLeanOpenCL/OpenCL/Basic.lean.
NumLean.startOpenCLProfiling : IO Unit
NumLean.stopOpenCLProfiling : IO Unit
NumLean.clearOpenCLProfile : IO Unit
NumLean.markOpenCLProfile : @& String → IO Unit
NumLean.dumpOpenCLProfile : IO StringTypical usage:
NumLean.clearOpenCLProfile
NumLean.markOpenCLProfile "profile/start"
let xs := OpenCLFloat32Array.ofArray hostInput
NumLean.markOpenCLProfile s!"after/upload/{xs.size}"
let ys := xs.scal alpha
NumLean.markOpenCLProfile s!"after/scal/{ys.size}"
let out := ys.toArray
NumLean.markOpenCLProfile s!"after/readback/{out.size}"
IO.println (← NumLean.dumpOpenCLProfile)clearOpenCLProfile clears old records and starts a new capture. markOpenCLProfile inserts a marker into the OpenCL command queue. Because Lean expressions can be lazy or hoisted if they are pure, marker labels should depend on values produced by the stage being marked, such as xs.size or out.size. This forces the relevant computation before the marker is enqueued.
dumpOpenCLProfile prints a CSV-like table. Device rows come from OpenCL event profiling. Host rows use a monotonic CPU clock and currently cover kernel compilation/build work such as:
compile/blas1compile/mapUnsafecompile/mapInContextUnsafe
The output columns are:
idx,kind,label,work_items,bytes,queued_us,submit_us,start_us,end_us,queue_to_submit_us,submit_to_start_us,duration_us,host_start_us,host_end_us,host_duration_us
For kind = device, the timing columns are OpenCL event profiling timestamps relative to the first event. For kind = host, the host timing columns describe CPU-side work such as compilation, and the device timing columns are filled to make the row visible in the same timeline.
Build the profiling workload:
lake build opencl_profileRun it with runtime arguments:
./.lake/build/bin/opencl_profile 65536 3Arguments:
- first argument: array size, default
65536 - second argument: input salt, default
0
The runtime arguments are intentional: they prevent the workload from being a closed compile-time constant and make the profiling data easier to trust.
Example workflow:
./.lake/build/bin/opencl_profile 6553600 2 > profile.txtThe static viewer is in:
profile-viewer/index.html
Open it in a browser and paste the output of dumpOpenCLProfile, or load a saved profile text file.
Viewer features:
- sortable event table
- horizontal scheduling timeline
- time axis above the timeline
- drag-to-select zoom on a subinterval
- double-click or
Reset Zoomto zoom out - filtering by
kind(all,device,host) - label filtering, for example
kernel,compile,write/, orread/ - idle-gap compression to make dense kernel clusters visible
- separate colors for queue delay, device wait, execution, host compilation, and transfers
Useful ways to inspect a profile:
- Use
Kind = deviceto focus on OpenCL queue behavior. - Use
Label Filter = kernelto focus on kernels. - Use
Label Filter = write/orread/to inspect upload/readback. - Enable
Compress idle gapswhen long host-side gaps make short kernel execution bars hard to see. - Drag around a dense cluster to zoom into scheduler behavior.
For device rows:
queued_us: when the command was queued by the host.submit_us: when the command was submitted to the device.start_us: when the device started executing the command.end_us: when the device finished the command.queue_to_submit_us: host/driver queue delay.submit_to_start_us: device-side wait, often because earlier commands are ahead in an in-order queue.duration_us: actual command execution time.
Large submit_to_start_us values for later kernels are normal on an in-order queue if many commands were queued quickly. The kernels may all be submitted near the same moment but execute sequentially.
Uploads and readbacks can dominate total time for large arrays. Host-side construction or conversion of Lean arrays can also create gaps that are not OpenCL device execution; use markers and host compile rows to distinguish those effects.
- Keep OpenCL input construction bulk-oriented with
ofArray. - Use named
mapFinIdxdefinitions when elaboration of inline lambdas becomes fragile. - Avoid profiling closed, pure workloads if you want to reason about runtime ordering; depend on runtime arguments and force stage results in marker labels.
- The profiling viewer is intentionally static and has no build step.