Up to speed with the AMD Strix Halo

Setting up and getting up to speed with an AMD Strix Halo (Ryzen AI Max+ 395) mini-PC for local AI development.
amd
rocm
local-ai
diffusers
Published

July 7, 2026

A few days back, AMD gifted me a GMKtec EVO-X2 so that I could do some of my compute-heavy work locally. NVIDIA did something similar with their DGX Spark a few months back, and using it in my regular tasks has been very productive.

For those who’re unfamiliar with EVO-X2, it’s a mini-PC equipped with an AMD Ryzen AI Max+ 395 (also known as “Strix Halo”) processor, ideal for local AI development work.

I found the onboarding experience for the EVO-X2 far less seamless than that of the DGX Spark, and I encountered some rough edges along the way. This is why I decided to write a short post documenting my setup experience and my process of getting up to speed with the device.

Snap of the EVO-X2 device, captured by my friend Sourav.

The rest of this post’s sections come from a setup and development experience I shared with my friend Sourav at his residence.

Booting up

To boot it up, the recommended path is to connect the device to a monitor, keyboard, and mouse. When the device is turned on for the first time, Windows appears. I didn’t want any Windows. Thankfully, my friend Sourav had it all set up with a bootable USB drive loaded with Ubuntu 26.04 and connected other peripherals mentioned above.

The bootup process was smooth, and it took about half an hour. Windows was also gone 😌

Ubuntu bootup success!

Note that this is a one-time process. If you want to access the device solely via SSH (as I do), you can discard the monitor, keyboard, and mouse after configuring SSH access.

Drivers

One of the very first things I do after getting access to an accelerator device like this (physically or remotely) is run commands to confirm that the device can detect the accelerator and print information about it. On the DGX Spark, this was about running the nvidia-smi . For this device, it would be rocm-smi. But getting there needed a few steps.

We’re in the era of Agents. So, to speed up this process, I installed Claude Code on the device and tasked it with setting up the Drivers, which proved quite productive. The end goal here was to make the system identify the accelerator so that I could query it. Since I had done this driver installation process for NVIDIA devices many times before, I had a good idea of what Claude Code was supposed to do here. Below is a summary of what it did:

  • Upgraded to the latest Linux kernels since Strix Halo’s RDNA 3.5 graphics require Linux kernel 6.14+ for native hardware detection.
  • Asked me to download the amdgpu-install package from the AMD Linux Drivers Page for my Ubuntu distribution. It then installed the amdgpu tool.

Because Strix Halo dynamically shares system memory, I needed to create a configuration rule to load and run some of my favorite models [1] [2].

sudo nano /etc/modprobe.d/amdgpu.conf
options amdgpu ttm_pages_limit=25600000
options amdgpu ttm_page_pool_size=25600000

And then:

sudo update-initramfs -u
sudo reboot now

This allows the system to allocate ~100GB out of a 128GB RAM kit, adjust the pages count down if your system has less memory.

After the bootup, I ran rocminfo to confirm the presence of gfx1151 in the outputs. I additionally installed rocm-smi so that I could monitor the usage. So, a bare rocm-smi after installing would print:

======================================== ROCm System Management Interface ========================================
================================================== Concise Info ==================================================
Device  Node  IDs              Temp    Power     Partitions          SCLK  MCLK  Fan  Perf  PwrCap  VRAM%  GPU%
              (DID,     GUID)  (Edge)  (Socket)  (Mem, Compute, ID)
==================================================================================================================
0       1     0x1586,   39404  32.0°C  9.012W    N/A, N/A, 0         N/A   N/A   0%   auto  N/A     0%     0%
==================================================================================================================
============================================== End of ROCm SMI Log ===============================================

A quick way to also verify how much memory is available to VRAM is to run sudo dmesg | grep "amdgpu.*memory":

[    5.532048] amdgpu 0000:c5:00.0:  65536M of VRAM memory ready
[    5.532053] amdgpu 0000:c5:00.0:  100000M of GTT memory ready.

