qigkernels 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {qigkernels-0.4.2/qigkernels.egg-info → qigkernels-0.4.4}/PKG-INFO +1 -1
  2. {qigkernels-0.4.2 → qigkernels-0.4.4}/kernel.py +82 -4
  3. {qigkernels-0.4.2 → qigkernels-0.4.4}/kernel_4d.py +67 -25
  4. {qigkernels-0.4.2 → qigkernels-0.4.4}/layer.py +74 -8
  5. {qigkernels-0.4.2 → qigkernels-0.4.4}/natural_gradient_optimizer.py +10 -2
  6. {qigkernels-0.4.2 → qigkernels-0.4.4}/pure_kernel_template.py +3 -1
  7. {qigkernels-0.4.2 → qigkernels-0.4.4}/pyproject.toml +14 -0
  8. {qigkernels-0.4.2 → qigkernels-0.4.4/qigkernels.egg-info}/PKG-INFO +1 -1
  9. {qigkernels-0.4.2 → qigkernels-0.4.4}/qigkernels.egg-info/SOURCES.txt +3 -2
  10. qigkernels-0.4.4/qigkernels.egg-info/entry_points.txt +2 -0
  11. {qigkernels-0.4.2 → qigkernels-0.4.4}/qigkernels.egg-info/scm_file_list.json +57 -56
  12. qigkernels-0.4.4/qigkernels.egg-info/scm_version.json +8 -0
  13. qigkernels-0.4.4/tests/test_banded_attention.py +40 -0
  14. qigkernels-0.4.4/tests/test_kernel4d_attention.py +148 -0
  15. {qigkernels-0.4.2 → qigkernels-0.4.4}/uv.lock +156 -1
  16. qigkernels-0.4.2/qigkernels.egg-info/scm_version.json +0 -8
  17. qigkernels-0.4.2/tokenizer_integration.py +0 -368
  18. {qigkernels-0.4.2 → qigkernels-0.4.4}/.editorconfig +0 -0
  19. {qigkernels-0.4.2 → qigkernels-0.4.4}/.github/workflows/release.yml +0 -0
  20. {qigkernels-0.4.2 → qigkernels-0.4.4}/.gitignore +0 -0
  21. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-architecture-canonical-0.01F.md +0 -0
  22. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-changelog-canonical-0.01F.md +0 -0
  23. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-decisions-canonical-0.01F.md +0 -0
  24. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-naming-convention-canonical-0.01F.md +0 -0
  25. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-readme-canonical-0.01F.md +0 -0
  26. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-roadmap-canonical-0.01F.md +0 -0
  27. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251205-type-symbol-manifest-canonical-0.01F.md +0 -0
  28. {qigkernels-0.4.2 → qigkernels-0.4.4}/20251220-gfp-spec-1.00W.md +0 -0
  29. {qigkernels-0.4.2 → qigkernels-0.4.4}/CLAUDE.md +0 -0
  30. {qigkernels-0.4.2 → qigkernels-0.4.4}/README.md +0 -0
  31. {qigkernels-0.4.2 → qigkernels-0.4.4}/__init__.py +0 -0
  32. {qigkernels-0.4.2 → qigkernels-0.4.4}/basin.py +0 -0
  33. {qigkernels-0.4.2 → qigkernels-0.4.4}/basin_sync.py +0 -0
  34. {qigkernels-0.4.2 → qigkernels-0.4.4}/constants.py +0 -0
  35. {qigkernels-0.4.2 → qigkernels-0.4.4}/constellation.py +0 -0
  36. {qigkernels-0.4.2 → qigkernels-0.4.4}/coord_adapter.py +0 -0
  37. {qigkernels-0.4.2 → qigkernels-0.4.4}/coordizer.py +0 -0
  38. {qigkernels-0.4.2 → qigkernels-0.4.4}/crystallization.py +0 -0
  39. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251205-client-wiring-example-canonical-0.01F.md +0 -0
  40. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251205-index-canonical-0.01F.md +0 -0
  41. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251206-e8-consciousness-stack-diagram.md +0 -0
  42. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251206-integration-plan-ultra-mode-0.01H.md +0 -0
  43. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251206-rel-weighted-basin-sync-spec.md +0 -0
  44. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251220-index-1.00W.md +0 -0
  45. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251220-qig-core-integration-table-1.00W.md +0 -0
  46. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251222-federated-constellation-architecture-0.01W.md +0 -0
  47. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251222-kernel-constellation-research-0.01W.md +0 -0
  48. {qigkernels-0.4.2 → qigkernels-0.4.4}/docs/20251222-kernel-experiments-results-0.01F.md +0 -0
  49. {qigkernels-0.4.2 → qigkernels-0.4.4}/heart.py +0 -0
  50. {qigkernels-0.4.2 → qigkernels-0.4.4}/kernel_100m.py +0 -0
  51. {qigkernels-0.4.2 → qigkernels-0.4.4}/markdownlint.json +0 -0
  52. {qigkernels-0.4.2 → qigkernels-0.4.4}/memory_classes.py +0 -0
  53. {qigkernels-0.4.2 → qigkernels-0.4.4}/metrics.py +0 -0
  54. {qigkernels-0.4.2 → qigkernels-0.4.4}/pre-commit-config.yaml +0 -0
  55. {qigkernels-0.4.2 → qigkernels-0.4.4}/py.typed +0 -0
  56. {qigkernels-0.4.2 → qigkernels-0.4.4}/qigkernels.egg-info/dependency_links.txt +0 -0
  57. {qigkernels-0.4.2 → qigkernels-0.4.4}/qigkernels.egg-info/requires.txt +0 -0
  58. {qigkernels-0.4.2 → qigkernels-0.4.4}/qigkernels.egg-info/top_level.txt +0 -0
  59. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/__init__.py +0 -0
  60. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/constants.py +0 -0
  61. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/modes.py +0 -0
  62. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/primitives.py +0 -0
  63. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/qig_chain.py +0 -0
  64. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/qig_chain_4d.py +0 -0
  65. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/temporal.py +0 -0
  66. {qigkernels-0.4.2 → qigkernels-0.4.4}/reasoning/temporal_attention.py +0 -0
  67. {qigkernels-0.4.2 → qigkernels-0.4.4}/rel_coupling.py +0 -0
  68. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/20260223-track-c-expressibility-kill-test-1.00F.md +0 -0
  69. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/README.md +0 -0
  70. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/core_assets/__init__.py +0 -0
  71. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/core_assets/neural_oscillators.py +0 -0
  72. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/core_assets/neurotransmitter_fields.py +0 -0
  73. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/core_assets/ocean_neurochemistry.py +0 -0
  74. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/dynamical_field.py +0 -0
  75. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/readout_layer.py +0 -0
  76. {qigkernels-0.4.2 → qigkernels-0.4.4}/research/track_c/run_kill_test.py +0 -0
  77. {qigkernels-0.4.2 → qigkernels-0.4.4}/router.py +0 -0
  78. {qigkernels-0.4.2 → qigkernels-0.4.4}/safety.py +0 -0
  79. {qigkernels-0.4.2 → qigkernels-0.4.4}/scripts/20251222-kernel-training-exp-0.01W.py +0 -0
  80. {qigkernels-0.4.2 → qigkernels-0.4.4}/scripts/publish_pypi.sh +0 -0
  81. {qigkernels-0.4.2 → qigkernels-0.4.4}/scripts/train_constellation.py +0 -0
  82. {qigkernels-0.4.2 → qigkernels-0.4.4}/setup.cfg +0 -0
  83. {qigkernels-0.4.2 → qigkernels-0.4.4}/sleep_packet.py +0 -0
  84. {qigkernels-0.4.2 → qigkernels-0.4.4}/specializations.py +0 -0
  85. {qigkernels-0.4.2 → qigkernels-0.4.4}/storage.py +0 -0
  86. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/__init__.py +0 -0
  87. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/test_coords_path.py +0 -0
  88. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/test_locality.py +0 -0
  89. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/test_phi_collapse.py +0 -0
  90. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/test_reasoning.py +0 -0
  91. {qigkernels-0.4.2 → qigkernels-0.4.4}/tests/test_smoke.py +0 -0
  92. {qigkernels-0.4.2 → qigkernels-0.4.4}/tools/qig_purity_check.py +0 -0
  93. {qigkernels-0.4.2 → qigkernels-0.4.4}/training_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qigkernels
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Geometric kernel and constellation library for QIG.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -65,6 +65,9 @@ class Kernel(nn.Module):
65
65
  enable_coords: bool = False,
