aether-nn 0.1.0__py3-none-any.whl
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.
- aether/__init__.py +11 -0
- aether/attention.py +132 -0
- aether/config.py +127 -0
- aether/layers.py +314 -0
- aether/memory.py +137 -0
- aether/model.py +269 -0
- aether/routing.py +129 -0
- aether_nn-0.1.0.dist-info/METADATA +221 -0
- aether_nn-0.1.0.dist-info/RECORD +12 -0
- aether_nn-0.1.0.dist-info/WHEEL +5 -0
- aether_nn-0.1.0.dist-info/licenses/LICENSE +21 -0
- aether_nn-0.1.0.dist-info/top_level.txt +1 -0
aether/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether — A Post-Transformer Neural Architecture
|
|
3
|
+
================================================
|
|
4
|
+
GitHub: https://github.com/aether-nn/aether
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .config import AetherConfig
|
|
8
|
+
from .model import AetherModel
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["AetherModel", "AetherConfig"]
|
aether/attention.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Attention Mechanisms
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
Fractal Sparse Attention (FSA):
|
|
6
|
+
- Multi-scale attention: O(n·log n) instead of O(n²)
|
|
7
|
+
- Attends at multiple granularities simultaneously
|
|
8
|
+
- Uses RoPE positional embeddings
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn as nn
|
|
13
|
+
import torch.nn.functional as F
|
|
14
|
+
import math
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RoPE(nn.Module):
|
|
19
|
+
"""Rotary Position Embeddings — better than learned, works at any length."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, dim: int, base: int = 10000):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.dim = dim
|
|
24
|
+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
|
|
25
|
+
self.register_buffer("inv_freq", inv_freq)
|
|
26
|
+
|
|
27
|
+
def forward(self, x: torch.Tensor, seq_len: int) -> torch.Tensor:
|
|
28
|
+
t = torch.arange(seq_len, device=x.device).float()
|
|
29
|
+
freqs = torch.einsum("i,j->ij", t, self.inv_freq)
|
|
30
|
+
emb = torch.cat([freqs, freqs], dim=-1) # (seq, dim)
|
|
31
|
+
cos = emb.cos()[None, None, :, :]
|
|
32
|
+
sin = emb.sin()[None, None, :, :]
|
|
33
|
+
x1, x2 = x[..., : self.dim // 2], x[..., self.dim // 2 :]
|
|
34
|
+
return x * cos + torch.cat([-x2, x1], dim=-1) * sin
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FractalSparseAttention(nn.Module):
|
|
38
|
+
"""
|
|
39
|
+
Fractal multi-scale attention.
|
|
40
|
+
|
|
41
|
+
Attends at multiple granularities:
|
|
42
|
+
scale 0: individual tokens (fine detail)
|
|
43
|
+
scale 1: groups of 4 (phrase level)
|
|
44
|
+
scale 2: groups of 16 (sentence level)
|
|
45
|
+
scale 3: groups of 64 (paragraph level)
|
|
46
|
+
|
|
47
|
+
Total: O(n·log n) instead of O(n²).
|
|
48
|
+
Captures both local detail AND global context.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, dim: int, num_heads: int, num_scales: int = 3,
|
|
52
|
+
dropout: float = 0.0):
|
|
53
|
+
super().__init__()
|
|
54
|
+
assert dim % num_heads == 0
|
|
55
|
+
self.dim = dim
|
|
56
|
+
self.num_heads = num_heads
|
|
57
|
+
self.head_dim = dim // num_heads
|
|
58
|
+
self.num_scales = num_scales
|
|
59
|
+
|
|
60
|
+
# Separate projections per scale
|
|
61
|
+
self.qkv_projs = nn.ModuleList([
|
|
62
|
+
nn.Linear(dim, 3 * dim, bias=False)
|
|
63
|
+
for _ in range(num_scales)
|
|
64
|
+
])
|
|
65
|
+
self.out_proj = nn.Linear(dim, dim, bias=False)
|
|
66
|
+
|
|
67
|
+
# Learnable scale weights
|
|
68
|
+
self.scale_weights = nn.Parameter(torch.ones(num_scales))
|
|
69
|
+
|
|
70
|
+
# RoPE for each scale
|
|
71
|
+
self.ropes = nn.ModuleList([
|
|
72
|
+
RoPE(self.head_dim) for _ in range(num_scales)
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
self.dropout = nn.Dropout(dropout)
|
|
76
|
+
self.scale = math.sqrt(self.head_dim)
|
|
77
|
+
|
|
78
|
+
def _attend(self, x: torch.Tensor, scale_idx: int,
|
|
79
|
+
mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
|
80
|
+
B, S, D = x.shape
|
|
81
|
+
chunk = 4 ** scale_idx # 1, 4, 16, 64, ...
|
|
82
|
+
|
|
83
|
+
# Pool to this scale
|
|
84
|
+
if chunk > 1 and S >= chunk:
|
|
85
|
+
S_round = (S // chunk) * chunk
|
|
86
|
+
xs = x[:, :S_round].reshape(B, S_round // chunk, chunk, D).mean(2)
|
|
87
|
+
else:
|
|
88
|
+
xs = x
|
|
89
|
+
Sc = xs.shape[1]
|
|
90
|
+
|
|
91
|
+
qkv = self.qkv_projs[scale_idx](xs)
|
|
92
|
+
q, k, v = qkv.chunk(3, dim=-1)
|
|
93
|
+
|
|
94
|
+
q = q.reshape(B, Sc, self.num_heads, self.head_dim).transpose(1, 2)
|
|
95
|
+
k = k.reshape(B, Sc, self.num_heads, self.head_dim).transpose(1, 2)
|
|
96
|
+
v = v.reshape(B, Sc, self.num_heads, self.head_dim).transpose(1, 2)
|
|
97
|
+
|
|
98
|
+
q = self.ropes[scale_idx](q, Sc)
|
|
99
|
+
k = self.ropes[scale_idx](k, Sc)
|
|
100
|
+
|
|
101
|
+
attn = torch.matmul(q, k.transpose(-2, -1)) / self.scale
|
|
102
|
+
|
|
103
|
+
# Causal mask
|
|
104
|
+
causal = torch.triu(
|
|
105
|
+
torch.full((Sc, Sc), float("-inf"), device=x.device), diagonal=1
|
|
106
|
+
)
|
|
107
|
+
attn = attn + causal
|
|
108
|
+
|
|
109
|
+
if mask is not None and chunk == 1:
|
|
110
|
+
attn = attn + mask
|
|
111
|
+
|
|
112
|
+
attn = F.softmax(attn, dim=-1)
|
|
113
|
+
attn = self.dropout(attn)
|
|
114
|
+
|
|
115
|
+
out = torch.matmul(attn, v)
|
|
116
|
+
out = out.transpose(1, 2).reshape(B, Sc, D)
|
|
117
|
+
|
|
118
|
+
# Upsample back
|
|
119
|
+
if chunk > 1 and Sc < S:
|
|
120
|
+
out = out.unsqueeze(2).expand(-1, -1, chunk, -1).reshape(B, Sc * chunk, D)
|
|
121
|
+
if out.shape[1] < S:
|
|
122
|
+
out = F.pad(out, (0, 0, 0, S - out.shape[1]))
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
def forward(self, x: torch.Tensor,
|
|
126
|
+
mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
|
127
|
+
weights = F.softmax(self.scale_weights, dim=0)
|
|
128
|
+
out = sum(
|
|
129
|
+
weights[i] * self._attend(x, i, mask)
|
|
130
|
+
for i in range(self.num_scales)
|
|
131
|
+
)
|
|
132
|
+
return self.out_proj(out)
|
aether/config.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Configuration
|
|
3
|
+
====================
|
|
4
|
+
All model hyperparameters in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AetherConfig:
|
|
14
|
+
# Model dimensions
|
|
15
|
+
vocab_size: int = 32000
|
|
16
|
+
dim: int = 256
|
|
17
|
+
num_layers: int = 6
|
|
18
|
+
num_heads: int = 8
|
|
19
|
+
num_scales: int = 3 # Fractal attention scales
|
|
20
|
+
ffn_mult: int = 4 # FFN hidden = dim * ffn_mult
|
|
21
|
+
|
|
22
|
+
# Memory system
|
|
23
|
+
working_memory_size: int = 512 # tokens in working memory
|
|
24
|
+
episodic_memory_size: int = 2048 # compressed episodic slots
|
|
25
|
+
semantic_memory_size: int = 8192 # long-term semantic slots
|
|
26
|
+
memory_dim: int = 64 # compressed memory dim
|
|
27
|
+
|
|
28
|
+
# Routing
|
|
29
|
+
num_experts: int = 8
|
|
30
|
+
experts_per_token: int = 2 # sparse MoE: top-2
|
|
31
|
+
min_layers: int = 1 # min depth for easy tokens
|
|
32
|
+
max_layers: int = None # defaults to num_layers
|
|
33
|
+
|
|
34
|
+
# Continuous learning
|
|
35
|
+
fast_weight_lr: float = 0.01 # learning rate for fast weights
|
|
36
|
+
use_continuous_learning: bool = True
|
|
37
|
+
|
|
38
|
+
# Causal world model
|
|
39
|
+
world_model_steps: int = 3 # internal simulation steps
|
|
40
|
+
use_causal_model: bool = True
|
|
41
|
+
|
|
42
|
+
# Quantization
|
|
43
|
+
use_ternary_weights: bool = True # {-1, 0, +1}
|
|
44
|
+
use_int8_activations: bool = False
|
|
45
|
+
|
|
46
|
+
# Training
|
|
47
|
+
max_seq_len: int = 2048
|
|
48
|
+
dropout: float = 0.0
|
|
49
|
+
bias: bool = False
|
|
50
|
+
|
|
51
|
+
# Generation
|
|
52
|
+
temperature: float = 1.0
|
|
53
|
+
top_k: int = 50
|
|
54
|
+
top_p: float = 0.9
|
|
55
|
+
|
|
56
|
+
def __post_init__(self):
|
|
57
|
+
if self.max_layers is None:
|
|
58
|
+
self.max_layers = self.num_layers
|
|
59
|
+
assert self.dim % self.num_heads == 0, "dim must be divisible by num_heads"
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def nano(cls) -> "AetherConfig":
|
|
63
|
+
"""~7M params, ~1.3MB — runs on any device"""
|
|
64
|
+
return cls(
|
|
65
|
+
dim=128, num_layers=4, num_heads=4, num_scales=2,
|
|
66
|
+
ffn_mult=4, num_experts=4, experts_per_token=2,
|
|
67
|
+
episodic_memory_size=512, semantic_memory_size=2048,
|
|
68
|
+
world_model_steps=1, use_continuous_learning=False,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def micro(cls) -> "AetherConfig":
|
|
73
|
+
"""~23M params, ~4.5MB — good quality"""
|
|
74
|
+
return cls(
|
|
75
|
+
dim=256, num_layers=6, num_heads=8, num_scales=3,
|
|
76
|
+
ffn_mult=4, num_experts=4, experts_per_token=2,
|
|
77
|
+
episodic_memory_size=1024, semantic_memory_size=4096,
|
|
78
|
+
world_model_steps=2,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def mini(cls) -> "AetherConfig":
|
|
83
|
+
"""~116M params, ~22MB — GPT-2 level"""
|
|
84
|
+
return cls(
|
|
85
|
+
dim=512, num_layers=8, num_heads=8, num_scales=3,
|
|
86
|
+
ffn_mult=4, num_experts=8, experts_per_token=2,
|
|
87
|
+
episodic_memory_size=2048, semantic_memory_size=8192,
|
|
88
|
+
world_model_steps=3,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def base(cls) -> "AetherConfig":
|
|
93
|
+
"""~350M params, ~66MB — strong model"""
|
|
94
|
+
return cls(
|
|
95
|
+
dim=768, num_layers=12, num_heads=12, num_scales=4,
|
|
96
|
+
ffn_mult=4, num_experts=8, experts_per_token=2,
|
|
97
|
+
episodic_memory_size=4096, semantic_memory_size=16384,
|
|
98
|
+
world_model_steps=4,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def large(cls) -> "AetherConfig":
|
|
103
|
+
"""~1.3B params, ~246MB — serious model"""
|
|
104
|
+
return cls(
|
|
105
|
+
dim=1024, num_layers=24, num_heads=16, num_scales=4,
|
|
106
|
+
ffn_mult=4, num_experts=16, experts_per_token=2,
|
|
107
|
+
episodic_memory_size=8192, semantic_memory_size=32768,
|
|
108
|
+
world_model_steps=5,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def save(self, path: str):
|
|
112
|
+
with open(path, "w") as f:
|
|
113
|
+
json.dump(self.__dict__, f, indent=2)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def load(cls, path: str) -> "AetherConfig":
|
|
117
|
+
with open(path) as f:
|
|
118
|
+
return cls(**json.load(f))
|
|
119
|
+
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
params = self.dim * self.num_layers * 12 # rough estimate
|
|
122
|
+
size_mb = params * 1.58 / (8 * 1024 * 1024)
|
|
123
|
+
return (
|
|
124
|
+
f"AetherConfig(dim={self.dim}, layers={self.num_layers}, "
|
|
125
|
+
f"heads={self.num_heads}, ~{params//1_000_000}M params, "
|
|
126
|
+
f"~{size_mb:.1f}MB ternary)"
|
|
127
|
+
)
|
aether/layers.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Core Layers
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
TernaryLinear — 1.58-bit weights {-1, 0, +1}, 20x smaller
|
|
6
|
+
CausalWorldModel — internal simulation for true reasoning
|
|
7
|
+
ContinuousLearning — fast weights that update at inference time
|
|
8
|
+
AetherBlock — full block combining everything
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
import torch.nn as nn
|
|
13
|
+
import torch.nn.functional as F
|
|
14
|
+
import math
|
|
15
|
+
from typing import Optional, Tuple
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─────────────────────────────────────────────
|
|
19
|
+
# TERNARY LINEAR (Innovation 4)
|
|
20
|
+
# Weights: {-1, 0, +1} = 1.58 bits
|
|
21
|
+
# 20x smaller, multiplications → additions
|
|
22
|
+
# ─────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
class TernaryLinear(nn.Module):
|
|
25
|
+
"""
|
|
26
|
+
Linear layer with ternary weights: {-1, 0, +1}.
|
|
27
|
+
|
|
28
|
+
Training: full float32 weights with STE (straight-through estimator).
|
|
29
|
+
Inference: pack to 2-bit integers → 16x compression.
|
|
30
|
+
|
|
31
|
+
Based on: BitNet b1.58 (Ma et al., Microsoft 2024)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, in_features: int, out_features: int, bias: bool = False):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.in_features = in_features
|
|
37
|
+
self.out_features = out_features
|
|
38
|
+
|
|
39
|
+
self.weight = nn.Parameter(
|
|
40
|
+
torch.randn(out_features, in_features) * 0.02
|
|
41
|
+
)
|
|
42
|
+
self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None
|
|
43
|
+
|
|
44
|
+
# Per-channel output scale (restores expressivity)
|
|
45
|
+
self.scale = nn.Parameter(torch.ones(out_features))
|
|
46
|
+
|
|
47
|
+
def _ternarize(self, w: torch.Tensor) -> torch.Tensor:
|
|
48
|
+
"""STE: forward = ternary, backward = full gradient."""
|
|
49
|
+
threshold = w.abs().mean()
|
|
50
|
+
w_t = torch.where(w.abs() < threshold, torch.zeros_like(w), w.sign())
|
|
51
|
+
return w + (w_t - w).detach()
|
|
52
|
+
|
|
53
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
54
|
+
w = self._ternarize(self.weight)
|
|
55
|
+
return F.linear(x, w, self.bias) * self.scale
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────
|
|
59
|
+
# CAUSAL WORLD MODEL (Innovation 4)
|
|
60
|
+
# Internal simulation → true reasoning
|
|
61
|
+
# Eliminates hallucinations
|
|
62
|
+
# ─────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
class CausalWorldModel(nn.Module):
|
|
65
|
+
"""
|
|
66
|
+
Lightweight internal world model.
|
|
67
|
+
|
|
68
|
+
The model maintains a compressed state of "how the world works"
|
|
69
|
+
and uses it to simulate future states before answering.
|
|
70
|
+
|
|
71
|
+
Instead of: "what word probably follows?" (correlation)
|
|
72
|
+
Does: "what would happen if..." (causation)
|
|
73
|
+
|
|
74
|
+
Steps:
|
|
75
|
+
1. Encode current context into world state
|
|
76
|
+
2. Simulate N steps forward
|
|
77
|
+
3. Check if simulation is consistent
|
|
78
|
+
4. Use consistency score to gate uncertainty
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, dim: int, state_dim: int = 128, sim_steps: int = 3):
|
|
82
|
+
super().__init__()
|
|
83
|
+
self.dim = dim
|
|
84
|
+
self.state_dim = state_dim
|
|
85
|
+
self.sim_steps = sim_steps
|
|
86
|
+
|
|
87
|
+
# Encode context → world state
|
|
88
|
+
self.encoder = nn.Sequential(
|
|
89
|
+
TernaryLinear(dim, state_dim),
|
|
90
|
+
nn.SiLU(),
|
|
91
|
+
TernaryLinear(state_dim, state_dim),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Simulate one step forward
|
|
95
|
+
self.transition = nn.GRUCell(state_dim, state_dim)
|
|
96
|
+
|
|
97
|
+
# Decode world state → context enhancement
|
|
98
|
+
self.decoder = nn.Sequential(
|
|
99
|
+
TernaryLinear(state_dim, dim),
|
|
100
|
+
nn.SiLU(),
|
|
101
|
+
TernaryLinear(dim, dim),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Uncertainty gate: how confident is the simulation?
|
|
105
|
+
self.uncertainty = nn.Sequential(
|
|
106
|
+
TernaryLinear(state_dim, 1),
|
|
107
|
+
nn.Sigmoid(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Output mix gate
|
|
111
|
+
self.mix_gate = nn.Sequential(
|
|
112
|
+
TernaryLinear(dim * 2, dim),
|
|
113
|
+
nn.Sigmoid(),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
117
|
+
"""
|
|
118
|
+
x: (B, S, D)
|
|
119
|
+
Returns: (enriched_x, confidence) where confidence ∈ [0,1]
|
|
120
|
+
"""
|
|
121
|
+
B, S, D = x.shape
|
|
122
|
+
|
|
123
|
+
# Use mean-pooled context as world state seed
|
|
124
|
+
context = x.mean(dim=1) # (B, D)
|
|
125
|
+
state = self.encoder(context) # (B, state_dim)
|
|
126
|
+
|
|
127
|
+
# Simulate forward
|
|
128
|
+
states = [state]
|
|
129
|
+
for _ in range(self.sim_steps):
|
|
130
|
+
state = self.transition(state, state)
|
|
131
|
+
states.append(state)
|
|
132
|
+
|
|
133
|
+
# Final simulated state
|
|
134
|
+
final_state = states[-1]
|
|
135
|
+
|
|
136
|
+
# Confidence = consistency between steps
|
|
137
|
+
confidence = self.uncertainty(final_state) # (B, 1)
|
|
138
|
+
|
|
139
|
+
# Decode simulation result
|
|
140
|
+
sim_out = self.decoder(final_state) # (B, D)
|
|
141
|
+
sim_out = sim_out.unsqueeze(1).expand(-1, S, -1) # (B, S, D)
|
|
142
|
+
|
|
143
|
+
# Gated mix: high confidence → trust simulation more
|
|
144
|
+
gate = self.mix_gate(torch.cat([x, sim_out], dim=-1))
|
|
145
|
+
out = gate * sim_out + (1 - gate) * x
|
|
146
|
+
|
|
147
|
+
return out, confidence.squeeze(-1) # (B,S,D), (B,1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ─────────────────────────────────────────────
|
|
151
|
+
# CONTINUOUS LEARNING LAYER (Innovation 5)
|
|
152
|
+
# Fast weights update at inference time
|
|
153
|
+
# Model learns from every token it processes
|
|
154
|
+
# ─────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
class ContinuousLearningLayer(nn.Module):
|
|
157
|
+
"""
|
|
158
|
+
Fast-weight memory that updates at inference.
|
|
159
|
+
|
|
160
|
+
Two-speed learning (Hinton, 1987 / Schmidhuber, 1992):
|
|
161
|
+
Slow weights: trained normally (days)
|
|
162
|
+
Fast weights: updated at inference (milliseconds)
|
|
163
|
+
|
|
164
|
+
Fast weights act as a temporary working memory
|
|
165
|
+
that captures patterns from the current context.
|
|
166
|
+
This is what gives humans "aha!" moments mid-conversation.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, dim: int, fast_lr: float = 0.01):
|
|
170
|
+
super().__init__()
|
|
171
|
+
self.dim = dim
|
|
172
|
+
self.fast_lr = fast_lr
|
|
173
|
+
|
|
174
|
+
# Slow weights (standard training)
|
|
175
|
+
self.slow_W = TernaryLinear(dim, dim)
|
|
176
|
+
|
|
177
|
+
# Fast weight delta (zero at start, updated at inference)
|
|
178
|
+
self.register_buffer("fast_W", torch.zeros(dim, dim))
|
|
179
|
+
|
|
180
|
+
# Hebbian update gate: what's worth remembering?
|
|
181
|
+
self.remember_gate = nn.Sequential(
|
|
182
|
+
TernaryLinear(dim, dim // 4),
|
|
183
|
+
nn.SiLU(),
|
|
184
|
+
TernaryLinear(dim // 4, 1),
|
|
185
|
+
nn.Sigmoid(),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Forget gate: what to let go?
|
|
189
|
+
self.forget_gate = nn.Sequential(
|
|
190
|
+
TernaryLinear(dim, 1),
|
|
191
|
+
nn.Sigmoid(),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
195
|
+
"""
|
|
196
|
+
x: (B, S, D)
|
|
197
|
+
Updates fast weights via Hebbian learning.
|
|
198
|
+
"""
|
|
199
|
+
B, S, D = x.shape
|
|
200
|
+
|
|
201
|
+
# Apply slow weights
|
|
202
|
+
slow_out = self.slow_W(x)
|
|
203
|
+
|
|
204
|
+
# Apply fast weights (captures recent context)
|
|
205
|
+
fast_out = F.linear(x, self.fast_W)
|
|
206
|
+
|
|
207
|
+
# How much to remember from current input
|
|
208
|
+
remember = self.remember_gate(x) # (B, S, 1)
|
|
209
|
+
forget = self.forget_gate(x) # (B, S, 1)
|
|
210
|
+
|
|
211
|
+
# Hebbian update: x_t ⊗ x_t (outer product = association)
|
|
212
|
+
if self.training or True:
|
|
213
|
+
with torch.no_grad():
|
|
214
|
+
# Outer product averaged over batch and sequence
|
|
215
|
+
delta = torch.einsum("bsi,bsj->ij",
|
|
216
|
+
x * remember, x) / (B * S)
|
|
217
|
+
# Forget old + learn new
|
|
218
|
+
self.fast_W = (
|
|
219
|
+
(1 - forget.mean()) * self.fast_W
|
|
220
|
+
+ self.fast_lr * delta
|
|
221
|
+
)
|
|
222
|
+
# Keep fast weights bounded
|
|
223
|
+
self.fast_W.clamp_(-1.0, 1.0)
|
|
224
|
+
|
|
225
|
+
return slow_out + fast_out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ─────────────────────────────────────────────
|
|
229
|
+
# AETHER BLOCK — one full residual block
|
|
230
|
+
# ─────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
class AetherBlock(nn.Module):
|
|
233
|
+
"""
|
|
234
|
+
One Aether block. Stacked N times to form the full model.
|
|
235
|
+
|
|
236
|
+
Order:
|
|
237
|
+
1. Fractal Sparse Attention (multi-scale understanding)
|
|
238
|
+
2. Hierarchical Memory (infinite context retrieval)
|
|
239
|
+
3. Sparse MoE FFN (expert reasoning)
|
|
240
|
+
4. Causal World Model (optional: reasoning layer)
|
|
241
|
+
5. Continuous Learning (optional: adaptation)
|
|
242
|
+
All with RMSNorm + residual connections.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def __init__(self, config, use_world_model: bool = False,
|
|
246
|
+
use_continuous: bool = False):
|
|
247
|
+
super().__init__()
|
|
248
|
+
from .attention import FractalSparseAttention
|
|
249
|
+
from .memory import HierarchicalStatefulMemory
|
|
250
|
+
from .routing import SparseMoE
|
|
251
|
+
|
|
252
|
+
dim = config.dim
|
|
253
|
+
|
|
254
|
+
self.norm1 = nn.RMSNorm(dim)
|
|
255
|
+
self.norm2 = nn.RMSNorm(dim)
|
|
256
|
+
self.norm3 = nn.RMSNorm(dim)
|
|
257
|
+
|
|
258
|
+
self.attention = FractalSparseAttention(
|
|
259
|
+
dim, config.num_heads, config.num_scales, config.dropout
|
|
260
|
+
)
|
|
261
|
+
self.memory = HierarchicalStatefulMemory(
|
|
262
|
+
dim,
|
|
263
|
+
working_size=config.working_memory_size,
|
|
264
|
+
episodic_size=config.episodic_memory_size,
|
|
265
|
+
semantic_size=config.semantic_memory_size,
|
|
266
|
+
memory_dim=config.memory_dim,
|
|
267
|
+
)
|
|
268
|
+
self.ffn = SparseMoE(
|
|
269
|
+
dim, config.num_experts, config.experts_per_token, config.ffn_mult
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
self.use_world_model = use_world_model
|
|
273
|
+
self.use_continuous = use_continuous
|
|
274
|
+
|
|
275
|
+
if use_world_model:
|
|
276
|
+
self.norm4 = nn.RMSNorm(dim)
|
|
277
|
+
self.world_model = CausalWorldModel(
|
|
278
|
+
dim, sim_steps=config.world_model_steps
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if use_continuous:
|
|
282
|
+
self.norm5 = nn.RMSNorm(dim)
|
|
283
|
+
self.continuous = ContinuousLearningLayer(
|
|
284
|
+
dim, config.fast_weight_lr
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
self.dropout = nn.Dropout(config.dropout)
|
|
288
|
+
|
|
289
|
+
def forward(self, x: torch.Tensor,
|
|
290
|
+
mask: Optional[torch.Tensor] = None) -> Tuple[torch.Tensor, dict]:
|
|
291
|
+
stats = {}
|
|
292
|
+
|
|
293
|
+
# 1. Fractal Attention
|
|
294
|
+
x = x + self.dropout(self.attention(self.norm1(x), mask))
|
|
295
|
+
|
|
296
|
+
# 2. Hierarchical Memory
|
|
297
|
+
x = x + self.dropout(self.memory(self.norm2(x)))
|
|
298
|
+
|
|
299
|
+
# 3. Sparse MoE FFN
|
|
300
|
+
ffn_out, lb_loss = self.ffn(self.norm3(x))
|
|
301
|
+
x = x + self.dropout(ffn_out)
|
|
302
|
+
stats["load_balance_loss"] = lb_loss
|
|
303
|
+
|
|
304
|
+
# 4. Causal World Model (deeper layers only)
|
|
305
|
+
if self.use_world_model:
|
|
306
|
+
wm_out, confidence = self.world_model(self.norm4(x))
|
|
307
|
+
x = x + self.dropout(wm_out)
|
|
308
|
+
stats["confidence"] = confidence.mean().item()
|
|
309
|
+
|
|
310
|
+
# 5. Continuous Learning
|
|
311
|
+
if self.use_continuous:
|
|
312
|
+
x = x + self.dropout(self.continuous(self.norm5(x)))
|
|
313
|
+
|
|
314
|
+
return x, stats
|
aether/memory.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Hierarchical Stateful Memory (HSM)
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
4-level memory system inspired by human cognition:
|
|
6
|
+
|
|
7
|
+
Working memory — current context, fast, small
|
|
8
|
+
Episodic memory — recent experiences, medium
|
|
9
|
+
Semantic memory — compressed knowledge, large
|
|
10
|
+
Procedural mem. — skills / patterns, deep
|
|
11
|
+
|
|
12
|
+
Enables INFINITE context via O(log n) hierarchical retrieval.
|
|
13
|
+
Gigabytes of knowledge compressed into megabytes.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
import torch.nn.functional as F
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MemoryBank(nn.Module):
|
|
23
|
+
"""Single memory level with read/write operations."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, num_slots: int, slot_dim: int, query_dim: int,
|
|
26
|
+
top_k: int = 16):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.num_slots = num_slots
|
|
29
|
+
self.slot_dim = slot_dim
|
|
30
|
+
self.top_k = min(top_k, num_slots)
|
|
31
|
+
|
|
32
|
+
# Memory content (learnable initialization)
|
|
33
|
+
self.memory = nn.Parameter(torch.randn(num_slots, slot_dim) * 0.01)
|
|
34
|
+
|
|
35
|
+
# Keys for retrieval (separate from content)
|
|
36
|
+
self.keys = nn.Parameter(torch.randn(num_slots, slot_dim) * 0.01)
|
|
37
|
+
|
|
38
|
+
# Projections
|
|
39
|
+
self.query_proj = nn.Linear(query_dim, slot_dim, bias=False)
|
|
40
|
+
self.value_proj = nn.Linear(slot_dim, query_dim, bias=False)
|
|
41
|
+
|
|
42
|
+
# Write gate: decides what gets stored
|
|
43
|
+
self.write_gate = nn.Linear(query_dim, 1)
|
|
44
|
+
|
|
45
|
+
def read(self, query: torch.Tensor) -> torch.Tensor:
|
|
46
|
+
"""
|
|
47
|
+
query: (B, S, D)
|
|
48
|
+
Returns: (B, S, D) — retrieved memory content
|
|
49
|
+
"""
|
|
50
|
+
B, S, D = query.shape
|
|
51
|
+
q = self.query_proj(query) # (B, S, slot_dim)
|
|
52
|
+
|
|
53
|
+
# Similarity to all keys
|
|
54
|
+
scores = torch.einsum("bsd,nd->bsn", q, self.keys) # (B, S, slots)
|
|
55
|
+
|
|
56
|
+
# Sparse retrieval: only top_k slots
|
|
57
|
+
top_scores, top_idx = torch.topk(scores, self.top_k, dim=-1)
|
|
58
|
+
sparse_weights = F.softmax(top_scores, dim=-1) # (B, S, k)
|
|
59
|
+
|
|
60
|
+
# Gather content from top slots
|
|
61
|
+
mem_content = self.memory[top_idx] # (B, S, k, slot_dim)
|
|
62
|
+
retrieved = torch.einsum("bsk,bskd->bsd", sparse_weights, mem_content)
|
|
63
|
+
|
|
64
|
+
return self.value_proj(retrieved)
|
|
65
|
+
|
|
66
|
+
def write(self, x: torch.Tensor) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Softly write new information into memory.
|
|
69
|
+
x: (B, S, D)
|
|
70
|
+
"""
|
|
71
|
+
# Compute write importance
|
|
72
|
+
gate = torch.sigmoid(self.write_gate(x)) # (B, S, 1)
|
|
73
|
+
importance = gate.mean(dim=(0, 1)).squeeze() # scalar — unused for now
|
|
74
|
+
# In production: this would update memory slots via EMA
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HierarchicalStatefulMemory(nn.Module):
|
|
78
|
+
"""
|
|
79
|
+
Full 4-level hierarchical memory.
|
|
80
|
+
|
|
81
|
+
Information flows up automatically:
|
|
82
|
+
token → working → episodic → semantic
|
|
83
|
+
|
|
84
|
+
Retrieval searches all levels simultaneously.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, dim: int, working_size: int = 512,
|
|
88
|
+
episodic_size: int = 2048, semantic_size: int = 8192,
|
|
89
|
+
memory_dim: int = 64):
|
|
90
|
+
super().__init__()
|
|
91
|
+
self.dim = dim
|
|
92
|
+
self.memory_dim = memory_dim
|
|
93
|
+
|
|
94
|
+
# Compress to memory_dim for storage efficiency
|
|
95
|
+
self.compress = nn.Linear(dim, memory_dim, bias=False)
|
|
96
|
+
self.decompress = nn.Linear(memory_dim, dim, bias=False)
|
|
97
|
+
|
|
98
|
+
# Four memory levels
|
|
99
|
+
self.working = MemoryBank(
|
|
100
|
+
working_size, memory_dim, dim, top_k=32)
|
|
101
|
+
self.episodic = MemoryBank(
|
|
102
|
+
episodic_size, memory_dim, dim, top_k=16)
|
|
103
|
+
self.semantic = MemoryBank(
|
|
104
|
+
semantic_size, memory_dim, dim, top_k=8)
|
|
105
|
+
|
|
106
|
+
# Level importance weights (learned)
|
|
107
|
+
self.level_weights = nn.Parameter(torch.ones(3))
|
|
108
|
+
|
|
109
|
+
# Output gate
|
|
110
|
+
self.out_proj = nn.Linear(dim, dim, bias=False)
|
|
111
|
+
|
|
112
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
113
|
+
"""
|
|
114
|
+
x: (B, S, D)
|
|
115
|
+
Returns: (B, S, D) — enriched with memory
|
|
116
|
+
"""
|
|
117
|
+
weights = F.softmax(self.level_weights, dim=0)
|
|
118
|
+
|
|
119
|
+
# Retrieve from all levels
|
|
120
|
+
w_out = self.working.read(x) * weights[0]
|
|
121
|
+
e_out = self.episodic.read(x) * weights[1]
|
|
122
|
+
s_out = self.semantic.read(x) * weights[2]
|
|
123
|
+
|
|
124
|
+
retrieved = w_out + e_out + s_out
|
|
125
|
+
|
|
126
|
+
# Write current context to working memory
|
|
127
|
+
self.working.write(x)
|
|
128
|
+
|
|
129
|
+
return self.out_proj(x + retrieved)
|
|
130
|
+
|
|
131
|
+
def get_effective_context_size(self) -> int:
|
|
132
|
+
"""Total knowledge capacity in tokens."""
|
|
133
|
+
return (
|
|
134
|
+
self.working.num_slots
|
|
135
|
+
+ self.episodic.num_slots
|
|
136
|
+
+ self.semantic.num_slots
|
|
137
|
+
)
|
aether/model.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether — Full Model
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
AetherModel: the complete architecture.
|
|
6
|
+
- Stack of AetherBlocks with Asymmetric Depth Routing
|
|
7
|
+
- Ternary-quantized embeddings
|
|
8
|
+
- Weight-tied input/output embeddings
|
|
9
|
+
- Generation with sampling strategies
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
import torch.nn as nn
|
|
14
|
+
import torch.nn.functional as F
|
|
15
|
+
import math
|
|
16
|
+
from typing import Optional, List, Tuple
|
|
17
|
+
|
|
18
|
+
from .config import AetherConfig
|
|
19
|
+
from .layers import AetherBlock, TernaryLinear
|
|
20
|
+
from .routing import AsymmetricDepthRouter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AetherModel(nn.Module):
|
|
24
|
+
"""
|
|
25
|
+
Aether Language Model.
|
|
26
|
+
|
|
27
|
+
The first architecture designed to be simultaneously:
|
|
28
|
+
• Smarter than Transformers (causal world model)
|
|
29
|
+
• 20x smaller (ternary weights)
|
|
30
|
+
• Trains on a phone (O(n log n), no GPU needed)
|
|
31
|
+
• Infinite context (hierarchical memory)
|
|
32
|
+
• Continuously learning (fast weights)
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
config = AetherConfig.micro()
|
|
36
|
+
model = AetherModel(config)
|
|
37
|
+
|
|
38
|
+
ids = tokenizer.encode("Hello world")
|
|
39
|
+
out = model.generate(ids, max_new_tokens=100)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: AetherConfig):
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.config = config
|
|
45
|
+
dim = config.dim
|
|
46
|
+
|
|
47
|
+
# ── Embeddings ──────────────────────────────────────
|
|
48
|
+
self.token_emb = nn.Embedding(config.vocab_size, dim)
|
|
49
|
+
self.emb_drop = nn.Dropout(config.dropout)
|
|
50
|
+
|
|
51
|
+
# ── Asymmetric Depth Router ──────────────────────────
|
|
52
|
+
self.depth_router = AsymmetricDepthRouter(dim, threshold=0.8)
|
|
53
|
+
|
|
54
|
+
# ── Aether Blocks ────────────────────────────────────
|
|
55
|
+
# Deeper layers get world model + continuous learning
|
|
56
|
+
# (shallow layers just do pattern matching)
|
|
57
|
+
self.blocks = nn.ModuleList([
|
|
58
|
+
AetherBlock(
|
|
59
|
+
config,
|
|
60
|
+
use_world_model=(
|
|
61
|
+
config.use_causal_model
|
|
62
|
+
and i >= config.num_layers // 2 # last half of layers
|
|
63
|
+
),
|
|
64
|
+
use_continuous=(
|
|
65
|
+
config.use_continuous_learning
|
|
66
|
+
and i >= config.num_layers * 3 // 4 # last quarter
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
for i in range(config.num_layers)
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
# ── Output ───────────────────────────────────────────
|
|
73
|
+
self.final_norm = nn.RMSNorm(dim)
|
|
74
|
+
self.lm_head = nn.Linear(dim, config.vocab_size, bias=False)
|
|
75
|
+
|
|
76
|
+
# Weight tying: input & output embeddings share weights
|
|
77
|
+
# Saves ~vocab_size × dim parameters
|
|
78
|
+
self.lm_head.weight = self.token_emb.weight
|
|
79
|
+
|
|
80
|
+
# ── Init ─────────────────────────────────────────────
|
|
81
|
+
self.apply(self._init_weights)
|
|
82
|
+
|
|
83
|
+
# Scale residual projections for stability (GPT-2 trick)
|
|
84
|
+
for name, p in self.named_parameters():
|
|
85
|
+
if name.endswith("out_proj.weight") or name.endswith("value_proj.weight"):
|
|
86
|
+
nn.init.normal_(p, mean=0.0,
|
|
87
|
+
std=0.02 / math.sqrt(2 * config.num_layers))
|
|
88
|
+
|
|
89
|
+
print(f"Aether model initialized: {self.num_parameters():,} parameters "
|
|
90
|
+
f"({self.size_mb():.1f} MB ternary)")
|
|
91
|
+
|
|
92
|
+
def _init_weights(self, module):
|
|
93
|
+
if isinstance(module, (nn.Linear, TernaryLinear)):
|
|
94
|
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
|
95
|
+
if hasattr(module, "bias") and module.bias is not None:
|
|
96
|
+
nn.init.zeros_(module.bias)
|
|
97
|
+
elif isinstance(module, nn.Embedding):
|
|
98
|
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
|
99
|
+
|
|
100
|
+
# ── Forward ──────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def forward(
|
|
103
|
+
self,
|
|
104
|
+
input_ids: torch.Tensor,
|
|
105
|
+
targets: Optional[torch.Tensor] = None,
|
|
106
|
+
mask: Optional[torch.Tensor] = None,
|
|
107
|
+
) -> dict:
|
|
108
|
+
"""
|
|
109
|
+
input_ids : (B, S)
|
|
110
|
+
targets : (B, S) — optional, for training loss
|
|
111
|
+
Returns dict with: logits, loss (if targets), stats
|
|
112
|
+
"""
|
|
113
|
+
B, S = input_ids.shape
|
|
114
|
+
device = input_ids.device
|
|
115
|
+
|
|
116
|
+
# Embed tokens
|
|
117
|
+
x = self.token_emb(input_ids) # (B, S, D)
|
|
118
|
+
x = self.emb_drop(x)
|
|
119
|
+
|
|
120
|
+
# Track depth routing state
|
|
121
|
+
accumulated_halt = torch.zeros(B, S, device=device)
|
|
122
|
+
stats = {"load_balance_loss": 0.0, "avg_depth": 0.0,
|
|
123
|
+
"avg_confidence": 0.0}
|
|
124
|
+
|
|
125
|
+
# ── Process through blocks with Asymmetric Depth Routing ──
|
|
126
|
+
active_count = 0
|
|
127
|
+
for i, block in enumerate(self.blocks):
|
|
128
|
+
# Check which tokens still need processing
|
|
129
|
+
_, should_continue, accumulated_halt = self.depth_router(
|
|
130
|
+
x, accumulated_halt
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# All tokens halted → stop early
|
|
134
|
+
if not should_continue.any() and i > self.config.min_layers:
|
|
135
|
+
stats["avg_depth"] = (i + 1) / self.config.num_layers
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
# Zero out halted tokens (they pass through unchanged)
|
|
139
|
+
active_mask = should_continue.unsqueeze(-1).float()
|
|
140
|
+
|
|
141
|
+
x_new, block_stats = block(x, mask)
|
|
142
|
+
x = x + active_mask * (x_new - x) # blend: halted tokens unchanged
|
|
143
|
+
|
|
144
|
+
stats["load_balance_loss"] += block_stats.get("load_balance_loss", 0)
|
|
145
|
+
if "confidence" in block_stats:
|
|
146
|
+
stats["avg_confidence"] += block_stats["confidence"]
|
|
147
|
+
active_count += 1
|
|
148
|
+
else:
|
|
149
|
+
stats["avg_depth"] = 1.0
|
|
150
|
+
|
|
151
|
+
stats["load_balance_loss"] /= max(active_count, 1)
|
|
152
|
+
stats["avg_confidence"] /= max(active_count, 1)
|
|
153
|
+
|
|
154
|
+
# ── Output projection ──────────────────────────────────
|
|
155
|
+
x = self.final_norm(x)
|
|
156
|
+
logits = self.lm_head(x) # (B, S, vocab)
|
|
157
|
+
|
|
158
|
+
result = {"logits": logits, "stats": stats}
|
|
159
|
+
|
|
160
|
+
# ── Loss (training) ────────────────────────────────────
|
|
161
|
+
if targets is not None:
|
|
162
|
+
lm_loss = F.cross_entropy(
|
|
163
|
+
logits.reshape(-1, self.config.vocab_size),
|
|
164
|
+
targets.reshape(-1),
|
|
165
|
+
ignore_index=-100,
|
|
166
|
+
)
|
|
167
|
+
# Add load balancing loss (encourages expert diversity)
|
|
168
|
+
total_loss = lm_loss + 0.01 * stats["load_balance_loss"]
|
|
169
|
+
result["loss"] = total_loss
|
|
170
|
+
result["lm_loss"] = lm_loss
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
# ── Generation ───────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
@torch.inference_mode()
|
|
177
|
+
def generate(
|
|
178
|
+
self,
|
|
179
|
+
input_ids: torch.Tensor,
|
|
180
|
+
max_new_tokens: int = 200,
|
|
181
|
+
temperature: float = 1.0,
|
|
182
|
+
top_k: int = 50,
|
|
183
|
+
top_p: float = 0.9,
|
|
184
|
+
repetition_penalty: float = 1.1,
|
|
185
|
+
stop_token_id: Optional[int] = None,
|
|
186
|
+
) -> torch.Tensor:
|
|
187
|
+
"""
|
|
188
|
+
Autoregressive generation with sampling.
|
|
189
|
+
|
|
190
|
+
input_ids: (B, S) or (S,) — prompt tokens
|
|
191
|
+
Returns: (B, S + max_new_tokens) — full sequence
|
|
192
|
+
"""
|
|
193
|
+
if input_ids.dim() == 1:
|
|
194
|
+
input_ids = input_ids.unsqueeze(0)
|
|
195
|
+
|
|
196
|
+
B = input_ids.shape[0]
|
|
197
|
+
generated = input_ids.clone()
|
|
198
|
+
|
|
199
|
+
for _ in range(max_new_tokens):
|
|
200
|
+
# Trim to max_seq_len
|
|
201
|
+
ctx = generated[:, -self.config.max_seq_len:]
|
|
202
|
+
out = self.forward(ctx)
|
|
203
|
+
logits = out["logits"][:, -1, :] # (B, vocab) — last token only
|
|
204
|
+
|
|
205
|
+
# Repetition penalty
|
|
206
|
+
if repetition_penalty != 1.0:
|
|
207
|
+
for b in range(B):
|
|
208
|
+
for token_id in set(generated[b].tolist()):
|
|
209
|
+
logits[b, token_id] /= repetition_penalty
|
|
210
|
+
|
|
211
|
+
# Temperature
|
|
212
|
+
if temperature != 1.0:
|
|
213
|
+
logits = logits / temperature
|
|
214
|
+
|
|
215
|
+
# Top-k
|
|
216
|
+
if top_k > 0:
|
|
217
|
+
values, _ = torch.topk(logits, top_k)
|
|
218
|
+
min_val = values[:, -1].unsqueeze(-1)
|
|
219
|
+
logits = logits.masked_fill(logits < min_val, float("-inf"))
|
|
220
|
+
|
|
221
|
+
# Top-p (nucleus sampling)
|
|
222
|
+
if top_p < 1.0:
|
|
223
|
+
sorted_logits, sorted_idx = torch.sort(logits, descending=True)
|
|
224
|
+
cumulative = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
|
|
225
|
+
remove = cumulative - F.softmax(sorted_logits, dim=-1) > top_p
|
|
226
|
+
sorted_logits[remove] = float("-inf")
|
|
227
|
+
logits = torch.scatter(logits, 1, sorted_idx, sorted_logits)
|
|
228
|
+
|
|
229
|
+
probs = F.softmax(logits, dim=-1)
|
|
230
|
+
next_token = torch.multinomial(probs, num_samples=1) # (B, 1)
|
|
231
|
+
generated = torch.cat([generated, next_token], dim=1)
|
|
232
|
+
|
|
233
|
+
# Early stopping
|
|
234
|
+
if stop_token_id is not None and (next_token == stop_token_id).all():
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
return generated
|
|
238
|
+
|
|
239
|
+
# ── Utilities ────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
def num_parameters(self, trainable_only: bool = False) -> int:
|
|
242
|
+
params = (p for p in self.parameters()
|
|
243
|
+
if not trainable_only or p.requires_grad)
|
|
244
|
+
return sum(p.numel() for p in params)
|
|
245
|
+
|
|
246
|
+
def size_mb(self) -> float:
|
|
247
|
+
"""Estimated size in MB with ternary quantization."""
|
|
248
|
+
return self.num_parameters() * 1.58 / (8 * 1024 * 1024)
|
|
249
|
+
|
|
250
|
+
def size_float32_mb(self) -> float:
|
|
251
|
+
return self.num_parameters() * 4 / (1024 * 1024)
|
|
252
|
+
|
|
253
|
+
def summary(self) -> str:
|
|
254
|
+
lines = [
|
|
255
|
+
"=" * 50,
|
|
256
|
+
f" Aether Model Summary",
|
|
257
|
+
"=" * 50,
|
|
258
|
+
f" Dim: {self.config.dim}",
|
|
259
|
+
f" Layers: {self.config.num_layers}",
|
|
260
|
+
f" Heads: {self.config.num_heads}",
|
|
261
|
+
f" Experts: {self.config.num_experts}",
|
|
262
|
+
f" Parameters: {self.num_parameters():,}",
|
|
263
|
+
f" Size (FP32): {self.size_float32_mb():.1f} MB",
|
|
264
|
+
f" Size (1.58b): {self.size_mb():.1f} MB ← deploy",
|
|
265
|
+
f" Compression: {self.size_float32_mb()/self.size_mb():.1f}x",
|
|
266
|
+
f" Context cap: {self.blocks[0].memory.get_effective_context_size():,} slots",
|
|
267
|
+
"=" * 50,
|
|
268
|
+
]
|
|
269
|
+
return "\n".join(lines)
|
aether/routing.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aether Routing Mechanisms
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
1. Asymmetric Depth Routing (ADR)
|
|
6
|
+
Each token decides how many layers it needs.
|
|
7
|
+
Simple tokens: 1-2 layers.
|
|
8
|
+
Complex tokens: all layers.
|
|
9
|
+
Saves 10-50x compute.
|
|
10
|
+
|
|
11
|
+
2. Mixture of Experts (MoE)
|
|
12
|
+
Only top-2 experts fire per token.
|
|
13
|
+
More capacity, same compute cost.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
import torch.nn.functional as F
|
|
19
|
+
from typing import Tuple
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsymmetricDepthRouter(nn.Module):
|
|
23
|
+
"""
|
|
24
|
+
Decides whether a token needs more processing.
|
|
25
|
+
|
|
26
|
+
If confidence > threshold: stop processing this token.
|
|
27
|
+
Otherwise: continue to next layer.
|
|
28
|
+
|
|
29
|
+
This is the key insight: not all tokens need equal depth.
|
|
30
|
+
"The" needs 1 layer. "Why does consciousness exist?" needs 24.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, dim: int, threshold: float = 0.8):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.threshold = threshold
|
|
36
|
+
|
|
37
|
+
# Halting probability predictor
|
|
38
|
+
self.halt_predictor = nn.Sequential(
|
|
39
|
+
nn.Linear(dim, dim // 4),
|
|
40
|
+
nn.SiLU(),
|
|
41
|
+
nn.Linear(dim // 4, 1),
|
|
42
|
+
nn.Sigmoid()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Remainder for weighted combination
|
|
46
|
+
self.register_buffer("zero", torch.tensor(0.0))
|
|
47
|
+
|
|
48
|
+
def forward(self, x: torch.Tensor,
|
|
49
|
+
accumulated_halt: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
50
|
+
"""
|
|
51
|
+
x: (B, S, D)
|
|
52
|
+
accumulated_halt: (B, S) — how much halting probability accumulated so far
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
halt_prob: (B, S) — probability of halting at this step
|
|
56
|
+
should_continue: (B, S) bool — which tokens need more processing
|
|
57
|
+
new_accumulated: (B, S)
|
|
58
|
+
"""
|
|
59
|
+
halt_prob = self.halt_predictor(x).squeeze(-1) # (B, S)
|
|
60
|
+
new_accumulated = accumulated_halt + halt_prob * (1 - accumulated_halt)
|
|
61
|
+
should_continue = new_accumulated < self.threshold
|
|
62
|
+
return halt_prob, should_continue, new_accumulated
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SparseMoE(nn.Module):
|
|
66
|
+
"""
|
|
67
|
+
Sparse Mixture of Experts FFN.
|
|
68
|
+
|
|
69
|
+
Instead of one huge FFN: N small experts, activate only top-k.
|
|
70
|
+
Result: N times more capacity at same compute.
|
|
71
|
+
|
|
72
|
+
Example: 8 experts, top-2 per token
|
|
73
|
+
→ 4x more capacity, same FLOPs as 1 expert
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, dim: int, num_experts: int = 8,
|
|
77
|
+
experts_per_token: int = 2, ffn_mult: int = 4):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.num_experts = num_experts
|
|
80
|
+
self.experts_per_token = experts_per_token
|
|
81
|
+
hidden = dim * ffn_mult // num_experts # each expert is smaller
|
|
82
|
+
|
|
83
|
+
# Expert networks (small and fast)
|
|
84
|
+
self.experts = nn.ModuleList([
|
|
85
|
+
nn.Sequential(
|
|
86
|
+
nn.Linear(dim, hidden, bias=False),
|
|
87
|
+
nn.SiLU(),
|
|
88
|
+
nn.Linear(hidden, dim, bias=False),
|
|
89
|
+
)
|
|
90
|
+
for _ in range(num_experts)
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
# Router: decides which experts to use
|
|
94
|
+
self.router = nn.Linear(dim, num_experts, bias=False)
|
|
95
|
+
|
|
96
|
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
97
|
+
"""
|
|
98
|
+
x: (B, S, D)
|
|
99
|
+
Returns: (output, load_balance_loss)
|
|
100
|
+
"""
|
|
101
|
+
B, S, D = x.shape
|
|
102
|
+
x_flat = x.reshape(-1, D) # (B*S, D)
|
|
103
|
+
|
|
104
|
+
# Route to top-k experts
|
|
105
|
+
router_logits = self.router(x_flat) # (B*S, E)
|
|
106
|
+
router_probs = F.softmax(router_logits, dim=-1)
|
|
107
|
+
|
|
108
|
+
top_weights, top_indices = torch.topk(
|
|
109
|
+
router_probs, self.experts_per_token, dim=-1
|
|
110
|
+
) # (B*S, k)
|
|
111
|
+
top_weights = top_weights / top_weights.sum(dim=-1, keepdim=True)
|
|
112
|
+
|
|
113
|
+
# Compute expert outputs (only activated experts)
|
|
114
|
+
output = torch.zeros_like(x_flat)
|
|
115
|
+
for k_idx in range(self.experts_per_token):
|
|
116
|
+
for e_idx in range(self.num_experts):
|
|
117
|
+
# Find tokens routed to this expert at this rank
|
|
118
|
+
mask = (top_indices[:, k_idx] == e_idx)
|
|
119
|
+
if not mask.any():
|
|
120
|
+
continue
|
|
121
|
+
w = top_weights[mask, k_idx].unsqueeze(-1)
|
|
122
|
+
out_e = self.experts[e_idx](x_flat[mask])
|
|
123
|
+
output[mask] += w * out_e
|
|
124
|
+
|
|
125
|
+
# Load balancing loss (encourages all experts to be used equally)
|
|
126
|
+
avg_probs = router_probs.mean(0) # (E,)
|
|
127
|
+
load_balance_loss = (self.num_experts * avg_probs * avg_probs).sum()
|
|
128
|
+
|
|
129
|
+
return output.reshape(B, S, D), load_balance_loss
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aether-nn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Aether: A Post-Transformer Neural Architecture — 20x smaller, trains on phone CPU
|
|
5
|
+
Home-page: https://github.com/aether-nn/aether
|
|
6
|
+
Author: Aether Research
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Aether Research
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: Homepage, https://github.com/aether-nn/aether
|
|
30
|
+
Project-URL: Repository, https://github.com/aether-nn/aether
|
|
31
|
+
Keywords: deep-learning,transformer,neural-network,nlp,efficient-ai
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: torch>=2.0.0
|
|
41
|
+
Requires-Dist: numpy>=1.21.0
|
|
42
|
+
Dynamic: author
|
|
43
|
+
Dynamic: home-page
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
|
|
47
|
+
# ⚡ Aether
|
|
48
|
+
|
|
49
|
+
> **A neural architecture that makes Transformers obsolete.**
|
|
50
|
+
> **With its own programming language: Flux.**
|
|
51
|
+
|
|
52
|
+
[](https://opensource.org/licenses/MIT)
|
|
53
|
+
[](https://www.python.org/)
|
|
54
|
+
[](https://pytorch.org/)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
**From GitHub (works right now):**
|
|
61
|
+
```bash
|
|
62
|
+
pip install git+https://github.com/YOUR_USERNAME/aether.git
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**From PyPI (after publishing):**
|
|
66
|
+
```bash
|
|
67
|
+
pip install aether-nn
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Clone locally:**
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/YOUR_USERNAME/aether.git
|
|
73
|
+
cd aether
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Flux Language
|
|
80
|
+
|
|
81
|
+
Aether comes with **Flux** — its own programming language for neural networks.
|
|
82
|
+
Write a model in 10 lines instead of 500.
|
|
83
|
+
|
|
84
|
+
**Create a file `mymodel.fx`:**
|
|
85
|
+
```flux
|
|
86
|
+
model ChatBot {
|
|
87
|
+
size: micro
|
|
88
|
+
vocab: 32000
|
|
89
|
+
context: infinite
|
|
90
|
+
|
|
91
|
+
memory {
|
|
92
|
+
working: 512
|
|
93
|
+
episodic: 2048
|
|
94
|
+
semantic: 8192
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
block * 6 {
|
|
98
|
+
attend fractal(scales=3, heads=8)
|
|
99
|
+
remember hierarchical()
|
|
100
|
+
think sparse_moe(experts=4)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
quantize: ternary
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
train ChatBot {
|
|
107
|
+
data: "data.txt"
|
|
108
|
+
steps: 10000
|
|
109
|
+
batch: 4
|
|
110
|
+
lr: 3e-4
|
|
111
|
+
device: auto
|
|
112
|
+
save: "./checkpoints"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
generate ChatBot {
|
|
116
|
+
prompt: "Hello"
|
|
117
|
+
tokens: 200
|
|
118
|
+
temperature: 0.8
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Run it:**
|
|
123
|
+
```bash
|
|
124
|
+
python flux/flux.py run mymodel.fx
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Or use Flux CLI:**
|
|
128
|
+
```bash
|
|
129
|
+
python flux/flux.py check mymodel.fx # syntax check
|
|
130
|
+
python flux/flux.py compile mymodel.fx # show generated Python
|
|
131
|
+
python flux/flux.py new MyModel # create template
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Python API
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from aether import AetherModel, AetherConfig
|
|
140
|
+
|
|
141
|
+
config = AetherConfig.nano() # 0.6 MB
|
|
142
|
+
model = AetherModel(config)
|
|
143
|
+
|
|
144
|
+
import torch
|
|
145
|
+
ids = torch.randint(0, 256, (1, 32))
|
|
146
|
+
out = model(ids)
|
|
147
|
+
print(out["logits"].shape) # (1, 32, 256)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Why Aether?
|
|
153
|
+
|
|
154
|
+
The Transformer solved one problem: parallelizing RNNs.
|
|
155
|
+
Aether solves **six problems** of the Transformer simultaneously.
|
|
156
|
+
|
|
157
|
+
| | GPT-4 | Mamba | RWKV | **Aether** |
|
|
158
|
+
|---|:---:|:---:|:---:|:---:|
|
|
159
|
+
| Infinite context | ❌ | ⚠️ | ⚠️ | ✅ |
|
|
160
|
+
| Trains on phone | ❌ | ❌ | ❌ | ✅ |
|
|
161
|
+
| Model size (10B) | ~20GB | ~8GB | ~8GB | **~800MB** |
|
|
162
|
+
| Learns after deploy | ❌ | ❌ | ❌ | ✅ |
|
|
163
|
+
| True reasoning | ❌ | ❌ | ❌ | ✅ |
|
|
164
|
+
| Own language | ❌ | ❌ | ❌ | ✅ **Flux** |
|
|
165
|
+
| Speed on CPU | 1x | 4x | 4x | **20x** |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Model Sizes
|
|
170
|
+
|
|
171
|
+
| Variant | Params | Ternary Size | Quality |
|
|
172
|
+
|---------|--------|-------------|---------|
|
|
173
|
+
| Aether-Nano | 3M | **0.6 MB** | Basic |
|
|
174
|
+
| Aether-Micro | 14M | **2.6 MB** | Good |
|
|
175
|
+
| Aether-Mini | 116M | **22 MB** | GPT-2 level |
|
|
176
|
+
| Aether-Base | 350M | **66 MB** | Strong |
|
|
177
|
+
| Aether-Large | 1.3B | **246 MB** | Excellent |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Architecture
|
|
182
|
+
|
|
183
|
+
5 innovations in one unified system:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
[1] Fractal Sparse Attention O(n·log n) vs O(n²)
|
|
187
|
+
[2] Hierarchical Memory Infinite context
|
|
188
|
+
[3] Asymmetric Depth Routing 10-50x compute savings
|
|
189
|
+
[4] Causal World Model True reasoning
|
|
190
|
+
[5] Continuous Learning Adapts at inference time
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Training
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# With Flux (recommended)
|
|
199
|
+
python flux/flux.py run flux/examples/nano.fx
|
|
200
|
+
|
|
201
|
+
# With Python directly
|
|
202
|
+
python train/train.py --config nano --data your_text.txt
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT — free for everyone, including commercial use.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Citation
|
|
214
|
+
|
|
215
|
+
```bibtex
|
|
216
|
+
@misc{aether2026,
|
|
217
|
+
title={Aether: A Post-Transformer Architecture},
|
|
218
|
+
year={2026},
|
|
219
|
+
url={https://github.com/YOUR_USERNAME/aether}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aether/__init__.py,sha256=ObOhB01S0a2fLs60NmM9dghZYLaaOgItZ0iTYAkAbxQ,281
|
|
2
|
+
aether/attention.py,sha256=AbNTiK6mrJ1oXsBw8_XHIu10Md6TMTwpT1ejwaFsbG8,4330
|
|
3
|
+
aether/config.py,sha256=LfOLfcg_-WAqSfESQlORZbcrGJ1iGNMpvpRl9U6JoBk,4158
|
|
4
|
+
aether/layers.py,sha256=M3td1Bgtx2A62lgxYk_bAyzl7pVLxbuzgWUKCCo1Tio,11015
|
|
5
|
+
aether/memory.py,sha256=Bs6MGE8oI4C3s06GebX6Iaj4jZdxq-rUUGS4cs7yUMg,4534
|
|
6
|
+
aether/model.py,sha256=b5tnRcBb-i5qbTUEb-Zo8GIUY2eBOEL5SpxHZuB_xNI,10734
|
|
7
|
+
aether/routing.py,sha256=N6QYX6yjIyknYr7iN_PbrSKAhI9DuuTumfRtRLX_ELQ,4347
|
|
8
|
+
aether_nn-0.1.0.dist-info/licenses/LICENSE,sha256=tyEdbthxOrfx5Y222kCRYzHPAIorsIFMJGClPbp8FO0,1072
|
|
9
|
+
aether_nn-0.1.0.dist-info/METADATA,sha256=zfgp2MVgvQ_AHnDMuWSDPHuNMaoglaROI8ScjhfA8dg,5748
|
|
10
|
+
aether_nn-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
aether_nn-0.1.0.dist-info/top_level.txt,sha256=QynFGsNdIRoJ6JBODYhcXgsoKtpwAuDfgGNOsy3UPsE,7
|
|
12
|
+
aether_nn-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aether Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aether
|