A Ruby block, compiled into a Vulkan compute shader. An add-on for mruby-gpu-narray.
a=GPU::SFloat.new(1024).seqa.map{ |x| x * 2 + sin(x)}# this block becomes one compute shaderIn mruby-gpu-narray, every operator is its own dispatch. a * 2 + 1 - 3 + 4 builds
four command buffers, submits four times, and waits on a fence four times — and the
waiting dominates: a 1,048,576-element operation takes about as long as an 8-element one,
because almost none of the time is arithmetic.
Written as a block, the same expression becomes one shader, one submit, one wait.
| 1024 elements, Apple M5 | time |
|---|---|
a * 2 + 1 - 3 + 4 (4 dispatches) | 1.02–1.12 ms |
| one traced kernel (1 dispatch) | 0.20–0.23 ms |
| ~5× faster |
Adding more operators to the block does not make it slower — the cost was the round trips, not the arithmetic.
mruby has no RubyVM::AbstractSyntaxTree, and driving the mruby parser from C to read a
block is heavy. So the block is never parsed. It is called once, with a proxy object
in place of the data. Every operator applied to that proxy records itself and returns a
new node, so running the block builds an expression tree instead of computing anything.
GPU::Expr.wrap(GPU::KernelContext.new.instance_exec(GPU::Expr.new(:var,["x"])){ |x| x * 2 + 1})#=> (+ (* x 2.0) 1.0)That tree is printed as GLSL, handed to glslangValidator, and the resulting SPIR-V
becomes a compute pipeline. Nothing is hidden — you can read exactly what was generated:
putsGPU.kernel_source{ |x| x * 2 + sin(x)}#version450layout(local_size_x =256) in;
layout(set =0, binding =0) buffer BufA { float a[]; };
layout(set =0, binding =1) buffer BufB { float b[]; };
layout(push_constant) uniform PushConstants { uint n; } pc;
// generated by GPU.kernelvoid main() {
uint idx = gl_GlobalInvocationID.x;
if (idx >= pc.n) { return; }
float x = a[idx];
b[idx] = ((x *2.0) +sin(x));
}a.map{ |x| x * 2 + 1}# trace, compile, run -> a new GPU::SFloatk=GPU.kernel{ |x| x * 2}# a reusable kernel; identical source compiles oncek.call(a)k.glsl# the exact GLSL that was compiledGPU.kernel_source{ |x| x}# trace only, no compilation (tests, slides)Inside a block you have GLSL's element-wise built-ins under their GLSL names: sin,
cos, tan, asin, acos, atan, sinh, cosh, tanh, exp, exp2, log,
log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, radians,
degrees, min, max, pow, mod, step, clamp, mix, smoothstep.
- The block runs via
instance_exec— that is what lets you writesin(x)instead ofx.sin. Methods of the enclosing object are therefore not visible inside the block. Local variables still are. - Keep the expression on the left.
2 * xraisesTypeError, because mruby has no numeric coercion — the same rule the arrays themselves follow.
One input and one output, element-wise only. No control flow, no reductions, no
multi-array kernels. A shader that fails to compile raises GPU::CompileError carrying
glslang's own message.
- mruby-gpu-narray — pulled in automatically as a dependency via mgem-list.
- A Vulkan 1.1+ loader and a compute-capable device.
glslangValidatoronPATHat run time (override withGLSLANG=). The base gem needs it only at build time; this one shells out to it whenever a new block is traced.
The only link dependency is the Vulkan loader (-lvulkan).
Add just this gem — mruby-gpu-narray comes along as a dependency, resolved through
mgem-list:
MRuby::Build.newdo |conf|
toolchain:clang# :gcc on the Piconf.gembox'default'conf.gem'/path/to/mruby-gpu-kernel'# macOS only:conf.cc.include_paths << '/opt/homebrew/include'conf.linker.library_paths << '/opt/homebrew/lib'conf.enable_testendcd /path/to/mruby && MRUBY_CONFIG=/path/to/mruby-gpu-kernel/build_config.reference.rb rake
./build/host/bin/mruby /path/to/mruby-gpu-kernel/test/kernel_test.rb # ALL TESTS PASSED
./build/host/bin/mruby /path/to/mruby-gpu-kernel/examples/kernel_dsl.rbTo develop against a local checkout of the base gem instead of the published one, list
it before this gem in build_config.rb.
mrblib/gpu_kernel.rb Expr / KernelContext / GPU.kernel / NArray#map
│ block -> expression tree -> GLSL
▼
src/gpu_kernel.c GPU::Kernel: glslangValidator -> SPIR-V -> VkPipeline -> dispatch
│
├─ uses mruby-gpu-narray's g_ctx (device, queue, layouts, pools)
└─ uses its create_buffer / wrap_buffer / gpu_buffer_type
This gem compiles against the base gem's src/gpu_internal.h and links against symbols
it already exports, so the base gem needs no changes. The dispatch routine here is a
copy of the base gem's dispatch_compute(): upstream's version is keyed to its internal
PipeId enum and cannot bind a pipeline built elsewhere. When upstream grows a
pipeline-taking variant, this copy should go away.
- macOS — Apple M5 GPU via MoltenVK, Vulkan 1.1: 20/20 tests pass.
- Raspberry Pi 5 — not yet run.
MIT © 2026 Yuji Teshima