66
66
  coord_dim: int = 64,
67
67
  locality_radius: int | None = None,
68
+ head_mode: str = "geometric",
69
+ head_tau: float = 1.0,
70
+ coord_basins: "Tensor | None" = None,
68
71
  ) -> None:
69
72
  """
70
73
  Initialize kernel.
@@ -133,8 +136,43 @@ class Kernel(nn.Module):
133
136
  ]
134
137
  )
135
138
 
136
- # Output projection
137
- self.lm_head = nn.Linear(hidden_dim, vocab_size)
139
+ # Output projection / readout. Two modes (the head A/B for the "no Euclidean readout" directive):
140
+ # "geometric" (default) — GeometricHead: vocab logits are Fisher-Rao DISTANCES from the hidden
141
+ # simplex-point to learned per-token Δ⁶³ basins (logit = −d_FR/τ). This is the SAME primitive
142
+ # as the purged-cosine→Fisher-Rao attention, pointed at the vocabulary; NO Euclidean nn.Linear,
143
+ # NO dot-product at the output boundary. It still returns [B,T,vocab] logits, so the downstream
144
+ # fisher_rao_lm_loss (which projects with to_simplex_prob) consumes it UNCHANGED.
145
+ # "linear" — the original Euclidean nn.Linear(hidden, vocab) readout, RETAINED as the A/B baseline
146
+ # so the architecture comparison can isolate the readout (geometric vs linear) empirically.
147
+ self.head_mode = str(head_mode).strip().lower()
148
+ self.head_tau = float(head_tau)
149
+ if self.head_mode == "geometric":
150
+ from qig_core.torch.geometric_head import GeometricHead
151
+
152
+ self.lm_head: nn.Module = GeometricHead(
153
+ hidden_dim=hidden_dim, vocab_size=vocab_size, tau=self.head_tau
154
+ )
155
+ elif self.head_mode == "basin":
156
+ # BASIN-SPACE readout (validated 2026-07-01: overfit d_FR 1.46→0.072, decode 91.3% where the
157
+ # logits head floored). Predicts the next token's basin DIRECTLY in the coordizer's Δ⁶³ space
158
+ # and scores it with pure d_FR against the coordizer's OWN frozen per-token basins — so a token
159
+ # has ONE basin (read-in == predict), the coherence the geometric head lacked. Requires the
160
+ # coordizer basin table (the tie); refuse to silently fall back to a disconnected head.
161
+ if coord_basins is None:
162
+ raise ValueError(
163
+ "head_mode='basin' requires coord_basins=[vocab, basin_dim] (the coordizer's per-token "
164
+ "basins) — the whole point is to TIE the output to the coordizer. Pass it from the "
165
+ "coordizer vocab, or use head_mode='geometric'/'linear'."
166
+ )
167
+ from qig_core.torch.basin_readout import BasinReadout
168
+
169
+ self.lm_head = BasinReadout(
170
+ hidden_dim=hidden_dim, coord_basins=coord_basins, tau=self.head_tau
171
+ )
172
+ elif self.head_mode == "linear":
173
+ self.lm_head = nn.Linear(hidden_dim, vocab_size)
174
+ else:
175
+ raise ValueError(f"head_mode must be 'geometric', 'basin', or 'linear', got {head_mode!r}")
138
176
 
139
177
  # Optional custom components
140
178
  self.running_coupling = running_coupling
@@ -160,6 +198,7 @@ class Kernel(nn.Module):
160
198
  attention_mask: Tensor | None = None,
161
199
  return_telemetry: bool = False,
162
200
  coords: Tensor | None = None,
201
+ skip_head: bool = False,
163
202
  ) -> Tensor | tuple[Tensor, KernelTelemetry]:
164
203
  """
