Upload folder using huggingface_hub
Browse files- README.md +173 -0
- config.py +85 -0
- model.py +401 -0
- seqlens-v2-micro-16k.pt +3 -0
- tokenizer.py +133 -0
- 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 |
+
}
|