A couple of weeks ago I was reading through the news that Fujitsu is bringing its FUJITSU-MONAKA CPU to market — a made-in-Japan, 2nm, 3D-stacked server chip going on sale to cloud and data center operators starting November 2026, alongside a matching Fujitsu MONAKA Server aimed at what Fujitsu calls "sovereign AI infrastructure." None of that has anything to do with Android apps on the surface. But it landed in my feed the same week I was still annoyed about a bug that took me three days to track down in a native audio library, and the two things clicked together in a way I didn't expect.

The short version: I'd been cross-compiling and testing an NDK library entirely on x86_64 CI runners for two years, and it had never once actually executed on real ARM silicon before it shipped to a device. MONAKA is just the latest, biggest data point in a trend that's been building for a while — Graviton, Ampere, Fujitsu's own earlier A64FX chip for the Fugaku supercomputer, and now this — where ARM stops being "the mobile architecture" and becomes just as normal a target for servers as x86 ever was. That trend is exactly why I no longer trust a green checkmark from an x86-emulated CI job as proof that my Android native code actually works. It's a slow shift, and easy to miss if you're heads-down shipping features, but it changes what "tested" is supposed to mean for anyone writing performance-critical native code.

This post is about that specific bug, why my test setup let it through for two years, and the CI change I made afterward that I think more Android teams shipping hand-tuned native code should be making too.

If your Android app ships NDK code with SIMD intrinsics or lock-free concurrency, cross-compiling for arm64 and running the tests on an x86 CI runner (even under QEMU) is not the same as testing on ARM. The instruction set matches; the memory model doesn't.

The day my NDK library passed CI and glitched on a real phone

The library in question is a small real-time audio mixer I maintain for a voice-recording app — nothing exotic, just a producer/consumer setup where the audio callback thread writes captured samples into a ring buffer and a background thread pulls them off for NEON-optimized resampling and mixing. It had years of unit tests, all green, all running happily on our GitHub Actions x86_64 runners against an NDK cross-compiled arm64-v8a build executed under QEMU.

Then a beta tester on a mid-range phone reported an intermittent audible click during long recordings — maybe once every ten minutes, impossible to reproduce on demand. I spent longer than I'd like to admit assuming it was a buffer underrun in the audio HAL before I actually looked hard at my own ring buffer code.

"It works every single time in CI" is one of the most dangerous sentences in software engineering, because it says nothing about the one environment you haven't actually tested.

What I eventually found was a classic memory-ordering bug: the writer thread incremented a plain volatile int write index after storing a sample, with no explicit memory barrier, and the reader thread spun on that same index. On x86, which has a strong memory model, stores from one core tend to become visible to other cores in roughly the order they were issued, so this kind of sloppy synchronization often works by accident. On a real Arm64 core, which has a much weaker memory ordering model, the reader could observe the incremented index before the actual sample write had become visible, and it would read a stale or torn value. That's the click.

What Fujitsu actually announced with MONAKA

Before I get further into the CI side of this, it's worth being precise about what Fujitsu actually said, because most of the coverage I read afterward blurred details together. The announcement is specifically about two products: the FUJITSU-MONAKA CPU sold as standalone silicon to cloud providers, data center operators, and server vendors, and the Fujitsu MONAKA Server, a complete system built around it. Both go on sale globally starting November 2026, with the server aimed at data center operators, enterprises, academic and HPC users, and the defense sector across Japan and Europe.

Fujitsu's framing leans hard on the phrase "sovereign AI infrastructure" — the pitch is that because the chip and server are designed and manufactured entirely in Japan, customers get traceability and supply chain transparency that they can't get from importing someone else's silicon. There's also a small footnote in the release that some of the underlying chip technology came out of a project subsidized by NEDO, Japan's New Energy and Industrial Technology Development Organization, which tells you this has been a long, state-supported bet rather than a quick product pivot.

On the raw numbers, Fujitsu's release gives a specific, narrow set of figures rather than a full spec sheet:

