Toolchain these notes describe: mojo ==1.2.0.dev2026092605 (set in MOJO_PIN in
bin/port.sh; every repo receives this file with the marker already substituted).
Every claim below was checked by compiling it. bin/probe-confirm.py and
bin/probe-hints.py in the factory regenerate the list; re-run them after any
toolchain bump rather than trusting this file.
MODULAR_HOMEmust point at the env'sshare/max. Without it every import fails withunable to locate module 'std', which looks like a missing package but is not.pixi runsets it for you. A baremojo builddoes not:export MODULAR_HOME="$(dirname "$(dirname "$(which mojo)")")/share/max"
mojo build -otakes an output FILE path, not a directory. A directory there fails late, at link time, withld: cannot open output file ... Is a directory.
fnhas been removed. Usedeffor everything.fnis a hard error:'fn' has been removed; use 'def' instead.UnsafePointeris deprecated in favour ofPointer. It still compiles, with a warning, so old code builds — but writePointer[T, AnyOrigin[mut=True]].int(x)/float(x)are not builtins. Use the type as a constructor:Int(x),Float64(x). There is no.to_int(),.int(), orround(x).to_int().simdwidthof[DType.float64]()is gone. It is nowfrom std.sys import simd_width_ofthensimd_width_of[DType.float64](). A hardcodedcomptime W = 4is always valid and is often the better choice.SIMDhas no.min()/.max()methods. The freemin(a, b)/max(a, b)work on both scalars andSIMDvalues.
@export("symbol_name")sits on the line above the def. The ABI is an effect before the arrow:def f(a: Int) abi("C") -> Float64:.@exportnow REQUIRES an explicitabi(). Omitting it is an error:@export requires an explicit 'abi()' effect on the function. This is stricter than 1.0, where it only warned.- An
abi("C")function may not beraises. Put the fallible work in atry:/except:inside the body instead. @exportrejects parametric functions, including an inferred pointer origin (Pointer[Float64, _]). Annotate the origin explicitly.- Buffers cross the C ABI as
Intaddresses, rebuilt inside the wrapper:var q = Pointer[Float64, AnyOrigin[mut=True]](unsafe_from_address=addr)
- Pointers are NON-NULLABLE: constructing one from address 0 fails a compile-time
constraint. Take
Intand construct inside the branch that uses it. AnyOrigin[mut=True]is the only usable mutable origin name.MutableAnyOriginand friends do not exist.
comptime Ptr = Pointer[Float64, AnyOrigin[mut=True]]
comptime W = simd_width_of[DType.float64]()
var i = 0
while i + W <= n: # vector body
p.store(i, p.load[width=W](i) * 2.0)
i += W
while i < n: # scalar tail
p.store(i, p.unsafe_load(i) * 2.0)
i += 1p.load[width=W](i)/p.store(i, v)/v.reduce_add()all work.p.unsafe_load(i)for a scalar load;p.unsafe_offset(i)[]also works.- Positional
p[i]still compiles but warns; preferunsafe_load.
parallelize is NOT gone. It moved package. On 1.2.0:
| import | result on 1.2.0 |
|---|---|
from max.algorithm import parallelize |
WORKS — this is the live path |
from max.algorithm import sync_parallelize |
WORKS |
from std.algorithm import parallelize |
package 'algorithm' does not contain 'parallelize' |
from std.algorithm.functional import parallelize |
gone |
from std.sys import parallelize / spawn |
package 'sys' does not contain ... |
from std.threading import ... |
unable to locate module 'threading' |
from std.algorithm import sort |
package 'algorithm' does not contain 'sort' |
std.algorithm no longer exporting sort is the tell: the std module was gutted
while the capability moved to max. So a port is parallelisable — just not through
the old import. Verified by building mojo-anndata on 1.2.0: its
from max.algorithm import parallelize compiles and the port builds.
max.parallel and max.core do NOT exist; max.algorithm is the one.
Do NOT write from std.algorithm import parallelize — it does not compile. If a
port is serial where it could be parallel, that is a real gap, not a toolchain
excuse; use the max form behind a size threshold and prove it with the benchmark.
Same story as parallelism. The std.gpu module is gone entirely
(unable to locate module 'gpu'), so from std.gpu import thread_idx no longer
resolves — but the capability is not gone with it:
from max.gpu import block_idx, thread_idx # WORKS
from max.gpu.host import DeviceContext # WORKSand the full host surface is present and compiles on 1.2.0:
ctx.enqueue_create_buffer[DType.float64](count)ctx.enqueue_copy(dst, src)ctx.enqueue_function[kernel](...)ctx.synchronize()ctx.name()
DeviceContext() construction needs a raises context (def main() raises:), and
DeviceContext is not Writable, so do not print it. There is no
ctx.devices(); enumerate through the max.gpu module instead.
Verified end to end: mojo-numba and mojo-imagehash carry working max.gpu.host
GPU paths and both build on 1.2.0.
from std.memory import stack_allocationresolves, but the oldstack_allocation[T](n)form does NOT: it fails withno matching function in call to 'stack_allocation'. Check the signature by compiling rather than assuming the historic shape.
Practical rule: a GPU path is only worth shipping if you can compile AND run it,
and the rules in prompts/accel.md about launch count, single copy-in/copy-out and
tail coverage all still apply. The GPU is shared with production workloads — see
the memory limits there. A CPU-only port is a legitimate answer when a kernel is
below roughly 2 flops per byte, but "the host API is missing" is NOT a valid
reason: it is in max, and 28 ports already use it.
These were each verified by compiling and running, across several ports. They are the difference between a one-line import fix and an afternoon.
The max conda package is required. max.mojoc / algorithm.mojoc ship in
<prefix>/lib/mojo from the max package, not from mojo. Without it the
max.* imports fail with unable to locate module 'max'. Add it beside the mojo
pin; it is released in lockstep, so mojo ==X goes with max ==matching-version:
[dependencies]
mojo = "==1.2.0.dev2026092605"
max = "==26.7.0.dev2026092605"parallelize signature changed. The bracket form no longer type-checks:
parallelize(func, num_work_items, num_workers[, ctx])— WORKSparallelize(func, num_work_items[, ctx])— WORKSparallelize[func](n)— does NOT type-check; the[func]form now binds the origins overload.
The worker must be a plain def, not a @parameter closure. func has to
convert to def(Int) -> None, so a @parameter def work(i: Int) is rejected as
capturing thin. Use a nested def work(i: Int) {imm}:. Nested {imm} calling
nested {imm} also works, which the chunked variants need.
Pass an explicit worker count. The 2-arg form crashed worker launch when
called from a ctypes host. sync_parallelize segfaulted from a ctypes host
entirely; prefer parallelize with an explicit count, and gate it behind a size
threshold.
max.gpu exports block_dim, block_idx, thread_idx, global_idx.
DeviceContext() construction needs a raises context, the value is not
Writable (do not print it), and there is no ctx.devices().
One trap worth naming: max's caching memory allocator can request a very large
chunk (mojo-eigen's batched GPU SVD asks for 16.75 GiB and gets
CUDA_ERROR_OUT_OF_MEMORY) and a broad except will swallow that into a silent
CPU fallback. If a GPU path "works" but never reports used_gpu=1, it is probably
running on the CPU — check rather than assume.
- Build cost is ~2-5s and essentially FIXED regardless of function count. Batch many functions into ONE compilation unit rather than compiling files separately.
mojo runJITs in ~1.2s per invocation; a built shared lib + ctypes call is ~0.9us.mojo build --emit shared-liberrors if the file definesmain.
Do not guess module paths. A bare unknown name makes the compiler name the module it expected:
def _p() -> Int:
return simd_width_of[DType.float64]() # -> "did you mean to import it from 'std.sys'?"No hint means the symbol is gone, not relocated. bin/probe-hints.py automates this.