Token Classification
Transformers
Safetensors
generanno
dna
genomics
biology
genome-annotation
custom_code
Instructions to use HuggingFaceBio/Carbon-A-1.2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use HuggingFaceBio/Carbon-A-1.2B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="HuggingFaceBio/Carbon-A-1.2B", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForTokenClassification model = AutoModelForTokenClassification.from_pretrained("HuggingFaceBio/Carbon-A-1.2B", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Match genome-scale inference windowing and batching
#3
by kashif HF Staff - opened
- INFERENCE.md +8 -6
- README.md +12 -13
- infer_genbank.py +129 -64
INFERENCE.md
CHANGED
|
@@ -18,11 +18,13 @@ python infer_genbank.py genome.fna.gz --format fasta --out-dir genome-prediction
|
|
| 18 |
|
| 19 |
Both commands process every sequence record in the file. Use `--revision <model-commit>` to pin the model, or `--threshold 0.7` to change the binary-call threshold (default 0.5).
|
| 20 |
|
| 21 |
-
The script uses
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|
| 27 |
```python
|
| 28 |
import json
|
|
@@ -34,9 +36,9 @@ record = json.loads((out / "summary.json").read_text())["records"][0]
|
|
| 34 |
with np.load(out / record["file"]) as prediction:
|
| 35 |
plus = prediction["cds_probability_plus"]
|
| 36 |
minus = prediction["cds_probability_minus"]
|
| 37 |
-
coding = prediction["cds_label_any"] #
|
| 38 |
-
|
| 39 |
-
print(record["accession"], plus.shape, int(
|
| 40 |
```
|
| 41 |
|
| 42 |
## Run inference on packed data
|
|
|
|
| 18 |
|
| 19 |
Both commands process every sequence record in the file. Use `--revision <model-commit>` to pin the model, or `--threshold 0.7` to change the binary-call threshold (default 0.5).
|
| 20 |
|
| 21 |
+
The script uses the same windowing and batching as Carbon-A's genome-scale predictions. Windows are 98,304 bases (16,384 tokens) and consecutive windows overlap by 6,144 bases (`--overlap-length`, default 1,024 tokens). The last window of a longer record is aligned to the record's end, so all windows are full length. A record shorter than one window is covered by one window from its start, of whole 6-mers, plus, when its length is not a multiple of six, a second one aligned to its end. CDS probabilities from the two strand heads are averaged where windows overlap. No BOS/EOS tokens or `N` bases are added; windows shorter than 16,384 tokens are padded with the pad token and masked. Records shorter than six bases get probability 0. Short records are supported, but the training data used full-length 98,304-base windows.
|
| 22 |
|
| 23 |
+
In BF16, probabilities depend slightly on the attention kernel and on how windows are batched. The script therefore batches the same way: records are processed longest first in groups of four (`--dispatch-chunk-size`), and full-length windows and shorter, padded windows are run in separate batches of eight (`--batch-size`). The genome-scale predictions used FlashAttention-2. With `flash-attn`, Transformers 4.x, and `--attn-implementation flash_attention_2`, the script reproduced those probabilities bit for bit in our tests. The default, PyTorch SDPA, needs no extra package; in the same tests, single-base probabilities differed by up to 0.11, and at least 99.8% of 0.5 calls were unchanged. A smaller `--batch-size` uses less GPU memory but also changes results slightly.
|
| 24 |
|
| 25 |
+
Outputs are one compressed NumPy file per record, named with its accession and record name, plus `summary.json`. Each file contains forward/reverse CDS probabilities and calls, a combined `cds_label_any` track, an `ambiguous` mask for bases other than A, C, G, and T, and the number of contributing windows per base (`window_count`). Array position `i` corresponds to record base `i + 1` on **both** strands; the reverse track is not reversed. These are coding-occupancy predictions, not GFF gene or transcript models.
|
| 26 |
+
|
| 27 |
+
Every base receives a probability. A 6-mer containing an ambiguous nucleotide is tokenized as an unknown token and still predicted; its predictions are less reliable. Record sequences must be present in the file; annotation-only records with undefined sequence cannot be used. The example retains per-base arrays for one record in memory, so large chromosomes require more host RAM.
|
| 28 |
|
| 29 |
```python
|
| 30 |
import json
|
|
|
|
| 36 |
with np.load(out / record["file"]) as prediction:
|
| 37 |
plus = prediction["cds_probability_plus"]
|
| 38 |
minus = prediction["cds_probability_minus"]
|
| 39 |
+
coding = prediction["cds_label_any"] # 1 if either strand passes the threshold
|
| 40 |
+
ambiguous = prediction["ambiguous"]
|
| 41 |
+
print(record["accession"], plus.shape, int(ambiguous.sum()))
|
| 42 |
```
|
| 43 |
|
| 44 |
## Run inference on packed data
|
README.md
CHANGED
|
@@ -48,9 +48,12 @@ Full-length 98,304-base windows rely on PyTorch's fused SDPA kernels (FlashAtten
|
|
| 48 |
|
| 49 |
### Use from Python
|
| 50 |
|
|
|
|
|
|
|
| 51 |
```python
|
| 52 |
import torch
|
| 53 |
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
|
|
|
| 54 |
|
| 55 |
model_id = "HuggingFaceBio/Carbon-A-1.2B"
|
| 56 |
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
|
@@ -58,16 +61,12 @@ model = AutoModelForTokenClassification.from_pretrained(
|
|
| 58 |
model_id, trust_remote_code=True, dtype=torch.bfloat16, attn_implementation="sdpa"
|
| 59 |
).to("cuda").eval()
|
| 60 |
|
| 61 |
-
sequence = "ATGTCTGCTACACAAAAGCAACACTAA" #
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
inputs = tokenizer(sequence, add_special_tokens=False, return_tensors="pt").to("cuda")
|
| 65 |
-
with torch.inference_mode():
|
| 66 |
-
logits = model(**inputs).logits # (1, 2 * len(sequence), 2)
|
| 67 |
-
cds_plus, cds_minus = logits.float().softmax(-1)[0, :, 1].reshape(2, -1).cpu()
|
| 68 |
```
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
### Annotate a GenBank or FASTA file
|
| 73 |
|
|
@@ -81,7 +80,7 @@ python infer_genbank.py genome.gbff.gz --out-dir predictions
|
|
| 81 |
python infer_genbank.py genome.fna.gz --format fasta --out-dir predictions-fasta
|
| 82 |
```
|
| 83 |
|
| 84 |
-
|
| 85 |
|
| 86 |
Each record produces an `.npz` file named with its accession and record name, plus an entry in `summary.json`. To read the predictions:
|
| 87 |
|
|
@@ -96,10 +95,10 @@ with np.load(out / record["file"]) as pred:
|
|
| 96 |
plus = pred["cds_probability_plus"]
|
| 97 |
minus = pred["cds_probability_minus"]
|
| 98 |
coding = pred["cds_label_any"] # 1 if either strand passes the threshold
|
| 99 |
-
|
| 100 |
```
|
| 101 |
|
| 102 |
-
Array index `i` corresponds to record base `i + 1` on both strands.
|
| 103 |
|
| 104 |
### Run on packed training or evaluation data
|
| 105 |
|
|
@@ -130,7 +129,7 @@ The **[packed-data bucket](https://huggingface.co/buckets/HuggingFaceBio/Carbon-
|
|
| 130 |
The inference examples have passed functional checks on an **NVIDIA H100**, using BF16, PyTorch 2.7.1, and Transformers 4.56.0:
|
| 131 |
|
| 132 |
- Two full-length packed evaluation windows, including strand outputs and thresholded calls.
|
| 133 |
-
- The complete **230,218-base yeast chromosome I** record
|
| 134 |
- Identical predictions when the same sequence is supplied as GenBank or FASTA.
|
| 135 |
- Independent checks of overlap averaging, coordinates, ambiguous bases, and short records.
|
| 136 |
|
|
@@ -140,7 +139,7 @@ These checks verify the inference code. **They do not measure agreement with ref
|
|
| 140 |
|
| 141 |
- Predictions represent **CDS occupancy**. Transcript paths, alternative isoforms, and complete gene structures are not resolved.
|
| 142 |
- The model is intended for **eukaryotic sequence**. Bacteria and viruses are outside its training scope.
|
| 143 |
-
-
|
| 144 |
- Shorter records can be processed, but training used full-length 98,304-base windows.
|
| 145 |
- The supplied evaluation panel was used during training; it is not a newly held-out test set.
|
| 146 |
- Full-length inference on CPU is slow. The file example holds per-base arrays for one record in memory, so large chromosomes need more host RAM.
|
|
|
|
| 48 |
|
| 49 |
### Use from Python
|
| 50 |
|
| 51 |
+
Run from the downloaded repository, so that `infer_genbank.py` can be imported:
|
| 52 |
+
|
| 53 |
```python
|
| 54 |
import torch
|
| 55 |
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
| 56 |
+
from infer_genbank import predict_record
|
| 57 |
|
| 58 |
model_id = "HuggingFaceBio/Carbon-A-1.2B"
|
| 59 |
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
|
|
|
| 61 |
model_id, trust_remote_code=True, dtype=torch.bfloat16, attn_implementation="sdpa"
|
| 62 |
).to("cuda").eval()
|
| 63 |
|
| 64 |
+
sequence = "ATGTCTGCTACACAAAAGCAACACTAA" # any length
|
| 65 |
+
probs, window_count, num_windows = predict_record(sequence, model, tokenizer, "cuda")
|
| 66 |
+
cds_plus, cds_minus = probs # per-base P(CDS) on the forward and reverse strand
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
```
|
| 68 |
|
| 69 |
+
`predict_record` handles windowing, including records whose length is not a multiple of six. To call the model directly, pass one window: an uppercase sequence of whole 6-mers, at most 98,304 bases, tokenized with `add_special_tokens=False`, together with the `attention_mask` the tokenizer returns. The tokenizer otherwise adds a BOS token, and lowercase (soft-masked) bases are not grouped into 6-mers, which shifts every later position. The logits are per base: all forward-strand positions, then all reverse-strand positions, both in input coordinates.
|
| 70 |
|
| 71 |
### Annotate a GenBank or FASTA file
|
| 72 |
|
|
|
|
| 80 |
python infer_genbank.py genome.fna.gz --format fasta --out-dir predictions-fasta
|
| 81 |
```
|
| 82 |
|
| 83 |
+
Records are split into **98,304-base windows that overlap by 6,144 bases** (1,024 tokens). The last window is aligned to the end of the record, so every window is full length and no `N` bases are added. A record shorter than one window, whose length is not a multiple of six, is run twice: once aligned to its start and once to its end. Probabilities are averaged where windows overlap. Windowing and batching match Carbon-A's genome-scale predictions; see the inference guide for reproducing them exactly. Existing GenBank annotations are not model inputs.
|
| 84 |
|
| 85 |
Each record produces an `.npz` file named with its accession and record name, plus an entry in `summary.json`. To read the predictions:
|
| 86 |
|
|
|
|
| 95 |
plus = pred["cds_probability_plus"]
|
| 96 |
minus = pred["cds_probability_minus"]
|
| 97 |
coding = pred["cds_label_any"] # 1 if either strand passes the threshold
|
| 98 |
+
ambiguous = pred["ambiguous"] # bases other than A, C, G, T
|
| 99 |
```
|
| 100 |
|
| 101 |
+
Array index `i` corresponds to record base `i + 1` on both strands. Every base has a probability; records shorter than six bases get 0.
|
| 102 |
|
| 103 |
### Run on packed training or evaluation data
|
| 104 |
|
|
|
|
| 129 |
The inference examples have passed functional checks on an **NVIDIA H100**, using BF16, PyTorch 2.7.1, and Transformers 4.56.0:
|
| 130 |
|
| 131 |
- Two full-length packed evaluation windows, including strand outputs and thresholded calls.
|
| 132 |
+
- The complete **230,218-base yeast chromosome I** record.
|
| 133 |
- Identical predictions when the same sequence is supplied as GenBank or FASTA.
|
| 134 |
- Independent checks of overlap averaging, coordinates, ambiguous bases, and short records.
|
| 135 |
|
|
|
|
| 139 |
|
| 140 |
- Predictions represent **CDS occupancy**. Transcript paths, alternative isoforms, and complete gene structures are not resolved.
|
| 141 |
- The model is intended for **eukaryotic sequence**. Bacteria and viruses are outside its training scope.
|
| 142 |
+
- A 6-mer containing an ambiguous nucleotide (`N` or another IUPAC code) is an unknown token. It still receives predictions, but they are less reliable; the `ambiguous` array marks these bases.
|
| 143 |
- Shorter records can be processed, but training used full-length 98,304-base windows.
|
| 144 |
- The supplied evaluation panel was used during training; it is not a newly held-out test set.
|
| 145 |
- Full-length inference on CPU is slow. The file example holds per-base arrays for one record in memory, so large chromosomes need more host RAM.
|
infer_genbank.py
CHANGED
|
@@ -11,59 +11,96 @@ import torch
|
|
| 11 |
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
| 12 |
|
| 13 |
MODEL_ID = 'HuggingFaceBio/Carbon-A-1.2B'
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
def safe_name(value):
|
| 19 |
return re.sub(r'[^A-Za-z0-9_.-]+', '_', value).strip('._')[:100] or 'record'
|
| 20 |
|
| 21 |
|
| 22 |
-
def
|
| 23 |
-
"""
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
with torch.inference_mode():
|
| 32 |
-
for
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
def main():
|
|
@@ -74,10 +111,22 @@ def main():
|
|
| 74 |
parser.add_argument('--revision', default='main', help='Use a model commit for repeatable runs.')
|
| 75 |
parser.add_argument('--device', choices=['cuda', 'cpu'], default='cuda')
|
| 76 |
parser.add_argument('--threshold', type=float, default=0.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
parser.add_argument('--out-dir', type=Path, default=Path('genbank-predictions'))
|
| 78 |
args = parser.parse_args()
|
| 79 |
if not 0 <= args.threshold <= 1:
|
| 80 |
parser.error('Threshold must be between 0 and 1.')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
if not args.input.is_file():
|
| 82 |
parser.error('Input file does not exist.')
|
| 83 |
if args.device == 'cuda' and not torch.cuda.is_available():
|
|
@@ -87,35 +136,51 @@ def main():
|
|
| 87 |
dtype = torch.float32
|
| 88 |
if args.device == 'cuda':
|
| 89 |
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
tokenizer = AutoTokenizer.from_pretrained(args.model, revision=args.revision, trust_remote_code=True)
|
| 91 |
model = AutoModelForTokenClassification.from_pretrained(
|
| 92 |
args.model, revision=args.revision, trust_remote_code=True,
|
| 93 |
-
dtype=dtype, attn_implementation=
|
| 94 |
).to(args.device).eval()
|
| 95 |
-
if (model.config.k, model.config.num_prediction_heads, model.config.num_labels) != (
|
| 96 |
raise ValueError('Expected a 6-mer, two-strand binary CDS model.')
|
| 97 |
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 98 |
summary = {'model':args.model, 'revision_requested':args.revision,
|
| 99 |
'revision_resolved':getattr(model.config, '_commit_hash', None),
|
| 100 |
'input_file':str(args.input), 'input_format':args.format,
|
| 101 |
-
'dtype':str(dtype), '
|
| 102 |
-
'
|
|
|
|
|
|
|
| 103 |
'coordinates':'Array index i corresponds to record base i+1 on both strands.',
|
| 104 |
'records':[]}
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
(args.out_dir / 'summary.json').write_text(json.dumps(summary, indent=2)+'\n')
|
| 120 |
|
| 121 |
|
|
|
|
| 11 |
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
| 12 |
|
| 13 |
MODEL_ID = 'HuggingFaceBio/Carbon-A-1.2B'
|
| 14 |
+
CONTEXT_TOKENS = 16384
|
| 15 |
+
OVERLAP_TOKENS = 1024
|
| 16 |
+
BATCH_SIZE = 8
|
| 17 |
+
DISPATCH_CHUNK_SIZE = 4
|
| 18 |
+
K = 6
|
| 19 |
|
| 20 |
|
| 21 |
def safe_name(value):
|
| 22 |
return re.sub(r'[^A-Za-z0-9_.-]+', '_', value).strip('._')[:100] or 'record'
|
| 23 |
|
| 24 |
|
| 25 |
+
def windows_for_sequence(length, window=CONTEXT_TOKENS * K, overlap=OVERLAP_TOKENS * K):
|
| 26 |
+
"""(start, length) windows, each a whole number of 6-mers; the last is right-aligned."""
|
| 27 |
+
if length < window:
|
| 28 |
+
windows = [(0, length - length % K)] if length >= K else []
|
| 29 |
+
if length % K and length >= K:
|
| 30 |
+
windows.append((length % K, length - length % K))
|
| 31 |
+
return windows
|
| 32 |
+
windows, start, remaining = [], 0, length
|
| 33 |
+
while True:
|
| 34 |
+
size = min(window, remaining)
|
| 35 |
+
windows.append((start, size))
|
| 36 |
+
if size == remaining:
|
| 37 |
+
return windows
|
| 38 |
+
if remaining - window + overlap < window:
|
| 39 |
+
start, remaining = length - window, window
|
| 40 |
+
else:
|
| 41 |
+
start, remaining = start + window - overlap, remaining - (window - overlap)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def predict_records(sequences, model, tokenizer, device, overlap_tokens=OVERLAP_TOKENS,
|
| 45 |
+
batch_size=BATCH_SIZE, dispatch_chunk_size=DISPATCH_CHUNK_SIZE):
|
| 46 |
+
"""Yield (index, probabilities, window_count, num_windows) for each sequence.
|
| 47 |
+
|
| 48 |
+
Probabilities have shape (2, length): forward, reverse strand. Overlapping windows are
|
| 49 |
+
averaged; sequences shorter than six bases get probability 0. Sequences are processed
|
| 50 |
+
longest first, in groups of `dispatch_chunk_size`, with full-length and padded windows
|
| 51 |
+
batched separately. Batch composition changes BF16 results slightly, so keep the
|
| 52 |
+
defaults to reproduce Carbon-A's genome-scale predictions.
|
| 53 |
+
"""
|
| 54 |
+
sequences = [sequence.upper() for sequence in sequences]
|
| 55 |
+
order = sorted(range(len(sequences)), key=lambda i: len(sequences[i]), reverse=True)
|
| 56 |
+
pad_id = tokenizer.pad_token_id
|
| 57 |
with torch.inference_mode():
|
| 58 |
+
for chunk_start in range(0, len(order), dispatch_chunk_size):
|
| 59 |
+
chunk = order[chunk_start:chunk_start + dispatch_chunk_size]
|
| 60 |
+
sums = {i: torch.zeros((2, len(sequences[i])), dtype=torch.float32) for i in chunk}
|
| 61 |
+
counts = {i: torch.zeros(len(sequences[i]), dtype=torch.long) for i in chunk}
|
| 62 |
+
num_windows = {}
|
| 63 |
+
full, padded = [], []
|
| 64 |
+
|
| 65 |
+
def run_batch(batch):
|
| 66 |
+
ids = [tokens + [pad_id] * (CONTEXT_TOKENS - len(tokens)) for *_, tokens in batch]
|
| 67 |
+
mask = [[1] * len(tokens) + [0] * (CONTEXT_TOKENS - len(tokens)) for *_, tokens in batch]
|
| 68 |
+
logits = model(input_ids=torch.tensor(ids, device=device),
|
| 69 |
+
attention_mask=torch.tensor(mask, device=device)).logits
|
| 70 |
+
probs = logits.softmax(dim=-1).cpu()
|
| 71 |
+
for row, (i, start, size, _) in enumerate(batch):
|
| 72 |
+
sums[i][:, start:start + size] += probs[row, :2 * size].view(2, size, -1)[:, :, 1].float()
|
| 73 |
+
counts[i][start:start + size] += 1
|
| 74 |
+
|
| 75 |
+
for i in chunk:
|
| 76 |
+
windows = windows_for_sequence(len(sequences[i]), overlap=overlap_tokens * K)
|
| 77 |
+
num_windows[i] = len(windows)
|
| 78 |
+
for start, size in windows:
|
| 79 |
+
tokens = tokenizer(sequences[i][start:start + size], add_special_tokens=False)['input_ids']
|
| 80 |
+
if len(tokens) * K != size:
|
| 81 |
+
raise ValueError(f'Tokenizer produced {len(tokens)} tokens for {size} bases')
|
| 82 |
+
pending = full if len(tokens) == CONTEXT_TOKENS else padded
|
| 83 |
+
pending.append((i, start, size, tokens))
|
| 84 |
+
if len(pending) >= batch_size:
|
| 85 |
+
run_batch(pending[:batch_size])
|
| 86 |
+
del pending[:batch_size]
|
| 87 |
+
for pending in (full, padded):
|
| 88 |
+
while pending:
|
| 89 |
+
run_batch(pending[:batch_size])
|
| 90 |
+
del pending[:batch_size]
|
| 91 |
+
|
| 92 |
+
for i in chunk:
|
| 93 |
+
if num_windows[i] and torch.any(counts[i] == 0):
|
| 94 |
+
raise RuntimeError('Some bases were not covered by any window')
|
| 95 |
+
probs = (sums[i] / counts[i].clamp(min=1).float()).numpy()
|
| 96 |
+
yield i, probs, counts[i].numpy().astype(np.uint32), num_windows[i]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def predict_record(sequence, model, tokenizer, device, overlap_tokens=OVERLAP_TOKENS):
|
| 100 |
+
"""Return (probabilities, window_count, num_windows) for one sequence; see predict_records."""
|
| 101 |
+
_, probs, counts, num_windows = next(predict_records([sequence], model, tokenizer, device,
|
| 102 |
+
overlap_tokens))
|
| 103 |
+
return probs, counts, num_windows
|
| 104 |
|
| 105 |
|
| 106 |
def main():
|
|
|
|
| 111 |
parser.add_argument('--revision', default='main', help='Use a model commit for repeatable runs.')
|
| 112 |
parser.add_argument('--device', choices=['cuda', 'cpu'], default='cuda')
|
| 113 |
parser.add_argument('--threshold', type=float, default=0.5)
|
| 114 |
+
parser.add_argument('--overlap-length', type=int, default=OVERLAP_TOKENS,
|
| 115 |
+
help='Overlap between consecutive windows, in 6-mer tokens.')
|
| 116 |
+
parser.add_argument('--batch-size', type=int, default=BATCH_SIZE,
|
| 117 |
+
help='Windows per forward pass; lower it if GPU memory is short.')
|
| 118 |
+
parser.add_argument('--dispatch-chunk-size', type=int, default=DISPATCH_CHUNK_SIZE,
|
| 119 |
+
help='Records whose windows are batched together.')
|
| 120 |
+
parser.add_argument('--attn-implementation', choices=['sdpa', 'flash_attention_2'], default='sdpa',
|
| 121 |
+
help='flash_attention_2 requires the flash-attn package.')
|
| 122 |
parser.add_argument('--out-dir', type=Path, default=Path('genbank-predictions'))
|
| 123 |
args = parser.parse_args()
|
| 124 |
if not 0 <= args.threshold <= 1:
|
| 125 |
parser.error('Threshold must be between 0 and 1.')
|
| 126 |
+
if not 0 <= args.overlap_length < CONTEXT_TOKENS:
|
| 127 |
+
parser.error(f'Overlap length must be between 0 and {CONTEXT_TOKENS - 1} tokens.')
|
| 128 |
+
if args.batch_size < 1 or args.dispatch_chunk_size < 1:
|
| 129 |
+
parser.error('Batch size and dispatch chunk size must be positive.')
|
| 130 |
if not args.input.is_file():
|
| 131 |
parser.error('Input file does not exist.')
|
| 132 |
if args.device == 'cuda' and not torch.cuda.is_available():
|
|
|
|
| 136 |
dtype = torch.float32
|
| 137 |
if args.device == 'cuda':
|
| 138 |
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
| 139 |
+
opener = gzip.open if args.input.suffix.lower() == '.gz' else open
|
| 140 |
+
with opener(args.input, 'rt') as handle:
|
| 141 |
+
records = [(record.id, record.name, str(record.seq).upper())
|
| 142 |
+
for record in SeqIO.parse(handle, args.format)]
|
| 143 |
+
if not records:
|
| 144 |
+
parser.error('No sequence records found; check the input format.')
|
| 145 |
+
for accession, _, sequence in records:
|
| 146 |
+
if not sequence:
|
| 147 |
+
raise ValueError(f'Empty sequence in record {accession}')
|
| 148 |
tokenizer = AutoTokenizer.from_pretrained(args.model, revision=args.revision, trust_remote_code=True)
|
| 149 |
model = AutoModelForTokenClassification.from_pretrained(
|
| 150 |
args.model, revision=args.revision, trust_remote_code=True,
|
| 151 |
+
dtype=dtype, attn_implementation=args.attn_implementation,
|
| 152 |
).to(args.device).eval()
|
| 153 |
+
if (model.config.k, model.config.num_prediction_heads, model.config.num_labels) != (K, 2, 2):
|
| 154 |
raise ValueError('Expected a 6-mer, two-strand binary CDS model.')
|
| 155 |
args.out_dir.mkdir(parents=True, exist_ok=True)
|
| 156 |
summary = {'model':args.model, 'revision_requested':args.revision,
|
| 157 |
'revision_resolved':getattr(model.config, '_commit_hash', None),
|
| 158 |
'input_file':str(args.input), 'input_format':args.format,
|
| 159 |
+
'dtype':str(dtype), 'attn_implementation':args.attn_implementation,
|
| 160 |
+
'threshold':args.threshold, 'window_bp':CONTEXT_TOKENS * K,
|
| 161 |
+
'overlap_bp':args.overlap_length * K, 'batch_size':args.batch_size,
|
| 162 |
+
'dispatch_chunk_size':args.dispatch_chunk_size,
|
| 163 |
'coordinates':'Array index i corresponds to record base i+1 on both strands.',
|
| 164 |
'records':[]}
|
| 165 |
+
entries = {}
|
| 166 |
+
predictions = predict_records([sequence for *_, sequence in records], model, tokenizer,
|
| 167 |
+
args.device, args.overlap_length, args.batch_size,
|
| 168 |
+
args.dispatch_chunk_size)
|
| 169 |
+
for index, probs, counts, windows in predictions:
|
| 170 |
+
accession, name, sequence = records[index]
|
| 171 |
+
calls = (probs >= args.threshold).astype(np.int8)
|
| 172 |
+
ambiguous = ~np.isin(np.frombuffer(sequence.encode(), dtype=np.uint8),
|
| 173 |
+
np.frombuffer(b'ACGT', dtype=np.uint8))
|
| 174 |
+
arrays = {'cds_probability_plus': probs[0], 'cds_probability_minus': probs[1],
|
| 175 |
+
'cds_label_plus': calls[0], 'cds_label_minus': calls[1],
|
| 176 |
+
'cds_label_any': calls.max(axis=0), 'ambiguous': ambiguous,
|
| 177 |
+
'window_count': counts}
|
| 178 |
+
filename = f'{index:04d}_{safe_name(accession)}_{safe_name(name)}.npz'
|
| 179 |
+
np.savez_compressed(args.out_dir / filename, **arrays)
|
| 180 |
+
entries[index] = {'accession':accession, 'name':name, 'length':len(sequence),
|
| 181 |
+
'windows':windows, 'ambiguous_bases':int(ambiguous.sum()), 'file':filename}
|
| 182 |
+
print(f'Saved {filename}: {len(sequence):,} bases, {windows} windows', flush=True)
|
| 183 |
+
summary['records'] = [entries[index] for index in sorted(entries)]
|
| 184 |
(args.out_dir / 'summary.json').write_text(json.dumps(summary, indent=2)+'\n')
|
| 185 |
|
| 186 |
|