tommytracx commited on
Commit
800af51
·
verified ·
1 Parent(s): b1d4f8b

Upload UPSTREAM_ISSUE.md with huggingface_hub

Browse files
Files changed (1) hide show
  1. UPSTREAM_ISSUE.md +99 -67
UPSTREAM_ISSUE.md CHANGED
@@ -1,102 +1,134 @@
1
- # `needle finetune` produces NaN loss from step 2 lower LR fails sooner
2
 
3
  **Package:** `cactus-needle` 2.0.0 (PyPI)
4
- **Reporter:** THOX.ai — training orchestration
5
- **Severity:** blocks all LoRA fine-tuning on this host
 
6
 
7
  ## Summary
8
 
9
- `needle finetune` computes a finite loss on step 1 and `nan` from step 2–4, on a
10
- dataset that the package's own loader and renderer accept without complaint.
11
- **Reducing the learning rate 10× makes it fail *earlier*, not later**, which
12
- argues against ordinary LR instability and points at the loss/merge path.
13
 
14
- ## Reproduction
 
 
 
 
15
 
16
- ```bash
17
- pip install cactus-needle==2.0.0
18
 
19
- # public base weights — needle finetune otherwise tries
20
- # Cactus-Compute/needle-prod/checkpoints/needle2.pkl, which 404s (private)
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
- ### Observed
 
 
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
- At `--lr 1e-5 --max-len 1024`:
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  ```
43
- epoch 1/3 step 1/42 loss 2.5293
44
- epoch 1/3 step 2/42 loss nan
 
45
  ```
46
 
47
- ### Expected
48
 
49
- Loss decreases; no NaN.
 
 
 
50
 
51
- ## What we ruled out first
 
 
52
 
53
- | hypothesis | how it was checked | result |
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
- Remaining candidate: numerical instability in `merge_lora` /
61
- `softmax_cross_entropy_with_integer_labels` under this jax/jaxlib build, or in
62
- the `1/rank`-scaled LoRA `A` init interacting with the checkpoint dtype.
63
 
64
- ## Dataset shape
 
 
 
 
 
 
 
65
 
66
- 216 rows, `{system, tools, query, reasoning, answers}`. 120 rows carry a single
67
- tool call; **96 carry `answers: []`** (no applicable tool — an intentional refusal
68
- class). Rendered target for a refusal row is:
 
 
69
 
70
  ```
71
- <think>No provided tool satisfies this request.</think>
72
- <tool_call>[]</tool_call><|im_end|>
73
  ```
74
 
75
- If an empty `answers` list is not a supported target, that would be a plausible
76
- cause and is worth confirming — but note step 1 produces a finite loss over a
77
- batch that already contains such rows.
 
 
78
 
79
- ## Environment
 
 
80
 
81
  ```
82
- cactus-needle 2.0.0
83
- jax 0.10.2 · jaxlib 0.10.2 · optax 0.2.8 · flax 0.12.8
84
- Windows-10-10.0.26300-SP0 · jax backend: cpu
85
- Python 3.11
86
  ```
87
 
88
- ## Secondary issue gated default checkpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- `DEFAULT_BASE = "checkpoints/needle2.pkl"` resolves against
91
- `HF_REPO = "Cactus-Compute/needle-prod"` (`needle/model/tokenizer.py:33`), which
92
- returns **404** for an unauthenticated/ordinary token:
93
 
94
  ```
95
- RepositoryNotFoundError: 404
96
- https://huggingface.co/Cactus-Compute/needle-prod/resolve/main/checkpoints/needle2.pkl
 
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
+ / (√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
  ```