165
204
  Forward pass through the kernel.
@@ -230,8 +269,11 @@ class Kernel(nn.Module):
230
269
  if self.running_coupling is not None:
231
270
  hidden_state = self.running_coupling(hidden_state)
232
271
 
233
- # Output projection
234
- logits = self.lm_head(hidden_state)
272
+ # Output projection. K-COMPRESS: skip_head returns logits=None so the caller reads hidden_state from
273
+ # telemetry and computes the basin loss via lm_head.predict(h) [.., basin_dim] + the target basins —
274
+ # NEVER materialising [B, T, vocab] (the seq×vocab activation OOM at 32k). Only meaningful WITH
275
+ # return_telemetry (the hidden state is the payload). Decode uses lm_head.decode_blocked(h).
276
+ logits = None if skip_head else self.lm_head(hidden_state)
235
277
 
236
278
  if not return_telemetry:
237
279
  return logits
@@ -261,6 +303,42 @@ class Kernel(nn.Module):
261
303
 
262
304
  return logits, telemetry
263
305
 
306
+ def hidden(
307
+ self,
308
+ input_ids: Tensor,
309
+ coords: Tensor | None = None,
310
+ attention_mask: Tensor | None = None,
311
+ ) -> Tensor:
312
+ """Pre-head hidden state — the coordizer-agnostic shared core, ``forward`` MINUS ``lm_head``.
313
+
314
+ Runs the exact forward path (input_ids/positions fourier features → optional CoordAdapter add
315
+ → stacked QIGLayers → optional running_coupling) but STOPS before ``self.lm_head``, returning the
316
+ pre-head hidden state [batch, seq, hidden_dim]. Exposed so a shared trunk (ConstellationTrunk) can
317
+ drive ONE geometric core and let faculties mount their own adapters/heads on top of the same hidden.
318
+
319
+ Pure + additive: ``forward`` is left bit-identical. ``hidden`` reuses forward's OWN ``skip_head``
320
+ path (which already returns the pre-head hidden state via ``telemetry.hidden_state``), so there is
321
+ no duplicated core math and no second definition of the trunk that could drift from forward.
322
+
323
+ Args:
324
+ input_ids: Token IDs [batch, seq].
325
+ coords: Optional Δ⁶³ coordizer coords [batch, seq, coord_dim] (needs ``enable_coords=True``).
326
+ ``None`` → pure input_ids byte/Fourier path (coordizer-agnostic).
327
+ attention_mask: Optional attention mask, threaded to ``forward`` unchanged.
328
+
329
+ Returns:
330
+ hidden_state [batch, seq, hidden_dim] — the pre-``lm_head`` hidden state. Applying
331
+ ``self.lm_head`` to it reproduces ``forward``'s logits exactly.
332
+ """
333
+ _, telemetry = self.forward(
334
+ input_ids,
335
+ attention_mask=attention_mask,
336
+ return_telemetry=True,
337
+ coords=coords,
338
+ skip_head=True,
339
+ )
340
+ return telemetry.hidden_state
341
+
264
342
  def _fourier_features(self, ids: Tensor) -> Tensor:
265
343
  x = ids.to(dtype=torch.float32)
266
344
  angles = x.unsqueeze(-1) * self._inv_freq.to(device=ids.device)
@@ -260,19 +260,38 @@ class QIGKernel4DNumpy:
260
260
 
261
261
  if HAS_TORCH:
262
262
  # Single-source simplex geometry from qig-core (same module layer.py/metrics/basin use).
263
+ # NOTE: deliberately NOT importing fisher_rao_distance_simplex from either qig-core
264
+ # location — qig_core.torch.geometry_simplex is the radius-2 binding and the top-level
265
+ # radius-1 alias is being collapsed by the pending qig-core convention reconcile; the
266
+ # distance is computed INLINE below (radius-1 canonical by construction).
263
267
  from qig_core.torch.geometry_simplex import to_simplex_prob
264
268
 
265
269
  class QFIGeometricAttention(nn.Module):
