Three independent backward-pass gaps that block training a vanilla ViT-Tiny (and likely any vision transformer) on ggml's auto-backward path. Found while building a Ruby/Spinel ML framework on top of ggml; happy to provide reproducers / discuss vendoring patches.
1. GGML_OP_NORM (LayerNorm) — no backward
ggml_build_backward_expand aborts at ggml.c:6874:
ggml_compute_backward: unsupported ggml op for backward pass: NORM
GGML_OP_RMS_NORM has a registered backward (ggml_rms_norm_back); LayerNorm doesn't. This forces every transformer that wants ggml training to swap LayerNorm → RMSNorm. For LLMs (Llama family) that's natural since they already use RMSNorm; for ViT variants and GPT-2-shape models, it's a real conversion cost (with downstream effects on weight-loader compat).
2. GGML_UNARY_OP_GELU — no backward
ggml_build_backward_expand aborts at ggml.c:6843 with the same shape:
ggml_compute_backward: unsupported unary op for backward pass: GELU
The unary backward dispatch switch (ggml.c:6780-6845) handles ABS, SGN, NEG, STEP, RELU, SILU, EXP, EXPM1, SOFTPLUS — but not GELU (or GELU_ERF, GELU_QUICK). Same effect: ViT MLPs (standard GELU FFN) can't backprop and must swap to SiLU.
A ggml-side ggml_gelu_back doesn't appear to exist; the GELU derivative is well-known (0.5 * (1 + tanh(sqrt(2/π)*(x + 0.044715*x³))) + ...) and could be added either as a new op or as a synthesized backward chain (it's a small composition of existing ops).
3. ggml_conv_2d backward asserts contiguous gradient
ggml.c:6649: GGML_ASSERT(ggml_is_contiguous(grad)) failed
Source of the issue: ggml_conv_2d's implementation (ggml.c:4585-4604) ends in ggml_cont(ggml_permute(...)). The auto-backward for GGML_OP_CONT (ggml.c:6645-6655) asserts the incoming gradient is contiguous. In a transformer-shaped graph the gradient flowing back from the next layer into the conv output isn't always contiguous.
Workarounds we've identified:
- Implement patch_embed as a flat linear
matmul(W_patch, flat_patches) instead of conv_2d. Mathematically equivalent to a stride=patch / no-overlap / no-padding conv. Trivial — but loses generality (overlapping conv, non-square strides, etc.).
- Vendor a conv2d implementation that ends differently, OR make the auto-bw of
OP_CONT materialise a contiguous grad before the assert.
Why this matters
Together these three gaps mean ggml cannot train a standard ViT through its native auto-backward. Each individually has a low-effort vendoring path; together they would unblock vision-transformer training. Happy to write the fixes if there's interest in the approach.
Repro environment
- Linux aarch64 (NVIDIA GB10)
- ggml HEAD as vendored in our repo (May 2026)
- Auto-backward via
ggml_build_backward_expand, no manual gradient construction
- Single-image batch (N=1)
Source where the three workarounds are pinned in code:
lib/vit_tiny_forward_ffi.rb — header comment + per-op notes
- Repository: OriPekelman/toy
Three independent backward-pass gaps that block training a vanilla ViT-Tiny (and likely any vision transformer) on ggml's auto-backward path. Found while building a Ruby/Spinel ML framework on top of ggml; happy to provide reproducers / discuss vendoring patches.
1.
GGML_OP_NORM(LayerNorm) — no backwardggml_build_backward_expandaborts atggml.c:6874:GGML_OP_RMS_NORMhas a registered backward (ggml_rms_norm_back); LayerNorm doesn't. This forces every transformer that wants ggml training to swap LayerNorm → RMSNorm. For LLMs (Llama family) that's natural since they already use RMSNorm; for ViT variants and GPT-2-shape models, it's a real conversion cost (with downstream effects on weight-loader compat).2.
GGML_UNARY_OP_GELU— no backwardggml_build_backward_expandaborts atggml.c:6843with the same shape:The unary backward dispatch switch (
ggml.c:6780-6845) handlesABS,SGN,NEG,STEP,RELU,SILU,EXP,EXPM1,SOFTPLUS— but notGELU(orGELU_ERF,GELU_QUICK). Same effect: ViT MLPs (standard GELU FFN) can't backprop and must swap to SiLU.A ggml-side
ggml_gelu_backdoesn't appear to exist; the GELU derivative is well-known (0.5 * (1 + tanh(sqrt(2/π)*(x + 0.044715*x³))) + ...) and could be added either as a new op or as a synthesized backward chain (it's a small composition of existing ops).3.
ggml_conv_2dbackward asserts contiguous gradientSource of the issue:
ggml_conv_2d's implementation (ggml.c:4585-4604) ends inggml_cont(ggml_permute(...)). The auto-backward forGGML_OP_CONT(ggml.c:6645-6655) asserts the incoming gradient is contiguous. In a transformer-shaped graph the gradient flowing back from the next layer into the conv output isn't always contiguous.Workarounds we've identified:
matmul(W_patch, flat_patches)instead ofconv_2d. Mathematically equivalent to a stride=patch / no-overlap / no-padding conv. Trivial — but loses generality (overlapping conv, non-square strides, etc.).OP_CONTmaterialise a contiguous grad before the assert.Why this matters
Together these three gaps mean ggml cannot train a standard ViT through its native auto-backward. Each individually has a low-effort vendoring path; together they would unblock vision-transformer training. Happy to write the fixes if there's interest in the approach.
Repro environment
ggml_build_backward_expand, no manual gradient constructionSource where the three workarounds are pinned in code:
lib/vit_tiny_forward_ffi.rb— header comment + per-op notes