We would like to ensure that the GTT memory is ≥ 100000M (~100 GB).

For this kind of tasks, using Sonnet as the model in Claude Code was enough and also cost-effective.

Development environment

Since I exactly knew what I wanted in my development environment (Python, uv, PyTorch, Hugging Face Hub, etc.), I further asked Claude Code to set it up for me with very clear instructions (Python version, PyTorch ROCm build, etc.). For the PyTorch installation step, I asked it to verify the installation by training and inferring a very small model on the AMD GPU (script).

============================================================
PyTorch build      : 2.12.1+rocm7.2
Built with ROCm/HIP: 7.2.53211
(null): No such file or directory
CUDA-API available : True
Device count       : 1
GPU name           : AMD Radeon 8060S
HSA_OVERRIDE_GFX_VERSION = (unset)
============================================================

Training 20 steps on cuda ...
  step  0  loss = 2.3256
  step  5  loss = 1.4674
  step 10  loss = 0.6972
  step 15  loss = 0.2105
  step 19  loss = 0.0623

Result tensor device : cuda:0
Matmul output device : cuda:0 | shape: (1024, 1024)
GPU memory allocated : 76.2 MiB

 SUCCESS: model trained and ran on the AMD GPU via ROCm.

The installation was done with:

uv pip install --index-url https://download.pytorch.org/whl/rocm7.2 \
    torch torchvision

This brought me to the most important check of the first day of my using the device — running actual models!

Running models

At the time of writing this post, I am working on image and video generation at Hugging Face (and thereby working on Diffusers). My favorite model to run and to get the lay of the land is black-forest-labs/FLUX.2-klein-4B. So, I started with a simple script:

import torch
from diffusers import Flux2KleinPipeline

device = "cuda"
dtype = torch.bfloat16

pipe = Flux2KleinPipeline.from_pretrained(
    "black-forest-labs/FLUX.2-klein-4B", torch_dtype=dtype
).to("cuda")

prompt = "a tiny astronaut hatching from an egg on the moon"
image = pipe(
    prompt=prompt,
    height=1024,
    width=1024,
    guidance_scale=1.0,
    num_inference_steps=8,
    generator=torch.Generator(device=device).manual_seed(0)
).images[0]
image.show()

Output from FLUX.2-klein-4B for the prompt “a tiny astronaut hatching from an egg on the moon”.

I tried another model at Sourav’s request: AceStep, since he’s into music. It also worked.

"""
Generate an audio clip with ACE-Step/acestep-v15-xl-turbo-diffusers.

Reads the generation spec from prompt.txt (prompt, lyrics, bpm, time
signature, key/scale, duration) and runs the diffusers AceStepPipeline.

Target hardware: AMD Radeon 8060S (Strix Halo APU, gfx1151), ROCm PyTorch.
On ROCm builds the GPU is exposed through the CUDA API, so device == 'cuda'.
If torch.cuda.is_available() is False, try exporting
    HSA_OVERRIDE_GFX_VERSION=11.0.0
before running.
"""

import re
import sys
from pathlib import Path

import torch
import soundfile as sf
from diffusers import AceStepPipeline

# prompt.txt i available here:
# https://huggingface.co/datasets/sayakpaul/sample-datasets/resolve/main/strix-halo/prompt.txt
MODEL_ID = "ACE-Step/acestep-v15-xl-turbo-diffusers"
PROMPT_FILE = Path(__file__).with_name("prompt.txt")
OUTPUT_FILE = Path(__file__).with_name("acestep_output.wav")
SEED = 42

def parse_spec(path: Path) -> dict:
    """Parse prompt.txt into the fields the pipeline needs.

    Expected layout: a fenced ```...``` block after a `prompt-` line, a second
    fenced block after a `lyrics-` line, then `key- value` metadata lines.
    """
    text = path.read_text(encoding="utf-8")

    # The prompt and lyrics are the first two fenced code blocks, in order.
    blocks = re.findall(r"```(.*?)```", text, flags=re.DOTALL)
    if len(blocks) < 2:
        raise ValueError(
            f"Expected 2 fenced ``` blocks (prompt, lyrics) in {path}, "
            f"found {len(blocks)}."
        )
    prompt = blocks[0].strip()
    lyrics = blocks[1].strip()

    def find(key: str):
        # Matches lines like "bpm- 170", "time signature- 3", "duration- 191s".
        m = re.search(rf"^{key}\s*[-:]\s*(.+)$", text, flags=re.IGNORECASE | re.MULTILINE)
        return m.group(1).strip() if m else None

    bpm_raw = find("bpm")
    dur_raw = find("duration")
    timesig = find("time signature")
    keyscale = find("keyscale")

    bpm = int(re.search(r"\d+", bpm_raw).group()) if bpm_raw else None
    # duration like "191s" -> 191.0 seconds
    duration = float(re.search(r"[\d.]+", dur_raw).group()) if dur_raw else 60.0

    return {
        "prompt": prompt,
        "lyrics": lyrics,
        "bpm": bpm,
        "timesignature": timesig,
        "keyscale": keyscale,
        "audio_duration": duration,
    }

def pick_device() -> str:
    if torch.cuda.is_available():
        return "cuda"
    raise SystemExit(
        "No GPU visible to PyTorch (torch.cuda.is_available() is False).\n"
        "For this Strix Halo (gfx1151) APU, try:\n"
        "    HSA_OVERRIDE_GFX_VERSION=11.0.0 python generate_audio.py"
    )

def main() -> None:
    spec = parse_spec(PROMPT_FILE)
    device = pick_device()

    print("=" * 64)
    print("Model         :", MODEL_ID)
    print("Torch / ROCm  :", torch.__version__, "| HIP", torch.version.hip)
    print("GPU           :", torch.cuda.get_device_name(0))
    print("Duration (s)  :", spec["audio_duration"])
    print("BPM           :", spec["bpm"])
    print("Time signature:", spec["timesignature"])
    print("Key / scale   :", spec["keyscale"])
    print("=" * 64)
    print("Prompt:\n", spec["prompt"], "\n")

    pipe = AceStepPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16)
    pipe = pipe.to(device)
    # Tiled VAE decode keeps memory bounded for long-form (multi-minute) audio.
    if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_tiling"):
        pipe.vae.enable_tiling()

    generator = torch.Generator(device=device).manual_seed(SEED)

    output = pipe(
        prompt=spec["prompt"],
        lyrics=spec["lyrics"],
        audio_duration=spec["audio_duration"],
        num_inference_steps=8,   # turbo model is designed for 8 steps
        bpm=spec["bpm"],
        keyscale=spec["keyscale"],
        timesignature=spec["timesignature"],
        generator=generator,
    )

    # audios shape: [batch, channels, samples] -> soundfile wants [samples, channels]
    audio = output.audios[0]                       # [channels, samples]
    audio_np = audio.T.cpu().float().numpy()       # [samples, channels]
    sr = getattr(pipe, "sample_rate", 48000)
    sf.write(str(OUTPUT_FILE), audio_np, sr)

    print(f"\n✅ Wrote {OUTPUT_FILE}  ({audio.shape[-1] / sr:.1f}s, {audio.shape[0]}ch @ {sr} Hz)")

if __name__ == "__main__":
    sys.exit(main())

What’s next?

The ability to run models on a non-NVIDIA device at similar speed has appealed to me. I want to use the EVO-X2 device in my regular development work on the two libraries I co-maintain at Hugging Face: diffusers and kernels. This will also help me to develop and test custom ROCm kernels and potentially speed up some of my favorite models. Finally, I want to test some video generation models, along with other community-favorite features such as quantization and offloading.

Acknowledgements: Thanks to Sourav for jamming with me on this through and through. Thanks to Guru for facilitating the EVO-X2 device!