MetricWhat Fujitsu states
Compute die process2nm, 3D-stacked design
Max operating frequency3.8 GHz
Memory transfer speed8,800 MT/s
AI inference throughput~2x that of other CPUs, per Fujitsu
CoolingDesigned for air-cooled data centers
General availabilityNovember 2026

The other detail I found genuinely interesting is the resource pooling technology Fujitsu mentions for future scalability, plus its own cooling engineering aimed at cutting the energy a data center spends just keeping the chip cool. Combined with the air-cooled claim, the whole pitch is really about power and space efficiency for AI inference at the server level, not about raw clock speed bragging rights.

Why an Android developer should care about a server CPU

I want to be upfront that MONAKA itself will never run an Android app. Nobody's APK is getting deployed to a Fujitsu data center chip. The reason it's relevant to my day job is indirect: every time a major vendor puts real engineering weight behind ARM in the server room, it becomes a little more normal for cloud providers and CI vendors to offer ARM64 compute as a first-class, cheap option instead of a novelty tier. GitHub already ships hosted arm64 Linux runners. AWS has had Graviton for years. Fujitsu adding a high-end entrant aimed at AI inference is one more signal that this isn't a fad that's going away.

That matters for Android specifically because the NDK's primary target — arm64-v8a — is architecturally in the same family as these server chips, even though the actual silicon, core designs, and surrounding platform are completely different. The instruction set overlap tricks people into thinking that if code compiles for arm64 and passes tests under an x86 host (natively or via QEMU), it's been meaningfully tested on ARM. It hasn't. QEMU's user-mode emulation for aarch64 on an x86 host generally doesn't reproduce ARM's weaker memory ordering faithfully in every code path, and even when it's technically correct, the host's own strong-ordering hardware underneath tends to mask races that a real ARM core would expose.

NEON is the SIMD instruction set present on effectively all Android arm64 devices today. SVE and SVE2 are newer, variable-width vector extensions that show up in Armv9 designs, including some of the higher-end chips increasingly used in servers. Most phones still only support NEON, so code that assumes a fixed vector width is safe there — but it's worth knowing the two families aren't interchangeable if you ever port intrinsics-heavy code between mobile and server targets.

It's also not just a server-side phenomenon anymore. Anyone building Android on an Apple Silicon Mac has already been running an arm64 host toolchain for their day-to-day development for a few years now, without necessarily thinking about it as "ARM testing" — the CI pipeline was just the odd one out, still running on x86 in the cloud while the developer's own laptop was ARM the whole time. None of this means every Android team needs to rearchitect its CI. It matters specifically for the subset of teams writing hand-tuned intrinsics, lock-free data structures, or anything else where correctness depends on the actual memory model of the chip rather than just the instruction set it exposes.

Setting up an actual ARM64 test job

The fix I landed on wasn't to change how the Android build itself works — the NDK cross-compilation toolchain is fine and I'm not touching it. What changed is that I pulled the performance-critical core of the mixer out into a small, Android-agnostic C++ library with its own googletest suite, one that doesn't touch JNI, bionic, or anything Android-specific. That library gets built and tested as a plain Linux executable on two runner architectures side by side, so a race condition that only shows up on real ARM ordering has somewhere to actually surface before it ever reaches a device.

Here's the relevant part of the GitHub Actions workflow, running the same test binary natively on both an x86_64 runner and a real arm64 one instead of relying on emulation for the ARM leg:

jobs:
  native-core-tests:
    strategy:
      matrix:
        runner: [ubuntu-latest, ubuntu-24.04-arm]
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v4
      - name: Configure
        run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
      - name: Build
        run: cmake --build build
      - name: Run ring buffer tests natively
        run: ./build/audio_ring_buffer_test --gtest_repeat=200 --gtest_shuffle

The ubuntu-24.04-arm runner is a real ARM64 host, not an emulated one, so the memory-ordering bug I described earlier reproduces there reliably within a handful of repeats once you add concurrent stress via --gtest_repeat and --gtest_shuffle. On the x86_64 runner, the exact same test binary, same source, same seed, just passes every time. That gap between the two columns in the CI results is the whole argument for doing this in one screenshot's worth of evidence.