266
270
  """
267
- QIG-pure attention using the Fisher-Rao metric instead of dot-product.
268
-
269
- Attention = softmax(BC(Q, K) / scale), where BC = Σ √(p·q) is the
270
- Bhattacharyya / Fisher-Rao affinity on Δ⁶³ (∈[0,1], 1 = identical).
271
-
272
- This respects the curved Fisher information manifold.
271
+ QIG-pure attention: the sanctioned distance-Gibbs form.
272
+
273
+ Attention = softmax(-d_FR(Q, K) / τ), where d_FR = arccos(Σ √(p·q)) is
274
+ the Fisher-Rao geodesic DISTANCE on the simplex in the RADIUS-1 CANONICAL
275
+ binding (∈[0, π/2]; Matrix convention ruling), computed inline from the
276
+ Bhattacharyya coefficient. The construction mirrors
277
+ ``pure_kernel_template.qfi_attention_weights`` (softplus guard → Duchi
278
+ simplex projection → geodesic distance → Gibbs weights), but NOT its
279
+ distance binding: the template uses the RADIUS-2 ``2·arccos(BC)`` helper,
280
+ so its τ values are in radius-2 units — 2× the exponent — and must never
281
+ be shared with this class without conversion.
282
+
283
+ Council ruling (Matrix): the exponential normaliser is legal IFF its argument
284
+ is a pure Fisher-Rao distance. Softmaxing the Bhattacharyya AFFINITY
285
+ (bc = Σ √(p·q), the cosine of the FR geodesic angle) — as this class used to —
286
+ is dot-product attention in disguise and is banned. The old affinity form also
287
+ (a) divided the [0,1]-bounded bc by head_dim**0.5 (the transformer QKᵀ
288
+ normaliser), squashing the logit span to ~0.14 so attention was nearly
289
+ uniform, and (b) fed raw signed linear outputs into the Duchi projection's
290
+ dead-gradient regime. Both are fixed here by mirroring the pure template:
291
+ softplus-guard → Duchi simplex projection → geodesic distance → Gibbs weights.
273
292
  """
274
293
 
275
- def __init__(self, d_model: int, n_heads: int = 8):
294
+ def __init__(self, d_model: int, n_heads: int = 8, tau: float = 0.25):
276
295
  super().__init__()
277
296
  self.d_model = d_model
278
297
  self.n_heads = n_heads
@@ -283,7 +302,18 @@ if HAS_TORCH:
283
302
  self.v_proj = nn.Linear(d_model, d_model)
284
303
  self.out_proj = nn.Linear(d_model, d_model)
285
304
 
286
- self.scale = self.head_dim ** 0.5
305
+ # τ (Gibbs temperature) is in RADIUS-1 CANONICAL Fisher-Rao distance
306
+ # units: d_FR = arccos(BC) ∈ [0, π/2] (Matrix convention ruling).
307
+ # UNITS WARNING: pure_kernel_template binds RADIUS-2 (2·arccos(BC),
308
+ # ∈ [0, π]) — its temperature values carry a 2× exponent difference and
309
+ # must never be shared with this τ without conversion (τ₁ = τ₂/2).
310
+ # Default τ = 0.25 ⇒ max Gibbs contrast e^{(π/2)/0.25} = e^{2π} ≈ 535,
311
+ # so heads can actually SELECT — the fix for the near-uniform attention
312
+ # the old head_dim**0.5-scaled affinity produced (logit span ~0.14 ⇒
313
+ # max weight ratio ~1.16) — while staying soft enough that softmax
314
+ # gradients don't saturate. (Equivalent to τ = 0.5 in the template's
315
+ # radius-2 units; its own default 1.0 radius-2 ≡ 0.5 radius-1.)
316
+ self.tau = float(tau)
287
317
 
