Skip to content

Commit a90e08d

Browse files
committed
Fix device handling
1 parent d96b1c6 commit a90e08d

6 files changed

Lines changed: 101 additions & 17 deletions

File tree

‎src/transcriptml/devices.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
import torch
4+
5+
6+
def resolve_device(device: str | torch.device = "cpu") -> torch.device:
7+
"""Resolve ``auto`` or explicit torch device names to a torch device."""
8+
9+
if isinstance(device, torch.device):
10+
return device
11+
name = str(device)
12+
if name == "auto":
13+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
14+
return torch.device(name)

‎src/transcriptml/interpret/codon_ism.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from transcriptml.data.encoding import infer_valid_lengths
1313
from transcriptml.data.schemas import SequenceSchema, get_schema
14+
from transcriptml.devices import resolve_device
1415
from transcriptml.interpret.predictor import Predictor
1516
from transcriptml.progress import ProgressReporter, log_progress
1617

@@ -844,7 +845,7 @@ def compute_codon_ism(
844845
raise ValueError("mutation_batch_size must be positive")
845846

846847
if isinstance(predictor, torch.nn.Module):
847-
predictor = Predictor(predictor, device=device, batch_size=reference_batch_size or 128)
848+
predictor = Predictor(predictor, device=resolve_device(device), batch_size=reference_batch_size or 128)
848849

849850
N, _, L = arr.shape
850851
analysis_indices = _resolve_analysis_indices(

‎src/transcriptml/interpret/predictor.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77
import torch
88

9+
from transcriptml.devices import resolve_device
910
from transcriptml.models.common import squeeze_prediction
1011
from transcriptml.models.registry import load_checkpoint
1112

@@ -30,7 +31,7 @@ def __init__(
3031
"""
3132

3233
self.model = model
33-
self.device = torch.device(device)
34+
self.device = resolve_device(device)
3435
self.batch_size = int(batch_size)
3536
if isinstance(model, torch.nn.Module):
3637
self.model.to(self.device)
@@ -53,8 +54,9 @@ def from_checkpoint(
5354
batch_size: Default batch size for prediction.
5455
"""
5556

56-
model, _ = load_checkpoint(checkpoint_path, map_location=device)
57-
return cls(model, device=device, batch_size=batch_size)
57+
resolved = resolve_device(device)
58+
model, _ = load_checkpoint(checkpoint_path, map_location=resolved)
59+
return cls(model, device=resolved, batch_size=batch_size)
5860

5961
@torch.no_grad()
6062
def predict(self, X: np.ndarray | torch.Tensor, *, batch_size: int | None = None) -> np.ndarray:

‎src/transcriptml/training/evaluation.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import torch
99

1010
from transcriptml.data.bundle import DatasetBundle, load_bundle
11+
from transcriptml.devices import resolve_device
1112
from transcriptml.models.common import squeeze_prediction
1213
from transcriptml.models.registry import load_checkpoint
1314
from transcriptml.training.metrics import mse, pearson_corr
@@ -33,6 +34,7 @@ def predict_array(
3334
progress: Whether to emit progress messages while predicting.
3435
"""
3536

37+
device = resolve_device(device)
3638
model = model.to(device)
3739
model.eval()
3840
preds: list[np.ndarray] = []
@@ -75,6 +77,7 @@ def _predict_indexed_array(
7577
progress_label: Label shown in progress messages.
7678
"""
7779

80+
device = resolve_device(device)
7881
model = model.to(device)
7982
model.eval()
8083
preds: list[np.ndarray] = []
@@ -115,6 +118,7 @@ def evaluate_model(
115118
progress: Whether to emit progress messages while evaluating.
116119
"""
117120

121+
device = resolve_device(device)
118122
idx = np.arange(bundle.X.shape[0]) if indices is None else np.asarray(indices, dtype=int)
119123
preds = _predict_indexed_array(
120124
model,
@@ -193,6 +197,7 @@ def evaluate_checkpoint(
193197
progress: Whether to emit progress messages while evaluating.
194198
"""
195199

200+
device = resolve_device(device)
196201
log_progress(f"evaluate: loading checkpoint {checkpoint_path}", enabled=progress)
197202
model, _ = load_checkpoint(checkpoint_path, map_location=device)
198203
log_progress(f"evaluate: loading dataset {dataset_path}", enabled=progress)

‎src/transcriptml/training/trainer.py‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from transcriptml.data.bundle import DatasetBundle, load_bundle
1515
from transcriptml.data.controls import apply_sequence_controls_to_bundle
16+
from transcriptml.devices import resolve_device
1617
from transcriptml.models.common import squeeze_prediction
1718
from transcriptml.models.registry import build_model, normalize_model_config, save_checkpoint
1819
from transcriptml.training.evaluation import evaluate_model, predict_to_csv
@@ -91,18 +92,6 @@ def _as_train_config(config: TrainConfig | Mapping[str, Any]) -> TrainConfig:
9192
return TrainConfig(**dict(config))
9293

9394

94-
def _select_device(name: str) -> torch.device:
95-
"""Resolve ``auto`` or explicit torch device names.
96-
97-
Args:
98-
name: ``"auto"`` or any string accepted by ``torch.device``.
99-
"""
100-
101-
if name == "auto":
102-
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
103-
return torch.device(name)
104-
105-
10695
def _make_config_splits(bundle: DatasetBundle, cfg: TrainConfig) -> dict[str, list[int]]:
10796
"""Choose dataset splits from the training config.
10897
@@ -404,7 +393,7 @@ def train_model(bundle: DatasetBundle, config: TrainConfig | Mapping[str, Any])
404393

405394
cfg = _as_train_config(config)
406395
_seed_everything(cfg.seed)
407-
device = _select_device(cfg.device)
396+
device = resolve_device(cfg.device)
408397
out = Path(cfg.output_dir)
409398
out.mkdir(parents=True, exist_ok=True)
410399
sequence_control_stats: dict[str, Any] | None = None

‎tests/test_device_auto.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import numpy as np
2+
3+
from transcriptml.data.bundle import DatasetBundle, save_bundle
4+
from transcriptml.devices import resolve_device
5+
from transcriptml.interpret.predictor import Predictor
6+
from transcriptml.models.registry import build_model, save_checkpoint
7+
from transcriptml.training.evaluation import evaluate_checkpoint
8+
9+
10+
def _small_model_config() -> dict[str, object]:
11+
return {
12+
"name": "small_cnn",
13+
"params": {
14+
"in_ch": 4,
15+
"n_filters": 4,
16+
"kernel_size": 3,
17+
"n_layers": 1,
18+
"dropout": 0.0,
19+
"head_hidden": 4,
20+
},
21+
}
22+
23+
24+
def _write_checkpoint_and_bundle(tmp_path):
25+
model_config = _small_model_config()
26+
model = build_model(model_config)
27+
checkpoint = tmp_path / "model.pt"
28+
save_checkpoint(checkpoint, model, model_config)
29+
30+
X = np.zeros((3, 4, 12), dtype=np.float32)
31+
X[:, 0, :] = 1.0
32+
bundle = DatasetBundle(
33+
X=X,
34+
y=np.array([0.0, 1.0, 2.0], dtype=np.float32),
35+
ids=["a", "b", "c"],
36+
schema="rna4",
37+
splits={"train": [0], "val": [1], "test": [2]},
38+
)
39+
dataset = tmp_path / "dataset"
40+
save_bundle(bundle, dataset)
41+
return checkpoint, dataset
42+
43+
44+
def test_resolve_device_auto_cpu_when_cuda_unavailable(monkeypatch):
45+
monkeypatch.setattr("torch.cuda.is_available", lambda: False)
46+
assert str(resolve_device("auto")) == "cpu"
47+
48+
49+
def test_evaluate_checkpoint_accepts_auto_device(tmp_path, monkeypatch):
50+
monkeypatch.setattr("torch.cuda.is_available", lambda: False)
51+
checkpoint, dataset = _write_checkpoint_and_bundle(tmp_path)
52+
53+
result = evaluate_checkpoint(
54+
checkpoint,
55+
dataset,
56+
tmp_path / "predictions.csv",
57+
split="test",
58+
device="auto",
59+
progress=False,
60+
)
61+
62+
assert len(result["predictions"]) == 1
63+
assert (tmp_path / "predictions.csv").exists()
64+
65+
66+
def test_predictor_from_checkpoint_accepts_auto_device(tmp_path, monkeypatch):
67+
monkeypatch.setattr("torch.cuda.is_available", lambda: False)
68+
checkpoint, _ = _write_checkpoint_and_bundle(tmp_path)
69+
70+
predictor = Predictor.from_checkpoint(checkpoint, device="auto", batch_size=2)
71+
preds = predictor.predict(np.zeros((2, 4, 12), dtype=np.float32))
72+
73+
assert preds.shape == (2,)

0 commit comments

Comments
 (0)