The memory-ordering bug x86 never caught

It's worth actually looking at the code, because the bug is embarrassingly small and reads as completely reasonable if you don't know to look for it. Here's roughly what the ring buffer looked like before I touched it:

struct AudioRingBuffer {
    int16_t data[kBufferFrames];
    volatile int writeIndex = 0;
    volatile int readIndex = 0;
};

void PushFrame(AudioRingBuffer* rb, int16_t sample) {
    rb->data[rb->writeIndex % kBufferFrames] = sample;
    rb->writeIndex++;  // no ordering guarantee relative to the store above
}

int16_t PopFrame(AudioRingBuffer* rb) {
    while (rb->readIndex == rb->writeIndex) { /* spin */ }
    int16_t sample = rb->data[rb->readIndex % kBufferFrames];
    rb->readIndex++;
    return sample;
}

volatile in C++ stops the compiler from reordering or eliding the access, but it says nothing about what the CPU is allowed to do, and it provides zero cross-thread ordering guarantees. On x86 this mostly gets away with it because of the platform's strong store ordering. Swapping the indices to proper atomics with explicit acquire/release semantics fixed it completely:

struct AudioRingBuffer {
    int16_t data[kBufferFrames];
    std::atomic writeIndex{0};
    std::atomic readIndex{0};
};

void PushFrame(AudioRingBuffer* rb, int16_t sample) {
    int idx = rb->writeIndex.load(std::memory_order_relaxed);
    rb->data[idx % kBufferFrames] = sample;
    rb->writeIndex.store(idx + 1, std::memory_order_release);
}

int16_t PopFrame(AudioRingBuffer* rb) {
    int widx;
    while ((widx = rb->writeIndex.load(std::memory_order_acquire)) ==
           rb->readIndex.load(std::memory_order_relaxed)) { /* spin */ }
    int idx = rb->readIndex.load(std::memory_order_relaxed);
    int16_t sample = rb->data[idx % kBufferFrames];
    rb->readIndex.store(idx + 1, std::memory_order_release);
    return sample;
}

The release store on the writer side guarantees the sample write is visible to any thread that observes the acquire load on the reader side, on every architecture, not just the ones with a forgiving memory model. It's a five-line diff. The hard part was never the fix — it was realizing my test suite had no way of ever telling me it was needed.


What I'd do differently, and where this doesn't apply

If I were setting this project up from scratch, I'd add the native arm64 CI leg on day one instead of two years in, and I'd make --gtest_shuffle with a decent repeat count the default for anything touching shared state between threads, on every architecture. Stress-testing concurrency locally on my x86 dev machine was never going to catch this no matter how many times I ran it.

QEMU-emulated arm64 test runs are useful for catching instruction-level mistakes and basic ABI issues, but don't mistake a green QEMU job for proof your concurrency is correct. Emulation running on x86 hardware inherits x86's memory ordering underneath it far more often than people assume.

I'd also push back a little on treating this as a universal rule. If your native code is straightforward, single-threaded, and free of hand-rolled synchronization, cross-compiling and testing under emulation is probably fine, and adding a second CI matrix leg is just cost for no benefit. This really only bites teams writing lock-free structures, custom atomics, or intrinsics where the actual silicon's behavior — not just its instruction set — determines correctness. A native arm64 CI leg also isn't a replacement for testing on an actual Android device or a service like Firebase Test Lab; it's a much faster, much cheaper first line of defense that catches the same class of bug in seconds instead of after a beta tester notices something is off.

Fujitsu shipping MONAKA doesn't change anything about how Android works tomorrow morning. But it's one more reminder that the industry's center of gravity keeps drifting toward ARM everywhere, not just in your pocket, and that the cheap ARM64 CI runner sitting in your pipeline options right now is worth turning on before a beta tester finds the bug for you.

Sources: Hacker News — Fujitsu launches made-in-Japan next-generation CPU FUJITSU-MONAKA