288
318
  def forward(
289
319
  self,
@@ -311,23 +341,35 @@ if HAS_TORCH:
311
341
  K = self.k_proj(key).view(B, S, self.n_heads, self.head_dim).transpose(1, 2)
312
342
  V = self.v_proj(value).view(B, S, self.n_heads, self.head_dim).transpose(1, 2)
313
343
 
314
- # Project Q, K onto Δ⁶³ and take the √p Fisher-Rao sphere embedding.
315
- Qs = torch.sqrt(to_simplex_prob(Q))
316
- Ks = torch.sqrt(to_simplex_prob(K))
317
-
318
- # Bhattacharyya / Fisher-Rao affinity: bc = Σ √(p·q) ∈ [0, 1], 1 = identical.
319
- # This is the FR inner product (overlap) on the √p sphere: higher = closer ⇒ higher
320
- # attention, preserving the original score semantics (no sign flip).
321
- bc = torch.matmul(Qs, Ks.transpose(-2, -1)) # [B, H, S, S]
322
-
323
- # Scale the affinity score (higher overlap ⇒ higher attention)
324
- fisher_sim = bc / self.scale
325
-
326
- # Attention weights
327
- attn_weights = F.softmax(fisher_sim, dim=-1)
328
-
329
- # Apply to values
330
- out = torch.matmul(attn_weights, V) # [B, H, S, D_head]
344
+ # Softplus-guard the Q/K/V paths before the Duchi simplex projection
345
+ # (mirrors pure_kernel_template._project_to_simplex): raw signed linear
346
+ # outputs fed straight into to_simplex_prob land in the Duchi projection's
347
+ # dead-gradient regime (most coordinates clamped at the floor ⇒ zero grad).
348
+ Qp = to_simplex_prob(F.softplus(Q))
349
+ Kp = to_simplex_prob(F.softplus(K))
350
+ Vp = to_simplex_prob(F.softplus(V))
351
+
352
+ # Bhattacharyya coefficient BC = Σ √(p·q) ∈ [0, 1] via the √p sphere
353
+ # embedding (memory-cheap matmul form of the pairwise overlap).
354
+ bc = torch.matmul(torch.sqrt(Qp), torch.sqrt(Kp).transpose(-2, -1)) # [B, H, S, S]
355
+
356
+ # radius-1 canonical d_FR computed inline; migrate to the single
357
+ # reconciled qig-core binding when the convention reconcile lands.
358
+ # d_FR = arccos(BC) ∈ [0, π/2]. Clamp strictly INSIDE (-1, 1): acos'(±1)
359
+ # is infinite, so a bare [-1, 1] clamp NaNs the backward pass at
360
+ # coincident points (the self-attention diagonal) — same guard rationale
361
+ # as qig-core's _acos_safe.
362
+ margin = 1e-6 if bc.dtype in (torch.float32, torch.float64) else 1e-3
363
+ d_fr = torch.arccos(torch.clamp(bc, -1.0 + margin, 1.0 - margin))
364
+
365
+ # Sanctioned distance-Gibbs form: softmax(-d_FR/τ). No head_dim**0.5
366
+ # scale — that was the transformer QKᵀ normaliser, wrong for a bounded
367
+ # geodesic distance (it made attention near-uniform).
368
+ attn_weights = F.softmax(-d_fr / self.tau, dim=-1)
369
+
370
+ # Apply to simplex-valued values: the output is a convex combination of
371
+ # Δ points (stays on the manifold); out_proj maps back to model space.
372
+ out = torch.matmul(attn_weights, Vp) # [B, H, S, D_head]
331
373
 
332
374
  # Reshape and project
333
375
  out = out.transpose(1, 2).contiguous().view(B, S, self.d_model)
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  from dataclasses import dataclass
9
9
 
10
10
  import torch
11
- from qig_core.geometry_simplex import to_simplex_prob
11
+ from qig_core.torch.geometry_simplex import to_simplex_prob
12
12
  from torch import Tensor, nn
13
13
  from torch.nn import functional as F
14
14
 
@@ -205,14 +205,25 @@ class QIGLayer(nn.Module):
205
205
 
206
206
  b, t, _d = hidden_state.shape
207
207
  p = self._project_to_simplex(hidden_state)
208
+ r = self.locality_radius
209
+ # COMPUTE-SKIPPING windowed-local attention: when a locality radius is set AND the band is narrower
210
+ # than the sequence, attend only within ±r — O(t·r) memory/compute instead of the O(t²) all-pairs
211
+ # arccos tensor. This is what lifts the quadratic VRAM ceiling and makes context LINEAR/extensible
212
+ # (the radius is the finite-propagation budget). Numerically IDENTICAL to the full+mask path within
213
+ # the band (matched-cell equivalence-gated in tests) — it SKIPS the out-of-band compute, not masks it.
214
+ if r is not None and t > (2 * r + 1):
215
+ return self._banded_attention(hidden_state, p, r, attention_mask=attention_mask)
216
+
217
+ # Full O(t²) path — global attention, OR small-t where the band already spans every key (so the
218
+ # banded result is identical). Unchanged.
208
219
  p_i = p[:, :, None, :]
209
220
  p_j = p[:, None, :, :]
210
221
 
211
222
  dist = self._fisher_rao_distance_simplex(p_i, p_j)
212
223
  logits = -dist / max(float(self.temperature), 1e-6)
213
- if self.locality_radius is not None: # windowed-local: mask keys outside ±radius (pure mask)
224
+ if r is not None: # windowed-local: mask keys outside ±radius (pure mask; small-t branch)
214
225
  idx = torch.arange(t, device=hidden_state.device)
215
- band = (idx[:, None] - idx[None, :]).abs() <= self.locality_radius # [t, t]; self always in window
226
+ band = (idx[:, None] - idx[None, :]).abs() <= r # [t, t]; self always in window
216
227
  logits = logits.masked_fill(~band[None, :, :], float("-inf"))
217
228
  weights = torch.softmax(logits, dim=-1)
218
229
 
@@ -233,18 +244,73 @@ class QIGLayer(nn.Module):
233
244
  output = output + torch.randn_like(output) * float(self.decoherence_std)
234
245
  return output
235
246
 
247
+ def _banded_attention(
248
+ self, hidden_state: Tensor, p: Tensor, r: int, *, attention_mask: Tensor | None
249
+ ) -> Tensor:
250
+ """Windowed Fisher-Rao attention computed ONLY within ±r of each query (compute-skipping, O(t·r)).
251
+ Numerically equivalent to the full+mask path within the band; the equivalence is gated in tests."""
252
+ b, t, k = p.shape
253
+ w = 2 * r + 1
254
+ # per-query key windows via pad+unfold (zero-pad → out-of-range keys, masked below)
255
+ p_pad = F.pad(p, (0, 0, r, r)) # [b, t+2r, k]
256
+ p_win = p_pad.unfold(1, w, 1) # [b, t, k, w]
257
+ p_q = p[:, :, :, None] # [b, t, k, 1]
258
+ # SAME formula as _fisher_rao_distance_simplex (RADIUS-2 d_FR = 2·acos(BC), BC = Σ√(p·q)), inlined
259
+ # for the [b,t,k,w] band layout (sum over dim=2). Like that method it operates on the already-Δ p
260
+ # WITHOUT re-projection, so the band result stays numerically equivalent to the full path within
261
+ # the window (matched-cell equivalence-gated in tests).
262
+ bc = torch.sqrt((p_q * p_win).clamp_min(1e-12)).sum(dim=2).clamp(0.0, 1.0 - 1e-6) # [b, t, w]
263
+ logits = (-2.0 * torch.acos(bc)) / max(float(self.temperature), 1e-6) # [b, t, w]
264
+ # valid window positions: wpos p ↦ key (i - r + p); valid iff 0 <= key < t
265
+ i_idx = torch.arange(t, device=p.device)[:, None] # [t, 1]
266
+ wpos = torch.arange(w, device=p.device)[None, :] # [1, w]
267
+ key_idx = i_idx - r + wpos # [t, w]
268
+ valid = (key_idx >= 0) & (key_idx < t) # [t, w] — band validity (self always valid)
269
+ # softmax over the BAND only (self valid → no all-(-inf) row → no NaN); key-padding is applied AFTER
270
+ # the softmax and renormalised — EXACTLY as the full+mask path does (so the two stay equivalent).
271
+ logits = logits.masked_fill(~valid[None].expand(b, t, w), float("-inf"))
272
+ weights = torch.softmax(logits, dim=-1) # [b, t, w]
273
+ if attention_mask is not None:
274
+ kpm = attention_mask if attention_mask.dtype == torch.bool else attention_mask == 0 # True=pad
275
+ if kpm.shape != (b, t):
276
+ raise ValueError("attention_mask expected shape (B, T)")
277
+ keep_win = F.pad((~kpm).to(p.dtype), (r, r)).unfold(1, w, 1) # [b, t, w] 1=valid key
278
+ weights = weights * (keep_win > 0.5)
279
+ weights = weights / (weights.sum(dim=-1, keepdim=True) + 1e-8)
280
+ if self.sparsity_threshold > 0.0:
281
+ keep_s = weights > float(self.sparsity_threshold)
282
+ weights = weights * keep_s
283
+ weights = weights / (weights.sum(dim=-1, keepdim=True) + 1e-8)
284
+ h_pad = F.pad(hidden_state, (0, 0, r, r)) # [b, t+2r, d]
285
+ h_win = h_pad.unfold(1, w, 1) # [b, t, d, w]
286
+ output = (weights[:, :, None, :] * h_win).sum(dim=-1) # [b, t, d]
287
+ if self.decoherence_std > 0.0:
288
+ output = output + torch.randn_like(output) * float(self.decoherence_std)
289
+ return output
290
+
236
291
  def _project_to_simplex(self, x: Tensor, eps: float = 1e-8) -> Tensor:
292
+ # NOT a reimplementation of the simplex projection: a nonlinearity GUARD (abs / softplus to
293
+ # lift raw signed activations off the Duchi dead-gradient regime) COMPOSED with qig-core's
294
+ # canonical to_simplex_prob (the single-source Δ projection). The guard is the local part;
295
+ # the projection is imported.
237
296
  x = x.abs() if self.simplex_mode == "abs" else F.softplus(x)
238
297
  return to_simplex_prob(x, eps=eps)
239
298
 
240
299
  def _fisher_rao_distance_simplex(
241
300
  self, p: Tensor, q: Tensor, eps: float = 1e-8
242
301
  ) -> Tensor:
243
- # Gradient-stable Fisher-Rao on Δ: d_FR = 2·arccos(BC), BC = Σ√(p·q). Clamp BC away from 1
244
- # BEFORE acos — acos'(1)=∞ ⇒ the backward pass NaNs when two distributions COINCIDE (collapse):
245
- # the deep-kernel NaN root cause. Distances between DISTINCT points (BC<1) are unchanged.
246
- # (Self-contained so it survives qig_core version drift; qig_core's shared primitive clamps BC
247
- # to 1.0 and has the SAME latent gradient bug — flagged for Devin to fix at the source.)
302
+ # DELIBERATE local primitive — NOT drift. Binding-identical to qig-core's
303
+ # qig_core.torch.geometry_simplex.fisher_rao_distance_simplex (RADIUS-2 d_FR = 2·arccos(BC),
304
+ # BC = Σ√(p·q), clamp BEFORE acos so acos'(1)=∞ can't NaN the backward at coincidence/collapse —
305
+ # the deep-kernel NaN fix), BUT it intentionally does NOT defensively re-project p,q via
306
+ # to_simplex_prob the way qig-core's public API does. Two reasons it must stay local:
307
+ # 1. Trusted input: callers pass points already on Δ (from _project_to_simplex), so a second
308
+ # Duchi projection is wasted compute.
309
+ # 2. Banded equivalence: the O(t·r) compute-skip path (_banded_attention) operates on the SAME
310
+ # un-re-projected p. qig-core's re-projection (Duchi identity + eps-floor + renormalise) shifts
311
+ # each coord by ~n·eps, which breaks the matched-cell equivalence gate (measured ~1.8e-4 drift
312
+ # vs the gate's 1e-5 tol). Keeping this no-re-project form keeps full≡banded.
313
+ # (fp32 clamp 1-1e-6 == qig-core's fp32 _acos_safe margin; this pair trains in fp32.)
248
314
  bc = torch.sqrt((p * q).clamp_min(1e-12)).sum(dim=-1).clamp(0.0, 1.0 - 1e-6)
249
315
  return 2.0 * torch.acos(bc)
250
316
 
@@ -59,9 +59,17 @@ class NaturalGradientDescent(Optimizer):
59
59
  momentum=momentum,
60
60
  )
