continuousintelligence 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.
- continuousintelligence/__init__.py +9 -0
- continuousintelligence/_data/NOTICE +16 -0
- continuousintelligence/_data/bike_sharing.csv +145 -0
- continuousintelligence/_example.py +20 -0
- continuousintelligence/_loader.py +140 -0
- continuousintelligence/_plot.py +158 -0
- continuousintelligence/_vendor/__init__.py +1 -0
- continuousintelligence/_vendor/mamba3/LICENSE +201 -0
- continuousintelligence/_vendor/mamba3/__init__.py +6 -0
- continuousintelligence/_vendor/mamba3/angle_dt.py +213 -0
- continuousintelligence/_vendor/mamba3/layernorm_gated.py +115 -0
- continuousintelligence/_vendor/mamba3/mamba3_mimo_fwd.py +483 -0
- continuousintelligence/_vendor/mamba3/mamba3_mimo_utils.py +112 -0
- continuousintelligence/_vendor/mamba3/utils.py +25 -0
- continuousintelligence/ci_proto/__init__.py +5 -0
- continuousintelligence/ci_proto/_calendar.py +153 -0
- continuousintelligence/ci_proto/_checkpoint.py +106 -0
- continuousintelligence/ci_proto/_cuda.py +135 -0
- continuousintelligence/ci_proto/_forecast.py +708 -0
- continuousintelligence/ci_proto/_groups.py +198 -0
- continuousintelligence/ci_proto/_mamba.py +190 -0
- continuousintelligence/ci_proto/_model.py +489 -0
- continuousintelligence-0.1.0.dist-info/METADATA +29 -0
- continuousintelligence-0.1.0.dist-info/RECORD +30 -0
- continuousintelligence-0.1.0.dist-info/WHEEL +5 -0
- continuousintelligence-0.1.0.dist-info/licenses/LICENCE +95 -0
- continuousintelligence-0.1.0.dist-info/licenses/NOTICE +33 -0
- continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_data/NOTICE +16 -0
- continuousintelligence-0.1.0.dist-info/licenses/src/continuousintelligence/_vendor/mamba3/LICENSE +201 -0
- continuousintelligence-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Group context: what the other series of an evaluation window add to each series' forecast.
|
|
2
|
+
|
|
3
|
+
All three functions take the univariate series of one window, each as its observed history
|
|
4
|
+
(``histories``) and that history's epoch-ns timestamps (``stamps``), in the window's order. Every
|
|
5
|
+
value they return comes from those histories, and a series only pools with series whose history
|
|
6
|
+
ends at the same timestamp, its forecast origin, so no value after the origin is used.
|
|
7
|
+
|
|
8
|
+
- :func:`static_group_rows` gives each series past covariate rows: the mean of its static
|
|
9
|
+
group's other z-scored histories, as Chronos-2's groups pool series by their metadata.
|
|
10
|
+
- :func:`joint_neighbour_rows` gives each series extra target rows: the series whose z-scored
|
|
11
|
+
histories correlate best with its own, forecast jointly with it.
|
|
12
|
+
- :func:`chunk_groups` splits the window into chunks that are forecast jointly, every series a
|
|
13
|
+
target, as t0-gamma's ``cross_inference`` and Chronos-2's ``cross_learning`` do on fev-bench.
|
|
14
|
+
|
|
15
|
+
The z-scores are taken on a grid of the window's last ``span`` distinct timestamps. Each row
|
|
16
|
+
these functions return covers its series' own history timestamps and is missing where no value
|
|
17
|
+
is available.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from typing import Final
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
# The timestamps that z-scores and correlations are taken over.
|
|
29
|
+
SPAN: Final = 2048
|
|
30
|
+
|
|
31
|
+
# Two histories correlate only over at least this many common steps.
|
|
32
|
+
_MIN_OVERLAP: Final = 32
|
|
33
|
+
|
|
34
|
+
# A window whose largest origin group holds more series is left without joint neighbours, whose
|
|
35
|
+
# correlation matrix would grow with the square of the group.
|
|
36
|
+
_MAX_JOINT_GROUP: Final = 8000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _codes(values: Sequence[object]) -> NDArray[np.int64]:
|
|
40
|
+
"""Number each distinct value; a group's members share a number."""
|
|
41
|
+
return np.unique(np.asarray(values), return_inverse=True)[1].astype(np.int64)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _origins(stamps: Sequence[NDArray[np.int64]]) -> NDArray[np.int64]:
|
|
45
|
+
"""Number each series by the last timestamp of its history, its forecast origin."""
|
|
46
|
+
return _codes([int(s[-1]) if s.size else -1 for s in stamps])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _z_scores(
|
|
50
|
+
histories: Sequence[NDArray[np.float64]], stamps: Sequence[NDArray[np.int64]], span: int
|
|
51
|
+
) -> tuple[NDArray[np.int64], NDArray[np.float32]]:
|
|
52
|
+
"""Return the timestamp grid and each history z-scored onto it.
|
|
53
|
+
|
|
54
|
+
A history without spread stays missing.
|
|
55
|
+
"""
|
|
56
|
+
grid = np.unique(np.concatenate([s[-span:] for s in stamps]))[-span:]
|
|
57
|
+
z = np.full((len(histories), grid.size), np.nan, dtype=np.float32)
|
|
58
|
+
for i, (row, s) in enumerate(zip(histories, stamps, strict=True)):
|
|
59
|
+
with np.errstate(all="ignore"):
|
|
60
|
+
scale = np.nanstd(row)
|
|
61
|
+
if not np.isfinite(scale) or scale <= 0:
|
|
62
|
+
continue
|
|
63
|
+
at = np.searchsorted(grid, s)
|
|
64
|
+
ok = (at < grid.size) & (grid[np.minimum(at, grid.size - 1)] == s)
|
|
65
|
+
z[i, at[ok]] = ((row[ok] - np.nanmean(row)) / scale).astype(np.float32)
|
|
66
|
+
return grid, z
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _on_own_stamps(
|
|
70
|
+
values: NDArray[np.float64], grid: NDArray[np.int64], stamps: NDArray[np.int64]
|
|
71
|
+
) -> NDArray[np.float64]:
|
|
72
|
+
"""Read a grid row at a series' own history timestamps; missing off the grid."""
|
|
73
|
+
if (
|
|
74
|
+
values.shape == stamps.shape
|
|
75
|
+
and np.array_equal(grid, stamps)
|
|
76
|
+
and np.all(grid[1:] > grid[:-1])
|
|
77
|
+
):
|
|
78
|
+
# Duplicate timestamps must retain searchsorted's first-occurrence semantics.
|
|
79
|
+
return values.copy()
|
|
80
|
+
at = np.searchsorted(grid, stamps)
|
|
81
|
+
ok = (at < grid.size) & (grid[np.minimum(at, grid.size - 1)] == stamps)
|
|
82
|
+
row = np.full(stamps.size, np.nan)
|
|
83
|
+
row[ok] = values[at[ok]]
|
|
84
|
+
return row
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def static_group_rows(
|
|
88
|
+
histories: Sequence[NDArray[np.float64]],
|
|
89
|
+
stamps: Sequence[NDArray[np.int64]],
|
|
90
|
+
statics: Sequence[Sequence[object]],
|
|
91
|
+
columns: int,
|
|
92
|
+
span: int = SPAN,
|
|
93
|
+
) -> list[NDArray[np.float64]] | None:
|
|
94
|
+
"""Return each series' static-group rows, one per grouping column, or ``None`` if none groups.
|
|
95
|
+
|
|
96
|
+
``statics`` holds each static column's value for every series. A column groups the window
|
|
97
|
+
when it has at least two distinct values, fewer than the window's series, and a median group
|
|
98
|
+
size of at least three. The ``columns`` of them with the most groups are kept, ties in column
|
|
99
|
+
order. Each gives every series one row: the mean of its same-origin group's other z-scored
|
|
100
|
+
histories. A missing value is a group of its own. A window of fewer than three series gets
|
|
101
|
+
no rows.
|
|
102
|
+
"""
|
|
103
|
+
if columns < 1:
|
|
104
|
+
raise ValueError(f"static groups need at least one column, got {columns}")
|
|
105
|
+
count = len(histories)
|
|
106
|
+
if count < 3:
|
|
107
|
+
return None
|
|
108
|
+
groupings = []
|
|
109
|
+
for values in statics:
|
|
110
|
+
codes = _codes([repr(value) for value in values])
|
|
111
|
+
sizes = np.bincount(codes)
|
|
112
|
+
if 2 <= sizes.size < count and np.median(sizes[codes]) >= 3:
|
|
113
|
+
groupings.append((sizes.size, codes))
|
|
114
|
+
chosen = sorted(groupings, key=lambda grouping: -grouping[0])[:columns]
|
|
115
|
+
if not chosen:
|
|
116
|
+
return None
|
|
117
|
+
grid, z = _z_scores(histories, stamps, span)
|
|
118
|
+
seen = np.isfinite(z)
|
|
119
|
+
filled = np.where(seen, z, np.float32(0.0))
|
|
120
|
+
origin = _origins(stamps)
|
|
121
|
+
rows: list[list[NDArray[np.float64]]] = [[] for _ in histories]
|
|
122
|
+
for _, codes in chosen:
|
|
123
|
+
group = _codes((codes * (origin.max() + 1) + origin).tolist())
|
|
124
|
+
sums_by_group = np.zeros((group.max() + 1, grid.size), dtype=np.float32)
|
|
125
|
+
counts_by_group = np.zeros_like(sums_by_group)
|
|
126
|
+
np.add.at(sums_by_group, group, filled)
|
|
127
|
+
np.add.at(counts_by_group, group, seen.astype(np.float32))
|
|
128
|
+
sums = sums_by_group[group].astype(np.float64)
|
|
129
|
+
counts = counts_by_group[group].astype(np.float64) - seen
|
|
130
|
+
with np.errstate(all="ignore"):
|
|
131
|
+
means = (sums - filled) / counts
|
|
132
|
+
means[counts < 1] = np.nan
|
|
133
|
+
for i, s in enumerate(stamps):
|
|
134
|
+
rows[i].append(_on_own_stamps(means[i], grid, s))
|
|
135
|
+
return [np.stack(own) for own in rows]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def joint_neighbour_rows(
|
|
139
|
+
histories: Sequence[NDArray[np.float64]],
|
|
140
|
+
stamps: Sequence[NDArray[np.int64]],
|
|
141
|
+
size: int,
|
|
142
|
+
span: int = SPAN,
|
|
143
|
+
) -> list[NDArray[np.float64]] | None:
|
|
144
|
+
"""Return each series' history followed by its ``size - 1`` neighbours', or ``None``.
|
|
145
|
+
|
|
146
|
+
A series' neighbours are the same-origin series whose z-scored histories correlate best with
|
|
147
|
+
its own over at least 32 common grid steps, each placed on the series' own history
|
|
148
|
+
timestamps. A series with fewer neighbours repeats its own history in the remaining rows. A
|
|
149
|
+
window of fewer than two series, or whose largest origin group holds more than 8,000, gets
|
|
150
|
+
none.
|
|
151
|
+
"""
|
|
152
|
+
if size < 2:
|
|
153
|
+
raise ValueError(f"joint neighbours need a group of at least two series, got {size}")
|
|
154
|
+
if len(histories) < 2:
|
|
155
|
+
return None
|
|
156
|
+
_, z = _z_scores(histories, stamps, span)
|
|
157
|
+
seen = np.isfinite(z)
|
|
158
|
+
filled = np.where(seen, z, np.float32(0.0))
|
|
159
|
+
origin = _origins(stamps)
|
|
160
|
+
if np.bincount(origin).max() > _MAX_JOINT_GROUP:
|
|
161
|
+
return None
|
|
162
|
+
neighbours: list[list[int]] = [[] for _ in histories]
|
|
163
|
+
for group in range(origin.max() + 1):
|
|
164
|
+
members = np.flatnonzero(origin == group)
|
|
165
|
+
if members.size < 2:
|
|
166
|
+
continue
|
|
167
|
+
zf, sf = filled[members], seen[members].astype(np.float32)
|
|
168
|
+
overlap = sf @ sf.T
|
|
169
|
+
corr = (zf @ zf.T) / np.maximum(overlap, 1.0)
|
|
170
|
+
corr[overlap < _MIN_OVERLAP] = -np.inf
|
|
171
|
+
np.fill_diagonal(corr, -np.inf)
|
|
172
|
+
for r, member in enumerate(members.tolist()):
|
|
173
|
+
order = np.argsort(-corr[r], kind="stable")[: size - 1]
|
|
174
|
+
neighbours[member] = [int(members[o]) for o in order if np.isfinite(corr[r, o])]
|
|
175
|
+
out = []
|
|
176
|
+
for i, (own, s) in enumerate(zip(histories, stamps, strict=True)):
|
|
177
|
+
rows = [own] + [_on_own_stamps(histories[j], stamps[j], s) for j in neighbours[i]]
|
|
178
|
+
rows += [own] * (size - len(rows))
|
|
179
|
+
out.append(np.stack(rows))
|
|
180
|
+
return out
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def chunk_groups(origins: Sequence[int], size: int) -> list[list[int]]:
|
|
184
|
+
"""Split a window's series into consecutive chunks of up to ``size`` with one origin.
|
|
185
|
+
|
|
186
|
+
``origins`` holds each series' last history timestamp. Series keep the window's order within
|
|
187
|
+
a chunk, and a chunk may hold one series.
|
|
188
|
+
"""
|
|
189
|
+
if size < 2:
|
|
190
|
+
raise ValueError(f"chunks need room for at least two series, got {size}")
|
|
191
|
+
by_origin: dict[int, list[int]] = {}
|
|
192
|
+
for index, origin in enumerate(origins):
|
|
193
|
+
by_origin.setdefault(origin, []).append(index)
|
|
194
|
+
return [
|
|
195
|
+
members[start : start + size]
|
|
196
|
+
for members in by_origin.values()
|
|
197
|
+
for start in range(0, len(members), size)
|
|
198
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""PyTorch Mamba-3 recurrence for CPU and CUDA inference.
|
|
2
|
+
|
|
3
|
+
Adapted by Continuous Intelligence from Mamba's module and
|
|
4
|
+
SISO/MIMO reference calculations:
|
|
5
|
+
https://github.com/state-spaces/mamba/tree/a14b1dff0454a3bc27d9eb31355dc01e4b2490ec
|
|
6
|
+
|
|
7
|
+
RMS normalisation is adapted from xLSTM Large:
|
|
8
|
+
https://github.com/NX-AI/xlstm/blob/ab22eadbd245f293dd8dec38ed29963d73758a12/xlstm/xlstm_large/components.py
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026, Dao AI Lab, Goombalab.
|
|
11
|
+
Copyright 2024 NXAI GmbH and its affiliates 2024
|
|
12
|
+
Upstream portions are licensed under Apache-2.0; see NOTICE.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn.functional as F
|
|
21
|
+
from einops import rearrange
|
|
22
|
+
from torch import Tensor, nn
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RMSNorm(nn.Module):
|
|
26
|
+
"""RMS norm with optional float32 reductions."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
num_features: int,
|
|
31
|
+
eps: float = 1e-06,
|
|
32
|
+
force_float32_reductions: bool = True,
|
|
33
|
+
use_bias: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.eps = eps
|
|
37
|
+
self.force_float32_reductions = force_float32_reductions
|
|
38
|
+
self.weight = nn.Parameter(torch.ones(num_features))
|
|
39
|
+
self.bias = nn.Parameter(torch.zeros(num_features)) if use_bias else None
|
|
40
|
+
|
|
41
|
+
def forward(self, x: Tensor) -> Tensor:
|
|
42
|
+
in_dtype = x.dtype
|
|
43
|
+
if self.force_float32_reductions:
|
|
44
|
+
x = x.float()
|
|
45
|
+
x = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
|
|
46
|
+
x = x.to(in_dtype)
|
|
47
|
+
x = x * self.weight
|
|
48
|
+
if self.bias is not None:
|
|
49
|
+
x = x + self.bias
|
|
50
|
+
return x
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _rotate_pairs(x: Tensor, cos: Tensor, sin: Tensor, interleaved: bool) -> Tensor:
|
|
54
|
+
"""Rotate pairs of the last axis; angles beyond ``cos`` leave their pairs unrotated."""
|
|
55
|
+
if interleaved:
|
|
56
|
+
pairs = x.view(*x.shape[:-1], -1, 2)
|
|
57
|
+
first, second = (pairs[..., 0], pairs[..., 1])
|
|
58
|
+
else:
|
|
59
|
+
half = x.shape[-1] // 2
|
|
60
|
+
first, second = (x[..., :half], x[..., half:])
|
|
61
|
+
if cos.shape[-1] < first.shape[-1]:
|
|
62
|
+
pad = first.shape[-1] - cos.shape[-1]
|
|
63
|
+
cos = F.pad(cos, (0, pad), value=1.0)
|
|
64
|
+
sin = F.pad(sin, (0, pad), value=0.0)
|
|
65
|
+
rotated_first = first * cos - second * sin
|
|
66
|
+
rotated_second = first * sin + second * cos
|
|
67
|
+
if interleaved:
|
|
68
|
+
return torch.stack([rotated_first, rotated_second], dim=-1).view_as(x)
|
|
69
|
+
return torch.cat([rotated_first, rotated_second], dim=-1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Mamba3Reference(nn.Module):
|
|
73
|
+
"""Mamba-3 block evaluated step by step in plain PyTorch."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
d_model: int,
|
|
78
|
+
d_state: int = 128,
|
|
79
|
+
expand: int = 2,
|
|
80
|
+
headdim: int = 64,
|
|
81
|
+
is_mimo: bool = False,
|
|
82
|
+
mimo_rank: int = 4,
|
|
83
|
+
) -> None:
|
|
84
|
+
super().__init__()
|
|
85
|
+
if type(d_state) is not int or d_state < 4 or d_state % 2:
|
|
86
|
+
raise ValueError("mamba_state_dim must be an even integer of at least 4")
|
|
87
|
+
self.d_state = d_state
|
|
88
|
+
self.is_mimo = is_mimo
|
|
89
|
+
self.mimo_rank = mimo_rank if is_mimo else 1
|
|
90
|
+
self.d_inner = expand * d_model
|
|
91
|
+
if self.d_inner % headdim != 0:
|
|
92
|
+
raise ValueError(f"expand * d_model {self.d_inner} not divisible by headdim {headdim}")
|
|
93
|
+
self.headdim = headdim
|
|
94
|
+
self.nheads = self.d_inner // headdim
|
|
95
|
+
split = d_state // 2 - d_state // 2 % 2
|
|
96
|
+
self.num_rope_angles = split // 2
|
|
97
|
+
bc = self.d_state * self.mimo_rank
|
|
98
|
+
d_in_proj = 2 * self.d_inner + 2 * bc + 3 * self.nheads + self.num_rope_angles
|
|
99
|
+
self.in_proj = nn.Linear(d_model, d_in_proj, bias=False)
|
|
100
|
+
dt = torch.exp(
|
|
101
|
+
torch.rand(self.nheads) * (math.log(0.1) - math.log(0.001)) + math.log(0.001)
|
|
102
|
+
).clamp(min=1e-4)
|
|
103
|
+
self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
|
|
104
|
+
self.B_bias = nn.Parameter(torch.ones(self.nheads, self.mimo_rank, d_state))
|
|
105
|
+
self.C_bias = nn.Parameter(torch.ones(self.nheads, self.mimo_rank, d_state))
|
|
106
|
+
self.B_norm = RMSNorm(d_state, eps=1e-05)
|
|
107
|
+
self.C_norm = RMSNorm(d_state, eps=1e-05)
|
|
108
|
+
if is_mimo:
|
|
109
|
+
shape = (self.nheads, self.mimo_rank, headdim)
|
|
110
|
+
self.mimo_x = nn.Parameter(torch.ones(shape) / self.mimo_rank)
|
|
111
|
+
self.mimo_z = nn.Parameter(torch.ones(shape))
|
|
112
|
+
self.mimo_o = nn.Parameter(torch.ones(shape) / self.mimo_rank)
|
|
113
|
+
self.D = nn.Parameter(torch.ones(self.nheads))
|
|
114
|
+
self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
|
|
115
|
+
|
|
116
|
+
def forward(self, u: Tensor) -> Tensor:
|
|
117
|
+
batch, seq, _ = u.shape
|
|
118
|
+
bc = self.d_state * self.mimo_rank
|
|
119
|
+
z, x, B, C, dd_dt, dd_A, trap, angles = torch.split(
|
|
120
|
+
self.in_proj(u),
|
|
121
|
+
[
|
|
122
|
+
self.d_inner,
|
|
123
|
+
self.d_inner,
|
|
124
|
+
bc,
|
|
125
|
+
bc,
|
|
126
|
+
self.nheads,
|
|
127
|
+
self.nheads,
|
|
128
|
+
self.nheads,
|
|
129
|
+
self.num_rope_angles,
|
|
130
|
+
],
|
|
131
|
+
dim=-1,
|
|
132
|
+
)
|
|
133
|
+
z = rearrange(z, "b l (h p) -> b l h p", p=self.headdim).to(torch.float32)
|
|
134
|
+
x = rearrange(x, "b l (h p) -> b l h p", p=self.headdim).to(torch.float32)
|
|
135
|
+
B = rearrange(B, "b l (r g n) -> b l r g n", r=self.mimo_rank, g=1)
|
|
136
|
+
C = rearrange(C, "b l (r g n) -> b l r g n", r=self.mimo_rank, g=1)
|
|
137
|
+
A = torch.clamp(-F.softplus(dd_A.to(torch.float32)), max=-1e-4)
|
|
138
|
+
DT = F.softplus(dd_dt.to(torch.float32) + self.dt_bias)
|
|
139
|
+
alpha = torch.exp(A * DT)
|
|
140
|
+
gate = torch.sigmoid(trap.to(torch.float32))
|
|
141
|
+
beta = (1.0 - gate) * DT * alpha
|
|
142
|
+
gamma = gate * DT
|
|
143
|
+
k = self.B_norm(B).expand(-1, -1, -1, self.nheads, -1).to(torch.float32)
|
|
144
|
+
q = self.C_norm(C).expand(-1, -1, -1, self.nheads, -1).to(torch.float32)
|
|
145
|
+
k = k + rearrange(self.B_bias, "h r n -> r h n")
|
|
146
|
+
q = q + rearrange(self.C_bias, "h r n -> r h n")
|
|
147
|
+
turns = torch.tanh(angles.to(torch.float32))[:, :, None, :] * DT[..., None] * math.pi
|
|
148
|
+
cumulative = torch.cumsum(turns, dim=1)
|
|
149
|
+
cos, sin = (torch.cos(cumulative)[:, :, None], torch.sin(cumulative)[:, :, None])
|
|
150
|
+
q = _rotate_pairs(q, cos, sin, interleaved=not self.is_mimo)
|
|
151
|
+
k = _rotate_pairs(k, cos, sin, interleaved=not self.is_mimo)
|
|
152
|
+
if self.is_mimo:
|
|
153
|
+
v = torch.einsum("blhp,hrp->blrhp", x, self.mimo_x.to(torch.float32))
|
|
154
|
+
gates = torch.einsum("blhp,hrp->blrhp", z, self.mimo_z.to(torch.float32))
|
|
155
|
+
else:
|
|
156
|
+
v, gates = (x[:, :, None], z[:, :, None])
|
|
157
|
+
state = u.new_zeros((batch, self.nheads, self.headdim, self.d_state), dtype=torch.float32)
|
|
158
|
+
previous = torch.zeros_like(state)
|
|
159
|
+
outputs = []
|
|
160
|
+
for t in range(seq):
|
|
161
|
+
current = torch.einsum("brhn,brhp->bhpn", k[:, t], v[:, t])
|
|
162
|
+
state = (
|
|
163
|
+
alpha[:, t, :, None, None] * state
|
|
164
|
+
+ beta[:, t, :, None, None] * previous
|
|
165
|
+
+ gamma[:, t, :, None, None] * current
|
|
166
|
+
)
|
|
167
|
+
previous = current
|
|
168
|
+
step = torch.einsum("bhpn,brhn->brhp", state, q[:, t])
|
|
169
|
+
step = step + self.D[None, None, :, None] * v[:, t]
|
|
170
|
+
step = step * F.silu(gates[:, t])
|
|
171
|
+
if self.is_mimo:
|
|
172
|
+
step = torch.einsum("brhp,hrp->bhp", step, self.mimo_o.to(torch.float32))
|
|
173
|
+
else:
|
|
174
|
+
step = step[:, 0]
|
|
175
|
+
outputs.append(step)
|
|
176
|
+
y = rearrange(torch.stack(outputs, dim=1), "b l h p -> b l (h p)")
|
|
177
|
+
return self.out_proj(y.to(u.dtype))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class Mamba3(Mamba3Reference):
|
|
181
|
+
"""Checkpoint-compatible cell with fused CUDA inference when configured by the loader."""
|
|
182
|
+
|
|
183
|
+
fused = False
|
|
184
|
+
|
|
185
|
+
def forward(self, u: Tensor) -> Tensor:
|
|
186
|
+
if self.fused and u.is_cuda and not torch.is_grad_enabled():
|
|
187
|
+
from ._cuda import mamba3_forward
|
|
188
|
+
|
|
189
|
+
return mamba3_forward(self, u)
|
|
190
|
+
return super().forward(u)
|