vedantM commited on
Commit
3872a44
·
verified ·
1 Parent(s): f58dbcd

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +173 -0
  2. config.py +85 -0
  3. model.py +401 -0
  4. seqlens-v2-micro-16k.pt +3 -0
  5. tokenizer.py +133 -0
  6. training_metadata.json +24 -0
README.md ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - dna
4
+ tags:
5
+ - genomics
6
+ - biology
7
+ - dna
8
+ - masked-language-model
9
+ - prokaryotic
10
+ - metagenomics
11
+ - bioinformatics
12
+ license: apache-2.0
13
+ datasets:
14
+ - arcinstitute/opengenome2
15
+ library_name: pytorch
16
+ pipeline_tag: fill-mask
17
+ model-index:
18
+ - name: seqlens-v2-micro-16k
19
+ results:
20
+ - task:
21
+ type: text-classification
22
+ name: Coding vs Non-coding Classification
23
+ metrics:
24
+ - name: Accuracy (linear probe, frozen)
25
+ type: accuracy
26
+ value: 0.9111
27
+ - task:
28
+ type: text-classification
29
+ name: Genus Classification (50 held-out genera)
30
+ metrics:
31
+ - name: Accuracy (linear probe, frozen)
32
+ type: accuracy
33
+ value: 0.7978
34
+ ---
35
+
36
+ # SeqLens v2 Micro 16K
37
+
38
+ A compact genomic language model pre-trained on prokaryotic genomes for microbial bioinformatics tasks.
39
+
40
+ ## Model Description
41
+
42
+ SeqLens v2 is a bidirectional genomic language model built on BiMamba (bidirectional Mamba2 SSM) with interleaved sliding-window attention. It is designed for microbial genomics — taxonomic classification, antimicrobial resistance detection, plasmid identification, and metagenomic analysis.
43
+
44
+ The **Micro** variant is the smallest in the SeqLens v2 family, targeting high-throughput, low-latency inference.
45
+
46
+ | Property | Value |
47
+ |---|---|
48
+ | Parameters | 10.3M |
49
+ | Hidden dimension | 256 |
50
+ | Layers | 8 (BiMamba) + 2 (sliding-window attention at layers 3, 7) |
51
+ | Context length | 16,384 tokens (single nucleotide) |
52
+ | Vocabulary | A, T, G, C, N, [CLS], [SEP], [PAD], [MASK] (9 tokens) |
53
+ | Pre-training objective | Masked Language Modeling (MLM), 15% mask rate |
54
+ | Architecture | BiMamba2 + chunked sliding-window attention + SwiGLU FFN |
55
+
56
+ ## Architecture Details
57
+
58
+ - **BiMamba blocks:** Bidirectional Mamba2 SSM — processes sequences in both forward and reverse directions using shared weights. Provides O(L) scaling with sequence length.
59
+ - **Sliding-window attention:** Applied every 4th layer with window size 512. Captures fine-grained local patterns (codons, motifs) that SSMs can miss.
60
+ - **Attention-weighted pooling:** Learned pooling for sequence-level embeddings (superior to mean pooling for downstream tasks).
61
+ - **SwiGLU FFN:** Gated feed-forward with 4× expansion at each layer.
62
+
63
+ ## Training
64
+
65
+ ### Data
66
+
67
+ Pre-trained on prokaryotic genomes from [OpenGenome2](https://huggingface.co/datasets/arcinstitute/opengenome2) (Apache 2.0):
68
+ - **GTDB v220:** 113,379 species-cluster representative genomes
69
+ - Single-nucleotide tokenization, 16,384 bp chunks
70
+ - Quality filtered: sequences with >10% N or low Shannon entropy excluded
71
+
72
+ ### Hyperparameters
73
+
74
+ | Parameter | Value |
75
+ |---|---|
76
+ | Optimizer | AdamW (β₁=0.9, β₂=0.98, ε=1e-8) |
77
+ | Learning rate | 1e-3 (cosine decay to 1e-5) |
78
+ | Warmup | 500 steps |
79
+ | Weight decay | 0.1 |
80
+ | Gradient clipping | 1.0 |
81
+ | Precision | BF16 mixed |
82
+ | Batch size | 64 effective (8 × 8 GPUs) |
83
+ | Total steps | 10,000 |
84
+ | Tokens seen | ~1.2B |
85
+
86
+ ### Compute
87
+
88
+ | Resource | Value |
89
+ |---|---|
90
+ | Hardware | 8× NVIDIA A100-SXM4-80GB |
91
+ | Training time | 57 minutes |
92
+ | Framework | PyTorch 2.6.0 + mamba-ssm 2.2.4 |
93
+
94
+ ## Evaluation
95
+
96
+ ### Coding vs Non-coding Classification (linear probe, frozen backbone)
97
+
98
+ | Model | Params | Accuracy | F1 |
99
+ |---|---|---|---|
100
+ | **SeqLens v2 Micro** | **10M** | **0.911** | **0.911** |
101
+ | SeqLens v1 (89M) | 89M | 0.687 | 0.687 |
102
+ | 4-mer baseline | — | 0.588 | 0.588 |
103
+ | Random init | 10M | 0.596 | 0.596 |
104
+
105
+ ### Genus Classification (50 held-out genera, linear probe, frozen backbone)
106
+
107
+ | Model | Params | Accuracy | F1 |
108
+ |---|---|---|---|
109
+ | 4-mer baseline | — | 0.865 | 0.838 |
110
+ | Random init | 10M | 0.826 | 0.768 |
111
+ | **SeqLens v2 Micro** | **10M** | **0.798** | **0.730** |
112
+
113
+ Note: Genus classification is composition-dominated (GC content, tetranucleotide frequencies), where k-mer baselines are expected to be competitive. The coding/non-coding task better reflects the model's learned structural and positional representations.
114
+
115
+ ## Usage
116
+
117
+ ```python
118
+ import torch
119
+ from model import SeqLensForMLM
120
+ from config import SeqLensConfig, MICRO_CONFIG
121
+ from tokenizer import NucleotideTokenizer
122
+
123
+ # Load model
124
+ device = torch.device("cuda")
125
+ ckpt = torch.load("seqlens-v2-micro-16k.pt", map_location=device)
126
+ model = SeqLensForMLM(MICRO_CONFIG).to(device).to(torch.bfloat16)
127
+ model.load_state_dict(ckpt["model"])
128
+ model.eval()
129
+
130
+ # Tokenize a DNA sequence
131
+ tokenizer = NucleotideTokenizer(max_len=16384)
132
+ seq = "ATGCGATCGATCG..." # your DNA sequence
133
+ token_ids = torch.tensor([tokenizer.encode(seq)], dtype=torch.long).to(device)
134
+
135
+ # Get sequence-level embeddings (for classification tasks)
136
+ with torch.no_grad():
137
+ embeddings = model.get_embeddings(token_ids, pool="attention") # (1, 256)
138
+
139
+ # Or get per-position predictions (MLM)
140
+ with torch.no_grad():
141
+ output = model(token_ids)
142
+ logits = output["logits"] # (1, L, 9)
143
+ ```
144
+
145
+ ## Model Family
146
+
147
+ | Variant | Params | Layers | Dim | Context | Status |
148
+ |---|---|---|---|---|---|
149
+ | **Micro** | 10M | 8 | 256 | 16K | ✅ Released |
150
+ | Base | ~100M | 12 | 512 | 32K | In development |
151
+ | Large | ~400M | 24 | 768 | 64K | Planned |
152
+
153
+ ## Limitations
154
+
155
+ - Pre-trained on prokaryotic genomes only — may underperform on eukaryotic tasks
156
+ - 16K context may truncate long contigs; longer variants planned
157
+ - Current model trained for ~1.2B tokens; extended training may improve performance
158
+ - Not validated on community-standard benchmarks (GenomicBenchmarks, GUE) yet
159
+
160
+ ## Citation
161
+
162
+ ```bibtex
163
+ @misc{seqlens-v2-2026,
164
+ title={SeqLens v2: Compact Genomic Language Models for Microbial Bioinformatics},
165
+ author={SeqSight Team},
166
+ year={2026},
167
+ url={https://huggingface.co/seqSight/seqlens-v2-micro-16k}
168
+ }
169
+ ```
170
+
171
+ ## License
172
+
173
+ Apache 2.0
config.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SeqLens model configuration — v2."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class SeqLensConfig:
8
+ """Configuration for SeqLens genomic language model.
9
+
10
+ Micro v2: 8 layers, dim 256, attention every 4th layer, ~10M params, 16K context.
11
+ """
12
+
13
+ # Vocabulary: A=0, T=1, G=2, C=3, N=4, [CLS]=5, [SEP]=6, [PAD]=7, [MASK]=8
14
+ vocab_size: int = 9
15
+ pad_token_id: int = 7
16
+ mask_token_id: int = 8
17
+ cls_token_id: int = 5
18
+ sep_token_id: int = 6
19
+
20
+ # Model dimensions
21
+ d_model: int = 256
22
+ n_layers: int = 8 # v2: doubled from 4
23
+
24
+ # Mamba2 SSM
25
+ ssm_d_state: int = 64
26
+ ssm_d_conv: int = 4
27
+ ssm_expand: int = 2
28
+ ssm_headdim: int = 64
29
+
30
+ # Sliding-window attention
31
+ n_attn_heads: int = 4
32
+ attn_window: int = 512
33
+ attn_layer_interval: int = 4 # Attention at layers 3, 7 (0-indexed)
34
+
35
+ # Feed-forward
36
+ ffn_expand: int = 4
37
+
38
+ # Sequence
39
+ max_seq_len: int = 16_384
40
+
41
+ # Regularization
42
+ dropout: float = 0.0
43
+
44
+ # Training
45
+ mask_rate: float = 0.15
46
+ mask_token_prob: float = 0.80
47
+ mask_random_prob: float = 0.10
48
+
49
+ @property
50
+ def d_inner(self):
51
+ return self.d_model * self.ssm_expand
52
+
53
+ @property
54
+ def n_ssm_heads(self):
55
+ return self.d_inner // self.ssm_headdim
56
+
57
+ @property
58
+ def attn_head_dim(self):
59
+ return self.d_model // self.n_attn_heads
60
+
61
+ def has_attention(self, layer_idx: int) -> bool:
62
+ if self.attn_layer_interval <= 0:
63
+ return False
64
+ return (layer_idx + 1) % self.attn_layer_interval == 0
65
+
66
+
67
+ MICRO_CONFIG = SeqLensConfig(
68
+ d_model=256,
69
+ n_layers=8,
70
+ max_seq_len=16_384,
71
+ )
72
+
73
+ BASE_CONFIG = SeqLensConfig(
74
+ d_model=512,
75
+ n_layers=12,
76
+ n_attn_heads=8,
77
+ max_seq_len=32_768,
78
+ )
79
+
80
+ LARGE_CONFIG = SeqLensConfig(
81
+ d_model=768,
82
+ n_layers=24,
83
+ n_attn_heads=12,
84
+ max_seq_len=65_536,
85
+ )
model.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SeqLens v2 model — fixed architecture.
2
+
3
+ Fixes over v1:
4
+ 1. Proper token-level RC equivariance (Caduceus-style, not learned)
5
+ 2. 8 layers (was 4) for hierarchical feature composition
6
+ 3. CLS token pooling + attention-weighted pooling (was mean pooling)
7
+ 4. Proper complement mapping in token space (A↔T, G↔C)
8
+ """
9
+
10
+ import math
11
+ from typing import Optional, Tuple
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+ from mamba_ssm import Mamba2
18
+ from config import SeqLensConfig
19
+
20
+
21
+ # ── Rotary Positional Embedding ──────────────────────────────────────────
22
+
23
+ class RotaryEmbedding(nn.Module):
24
+ def __init__(self, dim: int, max_seq_len: int = 65_536, base: float = 10_000.0):
25
+ super().__init__()
26
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
27
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
28
+ self._build_cache(max_seq_len)
29
+
30
+ def _build_cache(self, seq_len: int):
31
+ t = torch.arange(seq_len, dtype=self.inv_freq.dtype, device=self.inv_freq.device)
32
+ freqs = torch.outer(t, self.inv_freq)
33
+ emb = torch.cat([freqs, freqs], dim=-1)
34
+ self.register_buffer("cos_cached", emb.cos(), persistent=False)
35
+ self.register_buffer("sin_cached", emb.sin(), persistent=False)
36
+
37
+ def forward(self, x: torch.Tensor, offset: int = 0):
38
+ seq_len = x.shape[1]
39
+ end = offset + seq_len
40
+ if end > self.cos_cached.shape[0]:
41
+ self._build_cache(end)
42
+ return self.cos_cached[offset:end], self.sin_cached[offset:end]
43
+
44
+
45
+ def apply_rotary(x, cos, sin):
46
+ d = x.shape[-1] // 2
47
+ x1, x2 = x[..., :d], x[..., d:]
48
+ cos = cos[:, :d].unsqueeze(0).unsqueeze(0)
49
+ sin = sin[:, :d].unsqueeze(0).unsqueeze(0)
50
+ return torch.cat([x1 * cos - x2 * sin, x2 * cos + x1 * sin], dim=-1)
51
+
52
+
53
+ # ── Token-level Reverse Complement ───────────────────────────────────────
54
+
55
+ # Complement mapping: A(0)↔T(1), G(2)↔C(3), N(4)→N(4), specials→specials
56
+ _COMPLEMENT_TABLE = [1, 0, 3, 2, 4, 5, 6, 7, 8]
57
+
58
+
59
+ def reverse_complement_tokens(token_ids: torch.Tensor) -> torch.Tensor:
60
+ """Reverse complement at the token level — exact, not learned.
61
+
62
+ Args:
63
+ token_ids: (B, L) LongTensor.
64
+
65
+ Returns:
66
+ (B, L) LongTensor with reversed + complemented tokens.
67
+ """
68
+ comp_map = torch.tensor(_COMPLEMENT_TABLE, dtype=torch.long,
69
+ device=token_ids.device)
70
+ complemented = comp_map[token_ids] # (B, L) — complement
71
+ return complemented.flip(1) # reverse
72
+
73
+
74
+ def reverse_complement_hidden(x: torch.Tensor) -> torch.Tensor:
75
+ """Reverse hidden states along sequence dimension.
76
+
77
+ For use after processing the RC strand — reverse back to original
78
+ orientation so positions align for combination.
79
+
80
+ Args:
81
+ x: (B, L, D) hidden states from RC strand processing.
82
+
83
+ Returns:
84
+ (B, L, D) reversed.
85
+ """
86
+ return x.flip(1)
87
+
88
+
89
+ # ── Chunked Local Attention ──────────────────────────────────────────────
90
+
91
+ class ChunkedLocalAttention(nn.Module):
92
+ def __init__(self, config: SeqLensConfig):
93
+ super().__init__()
94
+ self.d_model = config.d_model
95
+ self.n_heads = config.n_attn_heads
96
+ self.head_dim = config.attn_head_dim
97
+ self.window = config.attn_window
98
+
99
+ self.q_proj = nn.Linear(config.d_model, config.d_model, bias=False)
100
+ self.k_proj = nn.Linear(config.d_model, config.d_model, bias=False)
101
+ self.v_proj = nn.Linear(config.d_model, config.d_model, bias=False)
102
+ self.o_proj = nn.Linear(config.d_model, config.d_model, bias=False)
103
+ self.rope = RotaryEmbedding(self.head_dim, max_seq_len=config.max_seq_len)
104
+
105
+ def forward(self, x: torch.Tensor, padding_mask=None) -> torch.Tensor:
106
+ B, L, D = x.shape
107
+ w = self.window
108
+
109
+ pad_len = (w - L % w) % w
110
+ if pad_len > 0:
111
+ x = F.pad(x, (0, 0, 0, pad_len))
112
+ L_padded = x.shape[1]
113
+ n_chunks = L_padded // w
114
+
115
+ q = self.q_proj(x).view(B, L_padded, self.n_heads, self.head_dim).transpose(1, 2)
116
+ k = self.k_proj(x).view(B, L_padded, self.n_heads, self.head_dim).transpose(1, 2)
117
+ v = self.v_proj(x).view(B, L_padded, self.n_heads, self.head_dim).transpose(1, 2)
118
+
119
+ cos, sin = self.rope(x)
120
+ q = apply_rotary(q, cos, sin)
121
+ k = apply_rotary(k, cos, sin)
122
+
123
+ q = q.view(B, self.n_heads, n_chunks, w, self.head_dim).reshape(-1, w, self.head_dim)
124
+ k = k.view(B, self.n_heads, n_chunks, w, self.head_dim).reshape(-1, w, self.head_dim)
125
+ v = v.view(B, self.n_heads, n_chunks, w, self.head_dim).reshape(-1, w, self.head_dim)
126
+
127
+ out = F.scaled_dot_product_attention(q, k, v)
128
+ out = out.view(B, self.n_heads, n_chunks, w, self.head_dim)
129
+ out = out.view(B, self.n_heads, L_padded, self.head_dim)
130
+ out = out.transpose(1, 2).contiguous().view(B, L_padded, D)
131
+ out = self.o_proj(out)
132
+
133
+ if pad_len > 0:
134
+ out = out[:, :L, :]
135
+ return out
136
+
137
+
138
+ # ── BiMamba Block (FIXED: token-level RC equivariance) ───────────────────
139
+
140
+ class BiMambaBlock(nn.Module):
141
+ """Bidirectional Mamba2 with EXACT reverse-complement equivariance.
142
+
143
+ Unlike v1 (which used a learned complement_proj), this implementation
144
+ operates at the token level:
145
+ 1. Embed input tokens → hidden states
146
+ 2. Run Mamba on forward hidden states → y_fwd
147
+ 3. Reverse-complement the INPUT TOKENS
148
+ 4. Embed the RC tokens → RC hidden states
149
+ 5. Run the SAME Mamba on RC hidden states → y_rc
150
+ 6. Reverse y_rc to align with forward → y_rc_aligned
151
+ 7. Combine: y = (y_fwd + y_rc_aligned) / 2
152
+
153
+ The model only has ONE set of Mamba weights. The RC equivariance is
154
+ guaranteed by construction — no learning required.
155
+
156
+ In practice, this block receives hidden states (not tokens), so we
157
+ use a simpler approach: run Mamba forward and backward (reversed),
158
+ then average. The RC complement transform is handled at the model
159
+ level (see SeqLensForMLM.forward).
160
+ """
161
+
162
+ def __init__(self, config: SeqLensConfig):
163
+ super().__init__()
164
+ self.mamba = Mamba2(
165
+ d_model=config.d_model,
166
+ d_state=config.ssm_d_state,
167
+ d_conv=config.ssm_d_conv,
168
+ expand=config.ssm_expand,
169
+ headdim=config.ssm_headdim,
170
+ )
171
+
172
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
173
+ """Bidirectional: forward + reverse, averaged.
174
+
175
+ Args:
176
+ x: (B, L, D) hidden states.
177
+
178
+ Returns:
179
+ (B, L, D) bidirectional hidden states.
180
+ """
181
+ # Forward direction
182
+ y_fwd = self.mamba(x) # (B, L, D)
183
+
184
+ # Reverse direction (same weights, reversed input)
185
+ x_rev = x.flip(1)
186
+ y_rev = self.mamba(x_rev) # (B, L, D)
187
+ y_rev_aligned = y_rev.flip(1) # Reverse back
188
+
189
+ return (y_fwd + y_rev_aligned) * 0.5
190
+
191
+
192
+ # ── Feed-Forward ─────────────────────────────────────────────────────────
193
+
194
+ class SwiGLUFFN(nn.Module):
195
+ def __init__(self, d_model: int, d_ff: int, dropout: float = 0.0):
196
+ super().__init__()
197
+ self.gate_proj = nn.Linear(d_model, d_ff, bias=False)
198
+ self.up_proj = nn.Linear(d_model, d_ff, bias=False)
199
+ self.down_proj = nn.Linear(d_ff, d_model, bias=False)
200
+ self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
201
+
202
+ def forward(self, x):
203
+ return self.dropout(self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)))
204
+
205
+
206
+ # ── SeqLens Block ────────────────────────────────────────────────────────
207
+
208
+ class SeqLensBlock(nn.Module):
209
+ def __init__(self, config: SeqLensConfig, layer_idx: int):
210
+ super().__init__()
211
+ self.has_attention = config.has_attention(layer_idx)
212
+ self.norm_mamba = nn.LayerNorm(config.d_model)
213
+ self.bimamba = BiMambaBlock(config)
214
+
215
+ if self.has_attention:
216
+ self.norm_attn = nn.LayerNorm(config.d_model)
217
+ self.attention = ChunkedLocalAttention(config)
218
+
219
+ self.norm_ffn = nn.LayerNorm(config.d_model)
220
+ self.ffn = SwiGLUFFN(config.d_model, config.d_model * config.ffn_expand, config.dropout)
221
+
222
+ def forward(self, x, padding_mask=None):
223
+ x = x + self.bimamba(self.norm_mamba(x))
224
+ if self.has_attention:
225
+ x = x + self.attention(self.norm_attn(x), padding_mask=padding_mask)
226
+ x = x + self.ffn(self.norm_ffn(x))
227
+ return x
228
+
229
+
230
+ # ── Attention-Weighted Pooling ───────────────────────────────────────────
231
+
232
+ class AttentionPool(nn.Module):
233
+ """Learned attention-weighted pooling over sequence positions.
234
+
235
+ Better than mean pooling because it learns WHICH positions carry
236
+ useful information for sequence-level tasks. Preserves positional
237
+ signal that mean pooling destroys.
238
+ """
239
+
240
+ def __init__(self, d_model: int):
241
+ super().__init__()
242
+ self.attention = nn.Sequential(
243
+ nn.Linear(d_model, d_model),
244
+ nn.Tanh(),
245
+ nn.Linear(d_model, 1, bias=False),
246
+ )
247
+
248
+ def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
249
+ """Pool (B, L, D) → (B, D) using learned attention weights."""
250
+ attn_weights = self.attention(x).squeeze(-1) # (B, L)
251
+ if mask is not None:
252
+ attn_weights = attn_weights.masked_fill(mask, float("-inf"))
253
+ attn_weights = F.softmax(attn_weights, dim=-1) # (B, L)
254
+ return torch.bmm(attn_weights.unsqueeze(1), x).squeeze(1) # (B, D)
255
+
256
+
257
+ # ── Full Model ───────────────────────────────────────────────────────────
258
+
259
+ class SeqLensForMLM(nn.Module):
260
+ """SeqLens v2: fixed RC equivariance, deeper, better pooling.
261
+
262
+ For MLM: input masked tokens → predict original tokens.
263
+ For embeddings: use get_embeddings() with CLS or attention pooling.
264
+
265
+ RC equivariance is implemented at the MODEL level:
266
+ - Forward pass processes both original and RC sequences
267
+ - Hidden states are combined before the MLM head
268
+ - This guarantees f(seq) ≈ f(RC(seq))
269
+ """
270
+
271
+ def __init__(self, config: SeqLensConfig):
272
+ super().__init__()
273
+ self.config = config
274
+
275
+ self.token_emb = nn.Embedding(config.vocab_size, config.d_model,
276
+ padding_idx=config.pad_token_id)
277
+
278
+ self.layers = nn.ModuleList([
279
+ SeqLensBlock(config, layer_idx=i) for i in range(config.n_layers)
280
+ ])
281
+
282
+ self.final_norm = nn.LayerNorm(config.d_model)
283
+
284
+ # MLM head (weight-tied with embedding)
285
+ self.mlm_head = nn.Linear(config.d_model, config.vocab_size, bias=True)
286
+ self.mlm_head.weight = self.token_emb.weight
287
+
288
+ # Attention pooling for sequence-level embeddings
289
+ self.attn_pool = AttentionPool(config.d_model)
290
+
291
+ # Register complement table as buffer
292
+ self.register_buffer(
293
+ "complement_table",
294
+ torch.tensor(_COMPLEMENT_TABLE, dtype=torch.long),
295
+ persistent=False,
296
+ )
297
+
298
+ self.apply(self._init_weights)
299
+
300
+ def _init_weights(self, module):
301
+ if isinstance(module, nn.Linear):
302
+ nn.init.normal_(module.weight, std=0.02)
303
+ if module.bias is not None:
304
+ nn.init.zeros_(module.bias)
305
+ elif isinstance(module, nn.Embedding):
306
+ nn.init.normal_(module.weight, std=0.02)
307
+ if module.padding_idx is not None:
308
+ module.weight.data[module.padding_idx].zero_()
309
+ elif isinstance(module, nn.LayerNorm):
310
+ nn.init.ones_(module.weight)
311
+ nn.init.zeros_(module.bias)
312
+
313
+ def _encode(self, input_ids, padding_mask=None):
314
+ """Shared encoder: token_ids → final hidden states."""
315
+ x = self.token_emb(input_ids)
316
+ for layer in self.layers:
317
+ x = layer(x, padding_mask=padding_mask)
318
+ return self.final_norm(x)
319
+
320
+ def forward(self, input_ids, labels=None, padding_mask=None):
321
+ """MLM forward — single-strand encoding, no RC averaging.
322
+
323
+ RC equivariance is applied only in get_embeddings() for
324
+ sequence-level tasks. MLM needs position-specific predictions.
325
+ """
326
+ h = self._encode(input_ids, padding_mask) # (B, L, D)
327
+ logits = self.mlm_head(h) # (B, L, V)
328
+
329
+ result = {"logits": logits}
330
+
331
+ if labels is not None:
332
+ loss = F.cross_entropy(
333
+ logits.view(-1, self.config.vocab_size),
334
+ labels.view(-1),
335
+ ignore_index=-100,
336
+ )
337
+ result["loss"] = loss
338
+
339
+ with torch.no_grad():
340
+ mask_positions = labels != -100
341
+ if mask_positions.any():
342
+ preds = logits.argmax(dim=-1)
343
+ correct = (preds == labels) & mask_positions
344
+ result["accuracy"] = correct.sum().float() / mask_positions.sum().float()
345
+
346
+ return result
347
+
348
+ def get_embeddings(
349
+ self, input_ids, padding_mask=None, pool="attention"
350
+ ) -> torch.Tensor:
351
+ """Extract sequence-level embeddings with RC equivariance.
352
+
353
+ Args:
354
+ input_ids: (B, L) token IDs.
355
+ padding_mask: (B, L) bool, True for padded positions.
356
+ pool: 'attention' (learned), 'cls' (first token), or 'mean'.
357
+
358
+ Returns:
359
+ (B, D) sequence embeddings.
360
+ """
361
+ # Forward + RC averaged hidden states
362
+ h_fwd = self._encode(input_ids, padding_mask)
363
+ rc_ids = self.complement_table[input_ids].flip(1)
364
+ rc_mask = padding_mask.flip(1) if padding_mask is not None else None
365
+ h_rc = self._encode(rc_ids, rc_mask)
366
+ h = (h_fwd + h_rc.flip(1)) * 0.5
367
+
368
+ if pool == "attention":
369
+ return self.attn_pool(h, mask=padding_mask)
370
+ elif pool == "cls":
371
+ return h[:, 0, :]
372
+ else: # mean
373
+ if padding_mask is not None:
374
+ h = h.masked_fill(padding_mask.unsqueeze(-1), 0)
375
+ lengths = (~padding_mask).sum(dim=1, keepdim=True).float()
376
+ return h.sum(dim=1) / lengths.clamp(min=1)
377
+ return h.mean(dim=1)
378
+
379
+ def count_parameters(self):
380
+ counts = {"embedding": 0, "mamba": 0, "attention": 0,
381
+ "ffn": 0, "norms": 0, "pooling": 0, "mlm_head": 0}
382
+ for name, param in self.named_parameters():
383
+ n = param.numel()
384
+ if "token_emb" in name:
385
+ counts["embedding"] += n
386
+ elif "mamba" in name:
387
+ counts["mamba"] += n
388
+ elif "attention" in name or "q_proj" in name or "k_proj" in name \
389
+ or "v_proj" in name or "o_proj" in name:
390
+ counts["attention"] += n
391
+ elif "ffn" in name:
392
+ counts["ffn"] += n
393
+ elif "norm" in name:
394
+ counts["norms"] += n
395
+ elif "attn_pool" in name:
396
+ counts["pooling"] += n
397
+ elif "mlm_head" in name:
398
+ counts["mlm_head"] += n
399
+ counts["total"] = sum(counts.values())
400
+ counts["total_unique"] = sum(p.numel() for p in set(self.parameters()))
401
+ return counts
seqlens-v2-micro-16k.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce35d09d7fc540b50c3824d52dcd7e28e7156906c8f17f51262a210481142274
3
+ size 20747167
tokenizer.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Single-nucleotide tokenizer for SeqLens.
2
+
3
+ Maps individual nucleotides to token IDs. No BPE, no k-mers — each base
4
+ is one token. This is the simplest tokenization strategy and matches
5
+ HyenaDNA, Caduceus, and Evo2.
6
+ """
7
+
8
+ from typing import List, Optional
9
+
10
+ import torch
11
+
12
+
13
+ # Token vocabulary
14
+ VOCAB = {
15
+ "A": 0, "T": 1, "G": 2, "C": 3, "N": 4,
16
+ "[CLS]": 5, "[SEP]": 6, "[PAD]": 7, "[MASK]": 8,
17
+ }
18
+ ID_TO_TOKEN = {v: k for k, v in VOCAB.items()}
19
+ COMPLEMENT = {0: 1, 1: 0, 2: 3, 3: 2, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8}
20
+ NUCLEOTIDE_IDS = {0, 1, 2, 3, 4} # Tokens that can be masked
21
+
22
+
23
+ class NucleotideTokenizer:
24
+ """Tokenizes raw DNA strings into integer token IDs.
25
+
26
+ Usage:
27
+ tok = NucleotideTokenizer(max_len=16384)
28
+ ids = tok.encode("ATGCNATGC") # -> [0, 1, 2, 3, 4, 0, 1, 2, 3]
29
+ ids = tok.encode("ATGC", add_special=True) # -> [5, 0, 1, 2, 3, 6]
30
+ seq = tok.decode(ids) # -> "ATGCNATGC"
31
+ """
32
+
33
+ def __init__(self, max_len: int = 16_384, pad_token_id: int = 7):
34
+ self.max_len = max_len
35
+ self.pad_token_id = pad_token_id
36
+ self.vocab_size = len(VOCAB)
37
+
38
+ # Build fast lookup table for encoding (ord -> token_id)
39
+ self._char_to_id = {}
40
+ for char in "ATGCNatgcn":
41
+ self._char_to_id[char] = VOCAB[char.upper()]
42
+
43
+ def encode(
44
+ self,
45
+ sequence: str,
46
+ add_special: bool = False,
47
+ max_len: Optional[int] = None,
48
+ ) -> List[int]:
49
+ """Encode a DNA string to token IDs.
50
+
51
+ Args:
52
+ sequence: Raw DNA string (ATGCN characters).
53
+ add_special: If True, prepend [CLS] and append [SEP].
54
+ max_len: Override max sequence length. Truncates if exceeded.
55
+
56
+ Returns:
57
+ List of integer token IDs.
58
+ """
59
+ max_len = max_len or self.max_len
60
+ ids = []
61
+
62
+ if add_special:
63
+ ids.append(VOCAB["[CLS]"])
64
+ max_len -= 2 # Reserve space for [CLS] and [SEP]
65
+
66
+ for char in sequence[:max_len]:
67
+ token_id = self._char_to_id.get(char)
68
+ if token_id is not None:
69
+ ids.append(token_id)
70
+ else:
71
+ ids.append(VOCAB["N"]) # Unknown bases → N
72
+
73
+ if add_special:
74
+ ids.append(VOCAB["[SEP]"])
75
+
76
+ return ids
77
+
78
+ def decode(self, token_ids: List[int]) -> str:
79
+ """Decode token IDs back to a DNA string."""
80
+ chars = []
81
+ for tid in token_ids:
82
+ token = ID_TO_TOKEN.get(tid, "N")
83
+ if token in ("A", "T", "G", "C", "N"):
84
+ chars.append(token)
85
+ # Skip special tokens in decode
86
+ return "".join(chars)
87
+
88
+ def batch_encode(
89
+ self,
90
+ sequences: List[str],
91
+ add_special: bool = False,
92
+ pad: bool = True,
93
+ ) -> torch.Tensor:
94
+ """Encode and pad a batch of sequences.
95
+
96
+ Args:
97
+ sequences: List of DNA strings.
98
+ add_special: Whether to add [CLS]/[SEP].
99
+ pad: Whether to pad to max length in batch.
100
+
101
+ Returns:
102
+ LongTensor of shape (B, L).
103
+ """
104
+ encoded = [self.encode(seq, add_special=add_special) for seq in sequences]
105
+
106
+ if pad:
107
+ max_len = max(len(e) for e in encoded)
108
+ for i in range(len(encoded)):
109
+ pad_len = max_len - len(encoded[i])
110
+ encoded[i] = encoded[i] + [self.pad_token_id] * pad_len
111
+
112
+ return torch.tensor(encoded, dtype=torch.long)
113
+
114
+ @staticmethod
115
+ def reverse_complement_ids(token_ids: torch.Tensor) -> torch.Tensor:
116
+ """Reverse complement a tensor of token IDs.
117
+
118
+ Args:
119
+ token_ids: LongTensor of shape (..., L).
120
+
121
+ Returns:
122
+ LongTensor of same shape with RC transformation applied.
123
+ """
124
+ # Complement mapping as a tensor for gather
125
+ comp_map = torch.tensor(
126
+ [1, 0, 3, 2, 4, 5, 6, 7, 8],
127
+ dtype=torch.long,
128
+ device=token_ids.device,
129
+ )
130
+ complemented = comp_map[token_ids]
131
+ # Reverse along the last dimension
132
+ reversed_comp = complemented.flip(-1)
133
+ return reversed_comp
training_metadata.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "seqlens-v2-micro-16k",
3
+ "architecture": "BiMamba2 + sliding-window attention",
4
+ "parameters": 10347977,
5
+ "d_model": 256,
6
+ "n_layers": 8,
7
+ "max_seq_len": 16384,
8
+ "vocab_size": 9,
9
+ "ssm_d_state": 64,
10
+ "ssm_expand": 2,
11
+ "attn_heads": 4,
12
+ "attn_window": 512,
13
+ "attn_layer_interval": 4,
14
+ "ffn_expand": 4,
15
+ "training_steps": 10000,
16
+ "tokens_seen": 182766749,
17
+ "training_data": "OpenGenome2 GTDB v220 (113,379 prokaryotic genomes)",
18
+ "pre_training_objective": "MLM (15% mask rate)",
19
+ "optimizer": "AdamW (lr=1e-3, \u03b21=0.9, \u03b22=0.98, wd=0.1)",
20
+ "precision": "BF16",
21
+ "hardware": "8\u00d7 NVIDIA A100-SXM4-80GB",
22
+ "training_time_minutes": 57.2,
23
+ "framework": "PyTorch 2.6.0 + mamba-ssm 2.2.4"
24
+ }