Vx 0.0.1: placement in the type system

A machine is no longer one processor. It is a CPU, a GPU, sometimes a neural accelerator, and half a dozen memory spaces with different capacities, different bandwidths, and different rules about who is allowed to read them. Most languages treat all of that as infrastructure. Vx treats it as types.

The machine has a shape

A single node, drawn as the hardware actually arranges it:

      CPU                            GPU
 +---------------+           +-----------------------+
 |               |   PCIe    |                       |
 |   CPU_DRAM    | <=======> |        GPU_HBM        |
 |    128 GiB    |  63 GB/s  |  80 GiB @ 3.35 TB/s   |
 |               |           |                       |
 +---------------+           |  +-----------------+  |
                             |  |      SMEM       |  |
                             |  |   228 KiB / SM  |  |
                             |  |    128 B/cyc    |  |
                             |  +-----------------+  |
                             +-----------------------+

Three memory spaces, three capacities, three very different bandwidths, and one edge between them that costs something to cross. SMEM is not a separate pool sitting beside HBM — it is a small, fast region inside it, private to one streaming multiprocessor, and there are a hundred and thirty-two of them. The ratios are what matter: SMEM is roughly four thousand times smaller than HBM, and the link to the host is fifty times slower than HBM itself.

Every one of those numbers is in a vendor datasheet. None of them are in a type.

In C++ all three spaces produce the same thing: float *. SYCL and Kokkos type which region within a device — registers, scratchpad, DRAM — but neither types which device, so two allocations on two GPUs are indistinguishable to the type system.

That is a ceiling rather than an oversight: a library can only type what a template parameter carries, and where code runs is not part of a C++ function's type.

So the structure above exists in the hardware, in the documentation, and in the programmer's head. The one place it does not exist is the program.

Putting it in the type

A tensor's type carries its element type, its shape, and the memory space it lives in. A spawn on block names the device it runs on. Here is a matmul on an accelerator, written out in full:

// Two matrices already resident in NPU memory.
fn custom_matmul(
    a: Pinned<Tensor<f32, [4, 4]>, Topology::NPU[0]>,
    b: Pinned<Tensor<f32, [4, 4]>, Topology::NPU[0]>)
    -> Verified<Tensor<f32, [4, 4], Memory::NPU_HBM>> {

    let mut result =
        Tensor<f32, [4, 4], Memory::NPU_HBM>::uninit();

    // Dispatch the computation to the accelerator.
    spawn on(Topology::NPU[0]) {
        for i in 0..4 {
            for j in 0..4 {
                result[i][j] = 0.0;
                for k in 0..4 {
                    result[i][j] += a[i][k] * b[k][j];
                }
            }
        }
    }

    return Verified(result);
}

Read the signature and the whole placement story is there. Pinned<T, Topology::NPU[0]> says both operands already live where the accelerator can reach them, so the function cannot be called with a host tensor by accident. The return type says the result is in NPU_HBM, so a caller that wants it on the host has to say so. And Verified<T> carries the fact that the value has been checked, in the type, rather than in a comment.

None of that is a runtime check. It is the signature.

What happens when the placement is wrong

Take the same idea and forget one transfer. The compiler names the space the value is in, the spaces the device can see, and the fix:

Error[E6003] at 13:9: 'a' lives in CPU_DRAM but NPU[0] sees only [NPU_HBM];
insert an explicit transfer to NPU_HBM (cost 50 on the declared path)

That last clause — cost 50 on the declared path — is there because the compiler also knows what the move costs, over the machine you described to it. The same declaration that makes the error possible is what prices the fix.

What it checks today

These are implemented, have diagnostic codes, and are held by tests:

CheckRules out
Address-space visibility (E6003)A device reading a space it cannot address; a host reading device memory
Capacity admission (E6009, E6010)A tensor larger than its space; a working set that fits tile by tile and not together
Transfer reachability (E6002)A move between spaces with no declared path
Borrow checkingAliasing and lifetime errors, with region tracking
Linear typesUse after move of a consumed buffer
Seam contractsReading a buffer whose asynchronous transfer is not yet visible; discharged by z3

The machine itself is a source file. fleet/ holds twelve of them — A100 in both memory sizes, H100, H200, B200, MI300X, an Apple M4, multi-GPU nodes — each eight to twenty-four declaration lines, each citing its sources and marking unverified figures as unverified.

It is a cross compiler

The machine Vx targets is declared, not detected. --host names the CPU and --machine fleet/<sku>.vx names the accelerator, so the compiler is not limited to the box it is running on. A100 binaries get built on an x86 EC2 instance and shipped to the GPU machine.

This matters more than it sounds. A previous --target flag was removed precisely because it stamped one triple across a whole module, including main — which produced modules that were wrong about the machine they named. Declaring the host and the device separately is what replaced it.

What is not built

This is 0.0.1, and the number is meant literally.

0.1 is being saved for the release that has a standard library worth the name. Until then the version stays in 0.0.x, where it can move as often as it needs to.

Trying it

curl -fsSL https://vxlang.org/install.sh | sh

macOS on Apple Silicon and Linux x86_64. LLVM 22 has to be present — Vx lowers through MLIR and drives mlir-translate, opt, llc and clang from it, and a different major version will not do, because the MLIR C API changes between releases. The installer checks before it downloads anything and tells you the exact command if something is missing.

The language tour is the fastest way to see whether the idea appeals. The documentation covers the type system, ownership, the heterogeneous model and machine files, and every code example in it is compiled by CI — a previous audit found a tutorial teaching syntax that had never been implemented, so now the build fails rather than the reader.

Vx is Apache 2.0 with the LLVM exception. Bug reports are the most useful thing you can send: github.com/vx-lang/Vx/issues. If you want to work on the compiler, the issues labelled good first issue each state the problem with a reproduction you can run.