Repository navigation
What's Changed
- AIDO.Cell FlashAttention fallback by @Markkunitomi in #21
- Cell utils clarification by @cnellington in #24
- Major refactor by @cnellington in #26
Key Changes
Infrastructure & Developer Experience
- Pre-commit hooks: Ruff formatter, YAML validation, trailing whitespace checks
- Poetry migration: Replaced pip-compile with Poetry for reproducible dependency management
- Docker: Upgraded to CUDA 12.4.0 with matching PyTorch Geometric dependencies
- Documentation: New kwargs docstring inheritance system for API documentation
New Features
- Scimilarity model: Full integration with 28K+ gene mappings for cell/gene expression analysis
- RNA inverse folding: Zero-shot analysis pipeline with iterative denoising and ablation studies
- Protein stability: New stability prediction experiment configuration
- Flash-attn fallback: Allow AIDO.Cell to run on CPU
Code Quality
- Applied Ruff formatting (100 char line limit) across entire codebase
- Fixed trailing whitespace and EOF issues throughout
- Updated all README examples with proper data column specifications
- New embedding caching documentation guide
Refactoring
- Major backbone architecture updates (1,996 lines across key files)
- Two-phase initialization (
__init__+setup()) - Structured outputs via
SequenceBackboneOutputdataclass - Embedding caching infrastructure (10-100x speedup)
- Two-phase initialization (
- Data module improvements (1,522 lines)
- New
ClassificationDataModulebase class - Unified column handling with
rename_cols - Automatic class weighting for imbalanced datasets
- New
- Task system enhancements (1,133 lines)
@once_onlydecorator for reliability- Data-dependent loss configuration
- Stage-specific data requirements
Cleanup
- Removed obsolete AIDO.Cell Jupyter notebook
- Updated experiment configurations across all domains
Testing
- Added new backbone base tests (311 lines)
- Updated existing test suites for refactored code
- All tests passing
Breaking Changes
None for most users - changes are backward compatible via legacy adapter system.
Custom backbone/task/data module developers will need to implement new required methods.
Migration Notes for Contributors
- Run
pre-commit installto enable new commit hooks - Use
poetry lockinstead ofpip-compilefor dependency updates
For Developers
| Component | Must Implement | Why |
|---|---|---|
| Custom backbones | setup(), process_batch(), required_data_columns() |
New architecture |
| Custom tasks | Use @once_only, extract .last_hidden_state, implement required_data_columns(stage) |
Reliability + validation |
| Custom data modules | provided_columns() property |
Column validation |
Full Changelog: v0.1.2...v0.1.3
🔧 Refactored Components
1. Backbone Architecture Refactoring (1,996 lines)
Old vs New Pattern
BEFORE:
class MyBackbone(SequenceBackboneInterface):
def __init__(self, ...):
super().__init__()
self.model = AutoModel.from_pretrained(...) # Loaded immediately
def forward(self, input_ids, attention_mask):
return self.model(input_ids, attention_mask) # Returns TensorAFTER:
class MyBackbone(SequenceBackboneInterface):
def __init__(self, ...):
super().__init__()
self.model_name = model_name # Store config only
def setup(self):
self.model = AutoModel.from_pretrained(self.model_name) # Lazy loading
def process_batch(self, batch, device):
return self.tokenize(batch["sequences"]) # Unified interface
def forward(self, input_ids, attention_mask):
output = self.model(input_ids, attention_mask)
return SequenceBackboneOutput( # Structured output
last_hidden_state=output.last_hidden_state,
hidden_states=output.hidden_states,
attention_mask=attention_mask
)Key Changes
1. Two-Phase Initialization
__init__(): Store configuration onlysetup(): Load actual models/weights- Benefit: Faster instantiation, better memory management, distributed training support
2. Structured Output: SequenceBackboneOutput
@dataclass
class SequenceBackboneOutput:
last_hidden_state: Tensor
hidden_states: Optional[List[Tensor]] = None
attention_mask: Optional[Tensor] = None
special_tokens_mask: Optional[Tensor] = None
def concat(cls, outputs, padding_value=0): # Batch concatenation
def __getitem__(self, idx): # Slicing support
def to_device(self, device): # Device movement
def to_dict(self) / from_dict(cls, d): # Serialization- Benefit: Type safety, IDE autocomplete, consistent interface
3. Embedding Caching Infrastructure
# Enable caching for frozen backbones
backbone = aido_rna_650m(
cache_config={
"cache_dir": "/path/to/cache",
"storage_backend": "lmdb", # or "indexed"
"enable_profiling": True
}
)- Two backends: LMDB (memory-mapped) and Indexed (append-only)
- Built-in profiling:
_CacheProfilertracks hits/misses - Benefit: 10-100x speedup for frozen backbone experiments
4. Required Data Columns
def required_data_columns(self, stage: str) -> List[str]:
return ["sequences"] if stage != "predict" else []- Benefit: Framework validates data availability before training
Migration Impact
⚠️ Custom backbones must implement:setup(),process_batch(),required_data_columns()⚠️ All backbone fixtures updated to call.setup()
2. Data Module Refactoring (1,522 lines)
Architecture Change
BEFORE:
SequenceClassificationDataModule(
x_col="sequence",
extra_cols=["metadata", "organism"],
extra_col_aliases=["meta", "org"] # Parallel list - error-prone
)AFTER:
# Introduced base class for reusability
ClassificationDataModule(...) # Generic base
SequenceClassificationDataModule(...) # Inherits from base
# Cleaner API
SequenceClassificationDataModule(
x_col=["sequence", "metadata", "organism"], # Multiple inputs
rename_cols={"metadata": "meta", "organism": "org"}, # Explicit mapping
generate_uid=True # For caching support
)Key Improvements
1. Class Hierarchy
BaseDataModule
└── ClassificationDataModule (NEW)
└── SequenceClassificationDataModule
- Benefit: Easier to create non-sequence classification tasks
2. Unified Column Handling
x_col: Can be string or list (multi-input support)rename_cols: Dictionary mapping (clearer than parallel lists)- Benefit: Supports multi-modal inputs, less error-prone
3. New Features
provided_columns()property: Declares available columnsgenerate_uidparameter: Auto-generates unique IDs for cachingclass_weightproperty: Automatic class weighting for imbalanced datasets
dm = SequenceClassificationDataModule(..., generate_uid=True)
dm.class_weight # Tensor([0.5, 2.0, 1.0]) for weighted lossMigration Impact
⚠️ Replaceextra_cols+extra_col_aliaseswithrename_cols⚠️ Custom data modules must implementprovided_columns()- ✅ All README examples updated with new syntax
3. Task System Refactoring (1,133 lines)
Initialization Pattern Change
BEFORE:
class MLM(TaskInterface):
def __init__(self, backbone, ...):
self.backbone_fn = backbone # Store factory
self.loss = nn.CrossEntropyLoss() # Created in __init__
def configure_model(self):
self.backbone = self.backbone_fn(...) # Create here
# Could be called multiple times → bug!AFTER:
class MLM(TaskInterface):
def __init__(self, backbone, ...):
self.backbone = backbone(...) # Create immediately
# Loss moved to configure_model
@once_only # Ensures single execution
def configure_model(self):
self.backbone.setup() # Load weights
self.loss = nn.CrossEntropyLoss(
weight=self.data_module.class_weight # Data-dependent
)Key Changes
1. @once_only Decorator
- Prevents double-initialization in distributed training
- Benefit: Eliminates subtle bugs from multiple
configure_model()calls
2. Backbone Created in __init__
- Enables early introspection
- Actual model loading deferred to
setup() - Benefit: Configuration validation without loading weights
3. Unified Batch Processing
# BEFORE
def transform(self, batch, batch_idx):
tokenized = self.backbone.tokenize(batch["sequences"])
return {"input_ids": tokenized["input_ids"].to(self.device), ...}
# AFTER
def transform(self, batch, batch_idx):
return self.backbone.process_batch(batch, device=self.device)
# Handles tokenization + device movement- Benefit: Less boilerplate, consistent across tasks
4. Data-Dependent Loss Configuration
class SequenceClassification(TaskInterface):
def __init__(self, ..., weighted_loss=False):
self.weighted_loss = weighted_loss
@once_only
def configure_model(self):
self.loss = nn.CrossEntropyLoss(
weight=self.data_module.class_weight if self.weighted_loss else None
)- Benefit: Automatic class weighting for imbalanced datasets
5. Stage-Specific Data Requirements
def required_data_columns(self, stage: str) -> List[str]:
if stage == "predict":
return [] # No labels needed
return ["target_sequences"] # Labels required for train/val- Benefit: Framework validates before training starts
Migration Impact
⚠️ Custom tasks must updatetransform()to useprocess_batch()⚠️ forward()must extract.last_hidden_statefromSequenceBackboneOutput⚠️ Must implementrequired_data_columns(stage)⚠️ Move loss instantiation toconfigure_model()if data-dependent
4. Adapter System Changes (215 lines)
Changes
- Mostly code formatting (Ruff 100-char limit)
- MLPAdapterWithoutOutConcat: Memory optimization for pairwise tasks
- Import cleanup: Removed unused imports
- Type hints: Improved annotations
- Docstring updates: Better documentation
Migration Impact
- ✅ No breaking changes
- ✅ Maintains compatibility with refactored backbone/task interfaces
5. Documentation System Refactoring (776 lines)
Problem Being Solved
OLD SITUATION:
class Parent:
def __init__(self, arg1: int, arg2: str):
"""Parent class.
Args:
arg1: First argument
arg2: Second argument
"""
class Child(Parent):
def __init__(self, arg3: bool, **kwargs):
"""Child class.
Args:
arg3: Third argument
**kwargs: ??? (User has no idea what's valid)
"""
super().__init__(**kwargs)
# API docs show Child.__init__(arg3, **kwargs)
# User doesn't know arg1, arg2 are accepted!NEW SOLUTION:
class Child(Parent, metaclass=GoogleKwargsDocstringInheritanceInitMeta):
def __init__(self, arg3: bool, **kwargs):
"""Child class.
Args:
arg3: Third argument
"""
super().__init__(**kwargs)
# API docs now show: Child.__init__(arg3, arg1, arg2)
# Docstring automatically includes arg1, arg2 descriptions!How It Works
Two-Part System:
-
Runtime:
GoogleKwargsDocstringInheritanceInitMetametaclass- Inspects parent
__init__signatures - Merges parent parameters into child
- Updates
__signature__and__doc__ - Stores in
__griffe_signature__for docs
- Inspects parent
-
Build Time:
KwargsDocstringInheritanceGriffe extension- Reads
__griffe_signature__during doc generation - Updates MkDocs API reference
- Shows complete parameter list
- Reads
Benefits
- ✅ DRY: Update parent docs once, all children inherit
- ✅ IDE Support: Autocomplete shows all valid parameters
- ✅ User Experience: Complete API documentation automatically
- ✅ Maintainability: No duplicate docstrings
Used Throughout Codebase
- All
SequenceBackboneInterfacesubclasses - Data modules with
**kwargs - Task classes inheriting complex parameters
6. Test Infrastructure Changes (868 lines)
New Tests
File: tests/backbones/test_base.py (311 lines)
1. SequenceBackboneOutput Tests
test_sequence_backbone_output_getitem() # Indexing/slicing
test_sequence_backbone_output_concat() # Batch concatenation
test_sequence_backbone_output_to_device() # Device movement
test_sequence_backbone_output_with_none_fields() # Optional handling2. Caching Tests
test_backbone_cache_forward_first_call() # Cache miss
test_backbone_cache_forward_cached() # Cache hit
test_indexed_store_*() # Indexed backend
test_lmdb_store_*() # LMDB backend
test_cache_profiler_*() # Performance tracking3. Fixture Updates
# All fixtures updated to call setup()
@pytest.fixture
def genbiobert(genbiobert_cls):
backbone = genbiobert_cls(None, None)
backbone.setup() # NEW
return backboneBenefits
- ✅ Comprehensive coverage for new features
- ✅ Regression prevention
- ✅ Tests serve as usage examples
- ✅ CI/CD validation
📊 Breaking Changes & Migration Guide
For End Users
| Area | Old | New | Migration |
|---|---|---|---|
| Backbone usage | output = backbone(...) returns Tensor |
Returns SequenceBackboneOutput |
Extract .last_hidden_state |
| Backbone setup | Automatic | Must call .setup() |
Add backbone.setup() before use |
| Data modules | extra_cols + extra_col_aliases |
x_col list + rename_cols dict |
Update config files |
| Column names | Hardcoded "sequences" |
Configurable via rename_cols |
Use explicit mapping |
Example Migration
OLD CODE:
# Backbone
backbone = aido_rna_650m(None, None)
output = backbone(input_ids, attention_mask) # Returns Tensor
embeddings = output
# Data module
dm = SequenceClassificationDataModule(
x_col="sequence",
extra_cols=["metadata"],
extra_col_aliases=["meta"]
)NEW CODE:
# Backbone
backbone = aido_rna_650m(None, None)
backbone.setup() # Required!
output = backbone(input_ids, attention_mask) # Returns SequenceBackboneOutput
embeddings = output.last_hidden_state
# Data module
dm = SequenceClassificationDataModule(
x_col=["sequence", "metadata"],
rename_cols={"metadata": "meta"}
)