Skip to content

atome_load() produces misaligned float* pointers, hangs strict-alignment MCU cores (e.g. base RV32I) #2

Description

@delaidam

Summary

atome_load()'s binary parser walks the ATOME01 blob with a byte
cursor and casts sections directly to const float* (gamma, beta,
a, b, c_out in read_norm()/read_ssm()) without ensuring
4-byte alignment. On any MCU core that enforces alignment on 32-bit
loads (confirmed: PicoRV32 with its default CATCH_MISALIGN=1), this
causes a hang or trap the first time atome_layer_norm() dereferences
gamma/beta — not a crash, not a wrong answer, just silence.

Root cause

The magic string "ATOME01" is 7 bytes. 7 mod 4 = 3. For any model
config where every packed-ternary section (embed, local_conv.packed,
attn.Wq/Wk/Wv.packed, router.packed, unembed) happens to size out
to an exact multiple of 4 bytes — true whenever the relevant dimensions
are powers of two, which is common (e.g. the nano/tiny config in
c_engine/RAM_TABLE.md: d_model=16, vocab=32, d_head=8) — nothing after
the magic ever corrects that initial 3-byte skew. It propagates through
the entire rest of the file. Every subsequent float* field
(norm.gamma, norm.beta, ssm.a/b/c_out, and the final norm) is
offset by the same constant 3 bytes from a 4-byte boundary.

Verified concretely: for the nano config, block[0].norm.gamma lands
at byte offset 7 + (4 + 128) = 139. 139 mod 4 = 3.

Why this hasn't been caught before

x86 (the test suite, tests/test_parity_with_c.py) and ARMv7-M
(c_engine/targets/cortex-m3-ram, the source of RAM_TABLE.md's
numbers) both perform unaligned 32-bit loads transparently in hardware
— no trap, no visible effect. QEMU (used for the Cortex-M3 target)
likely doesn't model the trap either. Real RV32I silicon with default
settings — PicoRV32 in this case — appears to be the first target in
this repo's history that actually enforces alignment, and it does, on
the very first LayerNorm call of the very first model loaded.

Reproduction

On real PicoRV32/Tang Nano 9K hardware (a new target being submitted
separately as c_engine/targets/picorv32-tangnano9k/): boots, atome_load()
returns 0, the embedding lookup runs correctly, atome_layer_norm()'s
mean/variance/sqrtf all compute real finite values — and then
execution stops permanently the instant params->gamma[0] is read.
Confirmed via temporary UART-instrumented build printing the actual
pointer value: gamma_addr=0x001042DB (0xDB mod 4 = 3).

Root cause confirmed independent of that specific hardware: the same
misalignment is present in the nano_seed42.atome file regardless of
what loads it — it's a property of the exported binary layout, not of
PicoRV32. (PicoRV32 is just the first target to care.)

Suggested fix

Either:

  1. Pad the magic to a 4-byte-aligned length (e.g. "ATOME01\0", 8
    bytes), and have atome_load()/export_to_atome.py agree that
    every read_*() helper rounds its cursor up to the next 4-byte
    boundary before treating a field as const float*. This needs
    matching changes in both atome_load() (c_engine/upstream/atome.c)
    and export_to_atome.py, since the loader and exporter must agree
    on the exact byte layout.
  2. Alternatively, have the loader memcpy each float field out of the
    blob into an aligned scratch location instead of pointer-casting
    directly — avoids the format change but costs a bit of extra state
    and a few more lines in read_norm()/read_ssm().

Happy to help test against real PicoRV32 hardware once a fix is up —
that's how this was found in the first place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions