The promise of Vx
TL;DR — The computer on your desk has many cores, memory in banks and a GPU. The apps you download use little of it, because most of them were written for one core, long before LLMs could help anyone write code. The promise of Vx is that you write the small app you need, and the compiler spreads it over the machine you have: its cores, its memories and its GPU. This post is about where Vx is going. A table near the end says what works today and what does not.
A laptop from this year has more parallel hardware than a supercomputer had thirty years ago. Most of the time, almost all of it does nothing.
The computer on your desk
Look at what a normal desktop or laptop has now:
- Many cores. Eight, twelve, sixteen or more, often of two kinds: fast cores and cores that use less power.
- Memory in parts. A large workstation has NUMA nodes: each group of cores has memory that is near to it and memory that is far away. A small machine has the same idea in smaller form, as banks and channels in the memory chips. Where the data is changes how fast a program runs.
- A GPU. Thousands of small lanes that do arithmetic in parallel. On a Mac it shares one memory with the CPU. On most PCs it has a memory of its own.
Now look at what uses it. A photo tool that resizes a folder of pictures one at a time. A script that reads a large CSV on one core while fifteen other cores wait. A video tool that uses the GPU for one filter and the CPU for the rest. The hardware is there. The software does not know about it.
Why the apps you download do not use it
It is not because their authors were careless. Most of these apps were written years ago, and the code that does the work is older still. Making old code use many cores is hard: shared data needs locks, and a missing lock gives a bug that shows up once a week. Making it use the GPU is harder: that is a second language, a second toolchain and a second copy of the data, often for one vendor only. And the work is never done, because the next machine has a different number of cores and a different GPU.
So most apps choose one core, or one core and one fixed GPU path, and the rest of the machine stays idle.
The promise
Two things changed. First, you can now write your own small app. An LLM assistant can write a first version of the program you need in an afternoon: the folder resizer, the log searcher, the simulation for a class. You do not have to wait for someone to ship it.
Second, a language can know about the machine. In Vx, the cores, the memories and the devices are part of the program. You say what can run in parallel, and where. The compiler checks that it is safe and generates the code for each device. That is the promise of Vx:
- You write the app once, in one language, for the CPU and the GPU together.
- The compiler checks the parallel parts. Two lanes that can write the same element, a task that keeps a borrow too long, a tensor that does not fit in a memory: the compiler refuses them before the program runs. This matters even more when an assistant writes the first version, because the mistakes of parallel code do not show up in a quick test.
- The program uses the machine it runs on. A machine file describes the cores, the memories and the GPU. The same source runs on another machine with a different file.
Three small programs
Use two cores
This program adds the numbers 1 to 100 with two tasks. Each task runs on a thread of its own
and borrows the vector from main. The tasks block ends only after both
tasks end, so the compiler knows the borrow is safe. No locks are needed.
import std::vec;
fn sum_range(v : &Vec<i64>, from : i32, to : i32) -> i64 {
let mut sum : i64 = 0;
for i in from..to {
sum = sum + *v.get_ref(i);
}
return sum;
}
fn main() -> i32 {
let mut v = Vec<i64>::new();
for i in 1..101 {
v.push(i as i64);
}
let total = tasks {
let low = spawn {
sum_range(&v, 0, 50)
};
let high = spawn {
sum_range(&v, 50, 100)
};
low.join() + high.join()
};
println!("total {}", total);
return 0;
}
Change sum_range to resize a picture, and the two tasks resize two halves of a
folder.
Use the GPU
This program blurs each row of a 1920 x 1080 image. spawn on(Topology::Device)
puts the region on the GPU. Each row is a block, and each pixel of a row is a lane. The
compiler proves that no two lanes write the same pixel.
pub fn blur_rows(dst : &mut Tensor<f32, [?, ?], Memory::HBM>, src : &Tensor<f32, [?, ?], Memory::HBM>, h : i32, w : i32) -> void {
spawn on(Topology::Device) {
for y in 0..h across blocks {
for x in 0..w across lanes(256) {
let mut acc : f32 = 0.0;
for d in 0..9 {
let mut xx = x + d - 4;
if xx < 0 {
xx = 0;
}
if xx > w - 1 {
xx = w - 1;
}
acc = acc + src[y][xx];
}
dst[y][x] = acc / 9.0;
}
}
}
return;
}
fn main() -> i32 {
let src = transfer(Tensor<f32, [1080, 1920]>::fill(1.0), Memory::HBM);
let mut dst = transfer(Tensor<f32, [1080, 1920]>::fill(0.0), Memory::HBM);
blur_rows(&mut dst, &src, 1080, 1920);
let out = transfer(dst, Memory::CPU_DRAM);
println!("{}", out[540][960]);
return 0;
}
On a MacBook with an M4, the compiler writes the kernel in Metal Shading Language, and the blur runs on the Apple GPU. On a Linux machine with an NVIDIA GPU, the same source becomes PTX. On a machine with no GPU, the same kernel runs on the CPU and prints the same answer.
vxc blur.vx --host default --machine fleet/m4-uma.vx --run
Put the data near the cores
On a machine with two NUMA nodes, the machine file names each node and its memory. When a program places a tensor in the memory of node 1, Vx puts its pages there and runs the work that reads it on the cores of node 1. On a two-socket Xeon we measured 1.34 times the bandwidth of the best placement that needs no change to the program, and 2.5 times that of the default placement (NUMA and the host's memory domains). The program does not change for a machine with one node. Only the machine file changes.
What works today
This post is about a promise, so here is the honest part.
| Machine | Today |
|---|---|
| Mac with Apple silicon | Works. Kernels run on the Apple GPU through Metal. Matmuls go to Metal Performance Shaders and Core ML. |
| Linux with an NVIDIA GPU | Works. Kernels become PTX, and matmuls go to cuBLAS. Tiles of bf16 and f16 are starting to use the tensor cores. |
| Linux on a multi-socket machine | Works. Tensors are placed on NUMA nodes. The CPU copy of a kernel still runs on one thread. |
| Linux with an AMD GPU | Not yet. A machine file exists. The backend does not. |
| Linux with an Intel GPU | Not yet. A contributor is planning a SPIR-V backend. |
| Windows | Not yet. The compiler does not build or run on Windows. |
The language does not change from one row to the next. A new row is a new backend in the compiler and a new machine file, not a new language. That is why the promise covers Windows and AMD too: the program you write today on a Mac is the program that will run there.
Try it
Install Vx, then write the program you wished an app did for you. If you use an assistant, give it the language tour first. Tell us what you made, and what the compiler refused, on r/vxlang.