ehartford commited on
Commit
f7c186c
·
verified ·
1 Parent(s): 9b51f42

Model card: inference and training examples (both executed), five-seed result; fused connectome-kernels path in modeling code

Browse files
Files changed (4) hide show
  1. README.md +76 -2
  2. model.safetensors +1 -1
  3. modeling_flygpt.py +43 -8
  4. tokenizer_config.json +1 -1
README.md CHANGED
@@ -81,7 +81,7 @@ h_i ← (1 − leak_i) h_i + leak_i · proposal_i (2 microsteps per chara
81
  512 output neuron states → linear → 65 logits
82
  ```
83
 
84
- ## Usage
85
 
86
  ```python
87
  import torch
@@ -93,9 +93,83 @@ model = AutoModelForCausalLM.from_pretrained("QuixiAI/FlyGPT", trust_remote_code
93
  ids = tok("ROMEO:", return_tensors="pt").input_ids
94
  out = model.generate(ids, max_new_tokens=300, do_sample=True, temperature=0.8)
95
  print(tok.decode(out[0]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ```
97
 
98
- The tokenizer is strict: only the 65 characters of Tiny Shakespeare are encodable.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  ## Citation
101
 
 
81
  512 output neuron states → linear → 65 logits
82
  ```
83
 
84
+ ## Inference
85
 
86
  ```python
87
  import torch
 
93
  ids = tok("ROMEO:", return_tensors="pt").input_ids
94
  out = model.generate(ids, max_new_tokens=300, do_sample=True, temperature=0.8)
95
  print(tok.decode(out[0]))
96
+
97
+ # The degree-preserving scrambled control (same neurons, same degrees, shuffled wiring), for comparison:
98
+ scrambled = AutoModelForCausalLM.from_pretrained("QuixiAI/FlyGPT", subfolder="scrambled", trust_remote_code=True, dtype=torch.float32)
99
+ print(tok.decode(scrambled.generate(ids, max_new_tokens=300, do_sample=True, temperature=0.8)[0]))
100
+
101
+ # Neuron activity, for visualization: [1, T, 5000] states after each character, plus MaleCNS body ids
102
+ with torch.no_grad():
103
+ states = model(ids).state # [B, N] after the last character
104
+ body_ids = model.graph.node_id # index -> MaleCNS body id, for lookup in QuixiAI/MaleCNS
105
+ ```
106
+
107
+ The tokenizer is strict: only the 65 characters of Tiny Shakespeare are encodable. `generate()` carries the neuron
108
+ state between characters instead of a KV cache.
109
+
110
+ ## Training
111
+
112
+ The recurrent core has one trainable weight per real synaptic connection. With the
113
+ [connectome-kernels](https://github.com/QuixiAI/connectome-kernels) package installed, the model's forward pass
114
+ runs on fused CUDA kernels (about 13× faster than `torch.sparse`, identical gradients); without it, it falls back
115
+ to `torch.sparse` automatically.
116
+
117
+ ```python
118
+ # Fine-tune / continue training FlyGPT on Tiny Shakespeare (character-level).
119
+ # pip install transformers safetensors
120
+ # pip install --no-build-isolation git+https://github.com/QuixiAI/connectome-kernels # fused CUDA path, ~13x faster
121
+ import requests, torch, torch.nn.functional as F
122
+ from transformers import AutoModelForCausalLM, AutoTokenizer
123
+
124
+ tok = AutoTokenizer.from_pretrained("QuixiAI/FlyGPT")
125
+ model = AutoModelForCausalLM.from_pretrained("QuixiAI/FlyGPT", trust_remote_code=True, dtype=torch.float32).cuda()
126
+ # start from the untrained initialization instead: subfolder="init"
127
+
128
+ text = requests.get("https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt").text
129
+ data = torch.tensor(tok(text).input_ids)
130
+ train, val = data[: int(0.9 * len(data))], data[int(0.9 * len(data)):] # FlyGPT's fixed 90/10 split
131
+
132
+ def batch(split, B=32, T=64):
133
+ i = torch.randint(0, len(split) - T - 1, (B,))
134
+ x = torch.stack([split[j : j + T] for j in i]); y = torch.stack([split[j + 1 : j + T + 1] for j in i])
135
+ return x.cuda(), y.cuda()
136
+
137
+ recurrent = list(model.recurrent.parameters()) # one weight per real synapse, bias, leak
138
+ adapters = [p for n, p in model.named_parameters() if not n.startswith("recurrent.")]
139
+ opt = torch.optim.AdamW([{"params": adapters, "lr": 1e-3}, {"params": recurrent, "lr": 3e-4}], weight_decay=0.01)
140
+
141
+ for step in range(1, 501):
142
+ x, y = batch(train)
143
+ logits = model(x).logits # [B, T, 65]; state resets to zero per window
144
+ loss = F.cross_entropy(logits.reshape(-1, 65), y.reshape(-1))
145
+ opt.zero_grad(set_to_none=True); loss.backward()
146
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step()
147
+ if step % 100 == 0:
148
+ with torch.no_grad():
149
+ vx, vy = batch(val); vl = F.cross_entropy(model(vx).logits.reshape(-1, 65), vy.reshape(-1))
150
+ print(f"step {step} train {loss.item():.3f} val {vl.item():.3f}")
151
+
152
+ model.save_pretrained("flygpt-finetuned"); tok.save_pretrained("flygpt-finetuned")
153
  ```
154
 
155
+ ## Result: does the wiring matter?
156
+
157
+ Pre-registered rule (before any result was seen): "wiring matters" is claimed only if all 5 paired
158
+ differences Δ = loss(scrambled) − loss(real) have the same sign **and** the mean Δ is at least 0.05 nats/char.
159
+
160
+ | seed | real | degree-preserving scramble | Δ |
161
+ |--:|--:|--:|--:|
162
+ | 1 | 1.7275 | 1.7450 | +0.0175 |
163
+ | 2 | 1.7285 | 1.7410 | +0.0125 |
164
+ | 3 | 1.7490 | 1.7594 | +0.0104 |
165
+ | 4 | 1.7516 | 1.7592 | +0.0076 |
166
+ | 5 | 1.7305 | 1.7377 | +0.0073 |
167
+ | **mean** | **1.7374** | **1.7485** | **+0.0111** |
168
+
169
+ Validation loss in nats/char at the end of 20,000 steps, same data order, batches, adapter init and edge-value RNG
170
+ stream per seed. Bigram reference on this split: 2.482. All five differences favour the real wiring, but the mean
171
+ gap is below the pre-registered minimum effect, so the verdict is: **no detectable difference at this scale.** The fly connectome learns
172
+ Shakespeare; whether its specific wiring helps, beyond its degree sequence, is not resolved at 5,000 neurons.
173
 
174
  ## Citation
175
 
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c1c0f9c6fc00d98d924fc273c9ea4b0ea0247701424240483ace1f344fbf59fe
3
  size 7495610
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d8c8b47fc37d85fb591f215a3cc54aa3d045e0edfb69a9d6116c1b1f87bbd317
3
  size 7495610
modeling_flygpt.py CHANGED
@@ -7,8 +7,10 @@ Dynamics (one scalar state per neuron, plan.md §7 of the FlyGPT spec):
7
  h_i_new = (1 - leak_i) * h_i + leak_i * proposal_i
8
 
9
  The connectome is stored in `model.safetensors` as integer tensors (`graph.*`); only the learned
10
- per-edge values and the adapters are floating point (bf16 on disk). The sparse recurrent matmul is
11
- rebuilt in fp32 at runtime (rows = destination, columns = source).
 
 
12
  """
13
  from __future__ import annotations
14
 
@@ -133,17 +135,50 @@ class FlyGPTForCausalLM(FlyGPTPreTrainedModel, GenerationMixin):
133
  def logits_from_state(self, state: torch.Tensor) -> torch.Tensor:
134
  return self.lm_head(state[:, self.graph.output_nodes].to(self.lm_head.weight.dtype)).float()
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def forward(self, input_ids: torch.LongTensor, state: Optional[torch.Tensor] = None,
137
  labels: Optional[torch.LongTensor] = None, use_cache: Optional[bool] = None,
138
  return_dict: Optional[bool] = None, **kwargs) -> FlyGPTOutput:
139
  B, T = input_ids.shape
140
  state = self.init_state(B, input_ids.device) if state is None else state
141
- W = self.sparse_weight()
142
- outs = []
143
- for t in range(T):
144
- state = self.step(state, input_ids[:, t], W)
145
- outs.append(self.logits_from_state(state))
146
- logits = torch.stack(outs, 1)
 
 
 
147
  loss = None
148
  if labels is not None:
149
  loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]), labels[:, 1:].reshape(-1))
 
7
  h_i_new = (1 - leak_i) * h_i + leak_i * proposal_i
8
 
9
  The connectome is stored in `model.safetensors` as integer tensors (`graph.*`); only the learned
10
+ per-edge values and the adapters are floating point (bf16 on disk). The sparse recurrent matmul runs
11
+ in fp32: through the fused kernels of the `connectome-kernels` package when it is installed and a
12
+ CUDA device is used (training speed), else through torch.sparse COO (rows = destination). Both give
13
+ the same logits and gradients.
14
  """
15
  from __future__ import annotations
16
 
 
135
  def logits_from_state(self, state: torch.Tensor) -> torch.Tensor:
136
  return self.lm_head(state[:, self.graph.output_nodes].to(self.lm_head.weight.dtype)).float()
137
 
138
+ # ---- fused CUDA path via the connectome-kernels package (optional, used for training) ------------
139
+ def _fused_graph(self):
140
+ from connectome_kernels import SparseGraph
141
+ dev = self.graph.edge_index.device
142
+ if getattr(self, "_fg", None) is None or self._fg_device != dev:
143
+ self._fg = SparseGraph(self.graph.edge_index[0].long(), self.graph.edge_index[1].long(), self.num_neurons,
144
+ self.graph.input_nodes)
145
+ self._fg_device = dev
146
+ return self._fg
147
+
148
+ def _fused_available(self, device) -> bool:
149
+ if device.type != "cuda":
150
+ return False
151
+ if not hasattr(self, "_fused_ok"):
152
+ try:
153
+ from connectome_kernels import available
154
+ self._fused_ok = available()
155
+ except Exception:
156
+ self._fused_ok = False
157
+ return self._fused_ok
158
+
159
+ def _forward_fused(self, input_ids, state):
160
+ from connectome_kernels import sparse_recurrence
161
+ drives = self.input_proj(self.embed(input_ids)).float().permute(1, 2, 0).contiguous() # [T, n_in, B]
162
+ vals = self.recurrent.edge_values.float() * self.edge_scale()
163
+ out = sparse_recurrence(vals, self.leak, self.recurrent.bias.float(), drives, state,
164
+ self._fused_graph(), self.config.microsteps) # [T, B, N]
165
+ logits = self.lm_head(out[:, :, self.graph.output_nodes].to(self.lm_head.weight.dtype)).float().permute(1, 0, 2)
166
+ return logits, out[-1]
167
+
168
  def forward(self, input_ids: torch.LongTensor, state: Optional[torch.Tensor] = None,
169
  labels: Optional[torch.LongTensor] = None, use_cache: Optional[bool] = None,
170
  return_dict: Optional[bool] = None, **kwargs) -> FlyGPTOutput:
171
  B, T = input_ids.shape
172
  state = self.init_state(B, input_ids.device) if state is None else state
173
+ if self._fused_available(input_ids.device):
174
+ logits, state = self._forward_fused(input_ids, state)
175
+ else:
176
+ W = self.sparse_weight()
177
+ outs = []
178
+ for t in range(T):
179
+ state = self.step(state, input_ids[:, t], W)
180
+ outs.append(self.logits_from_state(state))
181
+ logits = torch.stack(outs, 1)
182
  loss = None
183
  if labels is not None:
184
  loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]), labels[:, 1:].reshape(-1))
tokenizer_config.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "tokenizer_class": "PreTrainedTokenizerFast",
3
- "model_max_length": 1000000,
4
  "clean_up_tokenization_spaces": false,
5
  "add_prefix_space": false
6
  }
 
1
  {
2
  "tokenizer_class": "PreTrainedTokenizerFast",
3
+ "model_max_length": 10000000,
4
  "clean_up_tokenization_spaces": false,
5
  "add_prefix_space": false
6
  }