Run a MAX graph on CPU and GPU
Compile a small MAX 26.5 graph and check its output on a WendyOS device
MAX is Modular's platform for running AI models. It compiles computations for supported CPUs and GPUs and provides a server for exposing models through an API. On WendyOS, it lets you run inference locally on your device.
MAX's Python graph API describes the computation, while its runtime handles compilation and execution. This is useful when building a custom model or checking how an operation behaves on different processors. In this tutorial, Python defines relu(lhs + rhs), MAX runs it, and NumPy checks the result. No model weights or account are needed. See Modular's model development overview for how graphs fit into larger models.
Start with the CPU path, then run the same computation on a supported NVIDIA GPU. Selecting gpu explicitly fails if MAX cannot find an accelerator; the example never silently switches to CPU.
Prerequisites
- Install the Wendy CLI and start Docker on your development machine.
- Connect a 64-bit Linux WendyOS device with a supported ARM64 or x86-64 CPU over USB or your network. See CPU requirements.
- Ensure your development machine has internet access to build the container.
- For the GPU step, check your board and driver against the MAX requirements and Mojo GPU requirements.
The image pins MAX 26.5.0 and NumPy 2.4.3 on Python 3.12. You do not need to install MAX on the device host or on your development machine.
Use your default Wendy device. Create a project folder named max-graph and run the commands below from that folder.
For GPU execution, run wendy device info to inspect the GPU and CUDA version; an Orin Nano reports sm_87.
Define and verify the graph
Create graph.py:
import os
from importlib.metadata import version
import numpy as np
from max.driver import Accelerator, Buffer, CPU, accelerator_count
from max.dtype import DType
from max.engine import InferenceSession
from max.graph import Graph, TensorType, ops
def main():
target = os.environ.get("MAX_DEVICE", "cpu")
if target == "cpu":
device = CPU()
elif target == "gpu":
if accelerator_count() == 0:
raise RuntimeError("MAX_DEVICE=gpu requires a supported GPU and Wendy's gpu entitlement")
device = Accelerator(0)
else:
raise ValueError("MAX_DEVICE must be cpu or gpu")
print(f"MAX {version('max')}; device={target}", flush=True)
tensor_type = TensorType(DType.float32, shape=[4], device=device)
with Graph("relu_sum", input_types=[tensor_type, tensor_type]) as graph:
graph.output(ops.relu(graph.inputs[0] + graph.inputs[1]))
session = InferenceSession(devices=[device])
model = session.init(session.compile(graph))
lhs = np.array([-2.0, 1.0, 3.0, -4.0], dtype=np.float32)
rhs = np.array([1.0, 2.0, -5.0, 8.0], dtype=np.float32)
result = model.execute(
Buffer.from_numpy(lhs).to(device),
Buffer.from_numpy(rhs).to(device),
)[0].to_numpy()
np.testing.assert_allclose(result, np.maximum(lhs + rhs, 0))
print(f"relu(lhs + rhs) = {result.tolist()}", flush=True)
print("PASS: MAX matches NumPy", flush=True)
if __name__ == "__main__":
main()TensorType describes each input's dtype, shape, and device. The graph records the addition and ReLU without computing values. session.compile() compiles it, session.init() prepares an executable model, and model.execute() runs it with real buffers. .to(device) places inputs on the selected processor; .to_numpy() copies the output to host memory for verification.
The arithmetic has an exact expected output: adding the arrays gives [-1, 3, -2, 4], and ReLU replaces negative values with zero.
Package the app
Create Dockerfile:
FROM python:3.12-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libedit2 libnuma1 && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir max==26.5.0 numpy==2.4.3
WORKDIR /app
COPY graph.py .
ENV MAX_DEVICE=cpu
CMD ["python", "-u", "graph.py"]Create wendy.json:
{
"appId": "max-graph",
"platform": "linux",
"version": "0.1.0",
"entitlements": []
}The CPU computation needs no device entitlements or network access at runtime.
Run on the CPU
wendy run --no-restartWendy builds for the target device's architecture, transfers the image, and runs the app. Expected application output:
MAX 26.5.0; device=cpu
relu(lhs + rhs) = [0.0, 3.0, 0.0, 4.0]
PASS: MAX matches NumPyThis is a finite computation. Wendy normally restarts apps unless you stop them; --no-restart lets this one exit after the assertion. It can then appear as stopped in wendy device apps list. Look for the result and PASS line to confirm the calculation succeeded.
Run on the GPU
Replace wendy.json with:
{
"appId": "max-graph",
"platform": "linux",
"version": "0.1.0",
"entitlements": [{ "type": "gpu" }]
}Then redeploy with the GPU selected:
wendy run --no-restart --env MAX_DEVICE=gpuThe first line should now report device=gpu, followed by the same numbers and PASS. The gpu entitlement makes the device's GPU resources available inside the container. MAX_DEVICE is an environment variable consumed by this example, not a built-in MAX setting.
This four-element graph is a correctness check, not a benchmark. Compilation and data transfers dominate such a small workload. It does not demonstrate a GPU speedup or guarantee that a larger model fits your device.
Troubleshooting
- No accelerator found: confirm the
gpuentitlement is present and inspectwendy device info. CPU success does not establish GPU compatibility. - CUDA or PTX compilation error: compare your driver/toolkit with Modular's GPU requirements. Older Jetson software may require a compatible
ptxasinside the container andMODULAR_NVPTX_COMPILER_PATHpointing to it. A host path is not automatically a container path; adding the entitlement cannot upgrade a driver. - Shape or dtype error after editing: keep the declared
TensorTypein sync with the NumPy arrays. This example uses fourfloat32values per input.
Next steps
- Run a real model with the MAX embedding service.
- Explore Modular's graph guide and buffer API.