Upload UPSTREAM_ISSUE.md with huggingface_hub
Browse files- UPSTREAM_ISSUE.md +99 -67
UPSTREAM_ISSUE.md
CHANGED
|
@@ -1,102 +1,134 @@
|
|
| 1 |
-
# `needle finetune`
|
| 2 |
|
| 3 |
**Package:** `cactus-needle` 2.0.0 (PyPI)
|
| 4 |
-
**
|
| 5 |
-
**
|
|
|
|
| 6 |
|
| 7 |
## Summary
|
| 8 |
|
| 9 |
-
`needle finetune`
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
argues against ordinary LR instability and points at the loss/merge path.
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
```
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
huggingface-cli download Cactus-Compute/needle2 weights/needle2.pkl --local-dir ./nb
|
| 22 |
-
|
| 23 |
-
needle finetune corpus.jsonl \
|
| 24 |
-
--checkpoint ./nb/weights/needle2.pkl \
|
| 25 |
-
--epochs 3 --lora-rank 16 --lora-alpha 32 --max-len 512 --lr 1e-4 \
|
| 26 |
-
--out ./out/lora.pkl
|
| 27 |
```
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
training on 216 examples, seq_len 512
|
| 33 |
-
LoRA rank 16 on 10 weight groups (compiling...)
|
| 34 |
-
epoch 1/3 step 1/42 loss 2.9569
|
| 35 |
-
epoch 1/3 step 2/42 loss nan
|
| 36 |
-
epoch 1/3 step 3/42 loss nan
|
| 37 |
-
epoch 1/3 step 4/42 loss nan
|
| 38 |
-
```
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
```
|
| 43 |
-
|
| 44 |
-
|
|
|
|
| 45 |
```
|
| 46 |
|
| 47 |
-
##
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|---|---|---|
|
| 55 |
-
| Learning rate too high | ran 1e-4 and 1e-5 | **rejected** — 1e-5 NaNs *sooner* (step 2 vs 4) |
|
| 56 |
-
| Sequence truncation zeroing the loss mask | tokenised all rows through `render_example` + the package tokenizer | **rejected** — max total **371 tokens** (prompt 321–329, target 17–43) vs `--max-len` 512. No row's prompt reaches the limit, so no target is truncated away and no mask is all-zero |
|
| 57 |
-
| Malformed rows being skipped | counted rows surviving `load_jsonl`'s `if "query" not in example: continue` | **rejected** — 216/216 accepted |
|
| 58 |
-
| Wrong row schema | rendered rows through `render_example` and inspected prompt/target | **rejected** — renders correctly |
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
the `1/rank`-scaled LoRA `A` init interacting with the checkpoint dtype.
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
```
|
| 71 |
-
|
| 72 |
-
|
| 73 |
```
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
##
|
|
|
|
|
|
|
| 80 |
|
| 81 |
```
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
Windows-10-10.0.26300-SP0 · jax backend: cpu
|
| 85 |
-
Python 3.11
|
| 86 |
```
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
`HF_REPO = "Cactus-Compute/needle-prod"` (`needle/model/tokenizer.py:33`), which
|
| 92 |
-
returns **404** for an unauthenticated/ordinary token:
|
| 93 |
|
| 94 |
```
|
| 95 |
-
|
| 96 |
-
|
|
|
|
| 97 |
```
|
| 98 |
-
|
| 99 |
-
The equivalent file **is** public at `Cactus-Compute/needle2` under
|
| 100 |
-
`weights/needle2.pkl`. Passing `--checkpoint` with a local path works around it,
|
| 101 |
-
but the out-of-the-box default is unusable outside Cactus. Pointing `HF_REPO` at
|
| 102 |
-
the public repo would fix `needle finetune` and `needle run` for everyone else.
|
|
|
|
| 1 |
+
# `needle finetune` NaNs on the first optimiser step — root cause
|
| 2 |
|
| 3 |
**Package:** `cactus-needle` 2.0.0 (PyPI)
|
| 4 |
+
**Reported by:** THOX.ai — training orchestration
|
| 5 |
+
**Status:** root-caused and measured; THOX has worked around it with an
|
| 6 |
+
independent trainer.
|
| 7 |
|
| 8 |
## Summary
|
| 9 |
|
| 10 |
+
`needle finetune` prints a finite loss on step 1 and `nan` for every step
|
| 11 |
+
after, at every learning rate. The cause is a `0/0` inside AdamW, produced by
|
| 12 |
+
the interaction of three individually reasonable choices:
|
|
|
|
| 13 |
|
| 14 |
+
1. `needle2.pkl` stores **all 56 tensors as float16**.
|
| 15 |
+
2. `init_lora` (`needle/model/finetune.py:236`) casts the LoRA factors to
|
| 16 |
+
`weight.dtype`, so `A` and `B` are float16, and `B` is zero-initialised.
|
| 17 |
+
3. `optax.adamw`'s default `eps=1e-8` is **below float16's smallest
|
| 18 |
+
subnormal** (5.96e-8) and rounds to `0.0`.
|
| 19 |
|
| 20 |
+
Because `B` starts at zero, the step-1 gradient with respect to `A` is exactly
|
| 21 |
+
zero — `dL/dA = scale · grad_out @ Bᵀ`. Adam's update for `A` is therefore
|
| 22 |
|
| 23 |
+
```
|
| 24 |
+
m̂ / (√v̂ + ε) = 0 / (√0 + 0) = 0/0 = NaN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
|
| 27 |
+
on the **first** optimiser step. `A` becomes NaN, the merged weights become
|
| 28 |
+
NaN, and every subsequent loss is NaN. Step 1 reports a finite loss only
|
| 29 |
+
because loss is computed before the update is applied.
|
| 30 |
|
| 31 |
+
## Isolated reproduction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
```python
|
| 34 |
+
import numpy as np, jax.numpy as jnp, optax
|
| 35 |
+
|
| 36 |
+
print(np.float16(1e-8)) # -> 0.0
|
| 37 |
+
|
| 38 |
+
for dt in (jnp.float16, jnp.float32):
|
| 39 |
+
p = {"A": jnp.zeros((4, 4), dt)}
|
| 40 |
+
g = {"A": jnp.zeros((4, 4), dt)} # zero because B was zero-init
|
| 41 |
+
opt = optax.adamw(1e-4)
|
| 42 |
+
upd, _ = opt.update(g, opt.init(p), p)
|
| 43 |
+
print(dt.__name__, jnp.isfinite(optax.apply_updates(p, upd)["A"]).all())
|
| 44 |
+
```
|
| 45 |
|
| 46 |
```
|
| 47 |
+
0.0
|
| 48 |
+
float16 False
|
| 49 |
+
float32 True
|
| 50 |
```
|
| 51 |
|
| 52 |
+
## Why a learning-rate sweep cannot find this
|
| 53 |
|
| 54 |
+
A `0/0` is scale-invariant. Reducing the learning rate by 10× changes nothing.
|
| 55 |
+
Our earlier observation that `--lr 1e-5` appeared to fail "sooner" than `1e-4`
|
| 56 |
+
was an artifact of the progress cadence (`every = total_steps // 50`), not a
|
| 57 |
+
difference in behaviour — both fail on the first optimiser step.
|
| 58 |
|
| 59 |
+
This is worth stating because an LR sweep is the natural first response to a
|
| 60 |
+
NaN, and here it produces a *correct* negative result ("not the learning rate")
|
| 61 |
+
while being structurally incapable of identifying the actual cause.
|
| 62 |
|
| 63 |
+
## Suggested fixes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
Any one of these resolves it; the first is the standard mixed-precision
|
| 66 |
+
discipline and is what THOX adopted:
|
|
|
|
| 67 |
|
| 68 |
+
1. **Keep LoRA factors and optimiser state in float32**, treating the
|
| 69 |
+
checkpoint's float16 as a storage format only. Note that
|
| 70 |
+
`needle/model/decode.py:250` already does exactly this for inference
|
| 71 |
+
(`_f32(params)`); the training path simply omits it.
|
| 72 |
+
2. Pass an epsilon representable in float16, e.g. `optax.adamw(lr, eps=1e-4)`.
|
| 73 |
+
3. Initialise `A` to zero and `B` to random instead, so the step-1 gradient
|
| 74 |
+
with respect to the zero-initialised factor is non-zero. (Weakest option —
|
| 75 |
+
it removes this instance without removing the underflow.)
|
| 76 |
|
| 77 |
+
## Secondary issue — the default checkpoint is unreachable
|
| 78 |
+
|
| 79 |
+
`DEFAULT_BASE = "checkpoints/needle2.pkl"` (`finetune.py:18`) resolves against
|
| 80 |
+
`HF_REPO = "Cactus-Compute/needle-prod"` (`tokenizer.py:33`), which returns 404
|
| 81 |
+
for any account outside Cactus:
|
| 82 |
|
| 83 |
```
|
| 84 |
+
RepositoryNotFoundError: 404
|
| 85 |
+
https://huggingface.co/Cactus-Compute/needle-prod/resolve/main/checkpoints/needle2.pkl
|
| 86 |
```
|
| 87 |
|
| 88 |
+
The equivalent file is public at `Cactus-Compute/needle2` under
|
| 89 |
+
`weights/needle2.pkl`. Passing `--checkpoint` with a local path works around
|
| 90 |
+
it, but the out-of-the-box default is unusable outside Cactus. The same repo is
|
| 91 |
+
the fallback for `get_tokenizer`, so a user without the tokenizer cached hits
|
| 92 |
+
it there too.
|
| 93 |
|
| 94 |
+
## Third issue — `needle_load()` + `needle_complete()` faults on Windows
|
| 95 |
+
|
| 96 |
+
Loading *any* external `.cact` and then generating crashes the native library:
|
| 97 |
|
| 98 |
```
|
| 99 |
+
OSError: exception: access violation reading 0x0000000003110EA0
|
| 100 |
+
needle/__init__.py:76 in complete -> _lib().needle_complete(...)
|
|
|
|
|
|
|
| 101 |
```
|
| 102 |
|
| 103 |
+
This is not specific to a rebuilt artifact. The **upstream-shipped**
|
| 104 |
+
`needle2.cact` and a THOX-exported `.cact` fail identically, at the same call
|
| 105 |
+
site, with the same fault:
|
| 106 |
+
|
| 107 |
+
| weights | `needle_load` | `needle_init` | `needle_complete` |
|
| 108 |
+
|---|---|---|---|
|
| 109 |
+
| stock (no `weights=` argument) | n/a | ok | **ok** — 966 tok/s prefill, 452 tok/s decode, 35.8 MB peak |
|
| 110 |
+
| upstream-shipped `needle2.cact` | ok (returns 0) | ok | **access violation** |
|
| 111 |
+
| THOX-exported `.cact` | ok (returns 0) | ok | **access violation** |
|
| 112 |
+
|
| 113 |
+
So the load and init paths accept the artifact, and the library is functional
|
| 114 |
+
in its stock configuration; only the externally-loaded weights path faults.
|
| 115 |
+
Reproduced on Windows 11 (10.0.26300) with the `cactus-needle` 2.0.0 wheel's
|
| 116 |
+
auto-fetched native library.
|
| 117 |
+
|
| 118 |
+
Practical consequence: `needle build ... --lora` produces an artifact that
|
| 119 |
+
cannot be exercised on Windows via the Python binding, so end-to-end validation
|
| 120 |
+
of any fine-tune has to happen elsewhere.
|
| 121 |
+
|
| 122 |
+
## Licensing note
|
| 123 |
+
|
| 124 |
+
The published wheel is internally inconsistent about its own license:
|
| 125 |
+
`METADATA` declares `Apache-2.0` while `licenses/LICENSE` contains the MIT
|
| 126 |
+
License. Worth reconciling, since the two differ on patent grants.
|
| 127 |
|
| 128 |
+
## Environment
|
|
|
|
|
|
|
| 129 |
|
| 130 |
```
|
| 131 |
+
cactus-needle 2.0.0
|
| 132 |
+
jax 0.10.2 · jaxlib 0.10.2 · optax 0.2.8 · flax 0.12.8
|
| 133 |
+
Windows-11-10.0.26300 · jax backend: cpu · Python 3.11
|
| 134 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|