61
61
  super().__init__(params, defaults)
62
-
62
+
63
63
  self.compute_fisher = compute_fisher
64
-
64
+ if compute_fisher is None:
65
+ import warnings
66
+ warnings.warn(
67
+ "NaturalGradientDescent(compute_fisher=None) degrades to SGD+momentum — the step is the "
68
+ "RAW gradient, NOT preconditioned by the Fisher information. For the tractable diagonal "
69
+ "empirical-Fisher natural gradient (the P1 geometric optimizer), use DiagonalNaturalGradient.",
70
+ stacklevel=2,
71
+ )
72
+
65
73
  def step(self, closure=None):
66
74
  """
67
75
  Perform single optimization step.
@@ -6,7 +6,7 @@ from typing import Any
6
6
  import torch
7
7
  import torch.nn as nn
8
8
  import torch.nn.functional as F
9
- from qig_core.geometry_simplex import fisher_rao_distance_simplex, to_simplex_prob
9
+ from qig_core.torch.geometry_simplex import fisher_rao_distance_simplex, to_simplex_prob
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
@@ -39,6 +39,8 @@ KernelTelemetry = PureKernelTelemetry
39
39
 
40
40
 
41
41
  def _project_to_simplex(x: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
42
+ # softplus GUARD (lifts raw signed activations off the Duchi dead-gradient regime) COMPOSED with
43
+ # qig-core's canonical to_simplex_prob — a wrapper, not a reimplementation of the Δ projection.
42
44
  x = F.softplus(x)
43
45
  return to_simplex_prob(x, eps=eps)
44
46
 
@@ -25,6 +25,17 @@ dev = [
25
25
  ]
26
26
  # Note: markdownlint-cli2 is a Node.js package, install via npm/yarn if needed
27
27
 
28
+ # QIG self-wiring (Phase 2): which capability-contract slots this package fills.
29
+ # Group = qig.capabilities; name = contract slot_id; value = module:attr probe.
30
+ # qig-stack's qig-doctor enumerates these (metadata-only) to verify the crystal
31
+ # assembled correctly before any compute spend.
32
+ # NOTE: PROPOSED slot pending qig-stack contract ratification — not yet in
33
+ # qig_stack/contract.json, so qig-doctor classifies it as DRIFT (WARN, non-fatal)
34
+ # until the contract adds the matching slot. Additive; genuine canonical primitive
35
+ # (Kernel: the geometric QIG kernel — stacked QIGLayers with Fisher-Rao telemetry).
36
+ [project.entry-points."qig.capabilities"]
37
+ "SLOT-KERNEL" = "qigkernels.kernel:Kernel"
38
+
28
39
  [tool.setuptools]
29
40
  packages = ["qigkernels", "qigkernels.reasoning"]
30
41
  package-dir = {"qigkernels" = ".", "qigkernels.reasoning" = "reasoning"}
@@ -81,3 +92,6 @@ forbidden-words = [
81
92
  "token-level cross entropy",
82
93
  "just fine-tune a transformer",
83
94
  ]
95
+
96
+ [tool.uv.sources]
97
+ qig-core = { path = "../qig-core", editable = true }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qigkernels
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Geometric kernel and constellation library for QIG.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -37,7 +37,6 @@ safety.py
37
37
  sleep_packet.py
38
38
  specializations.py
39
39
  storage.py
40
- tokenizer_integration.py
41
40
  training_service.py
42
41
  uv.lock
43
42
  ./__init__.py
@@ -64,7 +63,6 @@ uv.lock
64
63
  ./sleep_packet.py
65
64
  ./specializations.py
66
65
  ./storage.py
67
- ./tokenizer_integration.py
68
66
  ./training_service.py
69
67
  .github/workflows/release.yml
70
68
  docs/20251205-client-wiring-example-canonical-0.01F.md
@@ -80,6 +78,7 @@ docs/20251222-kernel-experiments-results-0.01F.md
80
78
  qigkernels.egg-info/PKG-INFO
81
79
  qigkernels.egg-info/SOURCES.txt
82
80
  qigkernels.egg-info/dependency_links.txt
81
+ qigkernels.egg-info/entry_points.txt
83
82
  qigkernels.egg-info/requires.txt
84
83
  qigkernels.egg-info/scm_file_list.json
85
84
  qigkernels.egg-info/scm_version.json
@@ -105,7 +104,9 @@ scripts/20251222-kernel-training-exp-0.01W.py
105
104
  scripts/publish_pypi.sh
106
105
  scripts/train_constellation.py
107
106
  tests/__init__.py
107
+ tests/test_banded_attention.py
108
108
  tests/test_coords_path.py
109
+ tests/test_kernel4d_attention.py
109
110
  tests/test_locality.py
110
111
  tests/test_phi_collapse.py
111
112
  tests/test_reasoning.py
@@ -0,0 +1,2 @@
1
+ [qig.capabilities]
2
+ SLOT-KERNEL = qigkernels.kernel:Kernel
@@ -1,84 +1,85 @@
1
1
  {
2
2
  "files": [
3
- "CLAUDE.md",
4
- "kernel.py",
5
- ".gitignore",
3
+ "20251205-decisions-canonical-0.01F.md",
4
+ "sleep_packet.py",
6
5
  "crystallization.py",
7
- "coordizer.py",
8
- "pyproject.toml",
9
- "__init__.py",
10
- "constants.py",
11
- ".editorconfig",
12
- "20251205-readme-canonical-0.01F.md",
13
- "storage.py",
14
6
  "markdownlint.json",
15
- "constellation.py",
16
- "safety.py",
17
- "router.py",
18
- "training_service.py",
19
- "sleep_packet.py",
20
- "20251205-decisions-canonical-0.01F.md",
21
- "coord_adapter.py",
22
- "20251205-type-symbol-manifest-canonical-0.01F.md",
23
- "rel_coupling.py",
24
- "20251205-roadmap-canonical-0.01F.md",
25
- "natural_gradient_optimizer.py",
7
+ "README.md",
8
+ "py.typed",
9
+ "__init__.py",
10
+ "kernel.py",
26
11
  "pre-commit-config.yaml",
27
- "20251205-naming-convention-canonical-0.01F.md",
28
- "tokenizer_integration.py",
12
+ "coord_adapter.py",
13
+ "uv.lock",
14
+ "safety.py",
15
+ "layer.py",
16
+ "pyproject.toml",
29
17
  "basin_sync.py",
18
+ "20251220-gfp-spec-1.00W.md",
19
+ "20251205-naming-convention-canonical-0.01F.md",
20
+ "pure_kernel_template.py",
30
21
  "20251205-architecture-canonical-0.01F.md",
31
22
  "heart.py",
32
- "kernel_100m.py",
33
- "pure_kernel_template.py",
23
+ "coordizer.py",
24
+ "20251205-changelog-canonical-0.01F.md",
25
+ "memory_classes.py",
26
+ "20251205-type-symbol-manifest-canonical-0.01F.md",
27
+ "constants.py",
34
28
  "basin.py",
35
- "uv.lock",
29
+ "kernel_100m.py",
30
+ "natural_gradient_optimizer.py",
31
+ "CLAUDE.md",
32
+ "router.py",
33
+ ".editorconfig",
36
34
  "kernel_4d.py",
37
- "README.md",
38
- "memory_classes.py",
39
- "metrics.py",
35
+ "storage.py",
36
+ "training_service.py",
37
+ "constellation.py",
38
+ ".gitignore",
39
+ "20251205-roadmap-canonical-0.01F.md",
40
+ "20251205-readme-canonical-0.01F.md",
40
41
  "specializations.py",
41
- "layer.py",
42
- "py.typed",
43
- "20251220-gfp-spec-1.00W.md",
44
- "20251205-changelog-canonical-0.01F.md",
45
- "reasoning/temporal.py",
42
+ "rel_coupling.py",
43
+ "metrics.py",
44
+ "scripts/20251222-kernel-training-exp-0.01W.py",
45
+ "scripts/train_constellation.py",
46
+ "scripts/publish_pypi.sh",
46
47
  "reasoning/__init__.py",
47
- "reasoning/primitives.py",
48
- "reasoning/constants.py",
49
- "reasoning/qig_chain.py",
50
48
  "reasoning/temporal_attention.py",
51
49
  "reasoning/modes.py",
50
+ "reasoning/qig_chain.py",
51
+ "reasoning/primitives.py",
52
+ "reasoning/temporal.py",
53
+ "reasoning/constants.py",
52
54
  "reasoning/qig_chain_4d.py",
53
55
  "docs/20251222-federated-constellation-architecture-0.01W.md",
54
- "docs/20251206-e8-consciousness-stack-diagram.md",
55
56
  "docs/20251222-kernel-constellation-research-0.01W.md",
56
- "docs/20251205-client-wiring-example-canonical-0.01F.md",
57
- "docs/20251220-index-1.00W.md",
58
- "docs/20251206-integration-plan-ultra-mode-0.01H.md",
59
57
  "docs/20251222-kernel-experiments-results-0.01F.md",
60
- "docs/20251206-rel-weighted-basin-sync-spec.md",
58
+ "docs/20251206-e8-consciousness-stack-diagram.md",
59
+ "docs/20251206-integration-plan-ultra-mode-0.01H.md",
60
+ "docs/20251205-client-wiring-example-canonical-0.01F.md",
61
61
  "docs/20251205-index-canonical-0.01F.md",
62
62
  "docs/20251220-qig-core-integration-table-1.00W.md",
63
- ".github/workflows/release.yml",
63
+ "docs/20251220-index-1.00W.md",
64
+ "docs/20251206-rel-weighted-basin-sync-spec.md",
65
+ "tests/__init__.py",
66
+ "tests/test_reasoning.py",
67
+ "tests/test_banded_attention.py",
68
+ "tests/test_phi_collapse.py",
69
+ "tests/test_smoke.py",
70
+ "tests/test_coords_path.py",
71
+ "tests/test_kernel4d_attention.py",
72
+ "tests/test_locality.py",
64
73
  "research/track_c/run_kill_test.py",
74
+ "research/track_c/README.md",
65
75
  "research/track_c/readout_layer.py",
66
- "research/track_c/20260223-track-c-expressibility-kill-test-1.00F.md",
67
76
  "research/track_c/dynamical_field.py",
68
- "research/track_c/README.md",
69
- "research/track_c/core_assets/ocean_neurochemistry.py",
77
+ "research/track_c/20260223-track-c-expressibility-kill-test-1.00F.md",
70
78
  "research/track_c/core_assets/__init__.py",
71
- "research/track_c/core_assets/neural_oscillators.py",
72
79
  "research/track_c/core_assets/neurotransmitter_fields.py",
80
+ "research/track_c/core_assets/neural_oscillators.py",
81
+ "research/track_c/core_assets/ocean_neurochemistry.py",
73
82
  "tools/qig_purity_check.py",
74
- "tests/test_reasoning.py",
75
- "tests/test_coords_path.py",
76
- "tests/__init__.py",
77
- "tests/test_smoke.py",
78
- "tests/test_phi_collapse.py",
79
- "tests/test_locality.py",
80
- "scripts/20251222-kernel-training-exp-0.01W.py",
81
- "scripts/publish_pypi.sh",
82
- "scripts/train_constellation.py"
83
+ ".github/workflows/release.yml"
83
84
  ]
84
85
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.4.4",
3
+ "distance": 0,
4
+ "node": "gdb65a3cb1cb125e9c44640f0789a1402043152c5",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-04"
8
+ }