Skip to content

Add MaskedLMTask for protein/bio models (ESM-2, ProtBert) - #86

Draft
justinchuby wants to merge 4 commits into
mainfrom
justinchu/bio-models
Draft

Add MaskedLMTask for protein/bio models (ESM-2, ProtBert)#86
justinchuby wants to merge 4 commits into
mainfrom
justinchu/bio-models

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Summary

Adds MaskedLMTask — a new task type for masked language model inference. This unlocks the #1 protein model use case (masked amino acid prediction) for ESM-2, ProtBert, and all encoder-only models.

Changes

  • New MaskedLMTask in src/mobius/tasks/_masked_lm.py
  • BertForMaskedLM model class with MLM head (dense → GELU → LayerNorm → decoder)
  • _MaskedLMHead reusable prediction head component
  • Weight renaming for ESM/BERT/RoBERTa conventions (_rename_masked_lm_weight)
  • esm. prefix stripping fix for _rename_bert_weight
  • Registry: masked-lm task in TASK_REGISTRY
  • YAML golden test cases in testdata/cases/masked-lm/ (3 models)
  • Schema: masked-lm added to task_type enum
  • model_coverage_test.py updated with MASKED_LM_CONFIGS

Testing

  • 12 new build graph tests (bert, esm, roberta × 4 test methods)
  • 2231 passed, 29 skipped (full suite)
  • 361 passed (model coverage)
  • 189 passed (YAML schema validation)

Impact

Unlocks MLM inference for ESM-2 (1.44M+ monthly downloads), ProtBert, and any encoder-only model.

Add masked language modeling support for encoder-only models (ESM-2,
BERT, RoBERTa, etc.). This is the highest-ROI task for protein
language models like Meta's ESM-2 (3.3M+ downloads).

New components:
- MaskedLMTask: task defining masked-lm I/O contract
  (input_ids, attention_mask, token_type_ids -> logits)
- BertForMaskedLM: encoder + MLM head (dense -> GELU -> LayerNorm -> decoder)
- _MaskedLMHead: reusable MLM prediction head component
- _rename_masked_lm_weight: weight renaming for ESM/BERT/RoBERTa conventions

Testing:
- MASKED_LM_CONFIGS in _test_configs.py (bert, esm, roberta)
- TestBuildMaskedLMGraph with 4 test methods x 3 model types = 12 tests
- YAML golden test cases in testdata/cases/masked-lm/ (3 models)
- model_coverage_test.py updated with MASKED_LM_CONFIGS
- 'masked-lm' added to schema.json task_type enum

Documentation:
- .github/skills/adding-new-task-types/SKILL.md: comprehensive guide for
  adding new task types, using MaskedLMTask as worked example

Also:
- Add esm. prefix stripping to _rename_bert_weight (was missing)
- Register masked-lm task in TASK_REGISTRY
- Export BertForMaskedLM from models package

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 8124071cf0d304

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 61 61 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 66 66 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 53 53 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 61 61 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 360 KB 360 KB +0.0%
mamba (ssm-text-generation) num_nodes 105 105 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 61 61 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 58 58 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 61 61 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 409 409 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 174 174 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 140 140 +0.0%

No performance regressions.

Comment thread tests/build_graph_test.py Fixed
@github-actions

github-actions Bot commented Apr 2, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 8124071cf0d304

Model Sub-model Changes Status
bert (feature-extraction) model 0
falcon model 0
gemma2 model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new masked language modeling (MLM) inference path to mobius by introducing a dedicated MaskedLMTask, an encoder+MLM-head module (BertForMaskedLM), and corresponding tests / golden cases so encoder-only models (BERT/RoBERTa/ESM) can produce per-token vocabulary logits.

Changes:

  • Introduces MaskedLMTask and registers it as task_type: masked-lm (including YAML schema + new golden cases).
  • Adds BertForMaskedLM and _MaskedLMHead, plus MLM-specific HF→mobius weight renaming (including esm. prefix handling).
  • Extends test config/coverage lists and adds graph-build tests for masked-lm graphs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/model_coverage_test.py Includes MASKED_LM_CONFIGS in coverage computation.
tests/build_graph_test.py Adds parametrized build/checker tests for masked-lm graphs.
tests/_test_configs.py Adds MASKED_LM_CONFIGS and includes them in ALL_CONFIGS.
testdata/cases/schema.json Extends task_type enum with "masked-lm".
testdata/cases/masked-lm/bert-base-uncased.yaml Adds masked-lm golden case for BERT.
testdata/cases/masked-lm/esm2-8m.yaml Adds masked-lm golden case for ESM-2.
testdata/cases/masked-lm/roberta-base.yaml Adds masked-lm golden case for RoBERTa.
src/mobius/tasks/_masked_lm.py New task wiring encoder-only MLM modules to logits output.
src/mobius/tasks/init.py Exports MaskedLMTask and registers "masked-lm" in TASK_REGISTRY.
src/mobius/models/bert.py Adds BertForMaskedLM, _MaskedLMHead, and MLM weight renaming; extends _rename_bert_weight to strip esm..
src/mobius/models/init.py Exports BertForMaskedLM.

Comment thread tests/build_graph_test.py Outdated
Comment on lines +399 to +402
_MASKED_LM_MODEL_CONFIGS: list[tuple[str, dict]] = [
(mt, ov) for mt, ov, _ in MASKED_LM_CONFIGS
]

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_MASKED_LM_MODEL_CONFIGS is defined but never used. This will likely trigger an unused-variable lint failure (e.g., Ruff F841). Remove it or use it in the parametrization (e.g., in place of re-deriving params).

Suggested change
_MASKED_LM_MODEL_CONFIGS: list[tuple[str, dict]] = [
(mt, ov) for mt, ov, _ in MASKED_LM_CONFIGS
]

Copilot uses AI. Check for mistakes.
Comment thread src/mobius/models/bert.py Outdated
Comment on lines +341 to +342
# Top-level bias on the BERT predictions head
"cls.predictions.bias": "lm_head.bias",

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MLM weight renaming maps cls.predictions.bias to lm_head.bias, and _rename_masked_lm_weight also passes through lm_head.bias unchanged. But _MaskedLMHead only defines lm_head.decoder.bias (via Linear(..., bias=True)) and has no lm_head.bias parameter, so this bias will be skipped during weight application and logits will be wrong. Map HF bias keys (cls.predictions.bias and/or lm_head.bias) to the actual parameter name used by the module (likely lm_head.decoder.bias), or add an explicit lm_head.bias parameter and ensure it’s used/tied correctly.

Suggested change
# Top-level bias on the BERT predictions head
"cls.predictions.bias": "lm_head.bias",
# Top-level bias on the BERT predictions head maps to decoder bias
"cls.predictions.bias": "lm_head.decoder.bias",

Copilot uses AI. Check for mistakes.
Inputs:
- input_ids: [batch, sequence_len] INT64
- attention_mask: [batch, sequence_len] INT64
- token_type_ids: [batch, sequence_len] INT64 (optional, for BERT)

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says token_type_ids is optional, but build() always creates it as a required graph input and always passes it to the module. Either update the docstring to reflect that token_type_ids is always present (and should be zeros / type_vocab_size=1 for models that don’t use it), or implement an actually-optional input contract.

Suggested change
- token_type_ids: [batch, sequence_len] INT64 (optional, for BERT)
- token_type_ids: [batch, sequence_len] INT64
Always provided as an input. Models that do not use token types
should configure ``type_vocab_size=1`` and/or ignore this tensor
(e.g., pass or store all zeros).

Copilot uses AI. Check for mistakes.
Comment thread src/mobius/models/bert.py
Comment on lines 295 to +309
def _rename_bert_weight(name: str) -> str | None:
"""Rename a single HF BERT weight to our convention.

Strips model prefixes (bert./roberta.), collapses nested HF naming
Strips model prefixes (bert./roberta./esm.), collapses nested HF naming
(.self./.output.) to match the flat ONNX initializer paths, and
handles old-BERT gamma/beta compat. Returns None for pooler/cls
weights we don't need.
"""
# Strip "bert." or "roberta." prefix if present
# Strip "bert." / "roberta." / "esm." prefix if present
if name.startswith("bert."):
name = name[5:]
elif name.startswith("roberta."):
name = name[8:]
elif name.startswith("esm."):
name = name[4:]

Copilot AI Apr 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New weight-renaming logic (_rename_masked_lm_weight and the updated _rename_bert_weight handling esm.) isn’t covered by any targeted unit tests, and silent mis-renames will only surface as runtime warnings/incorrect outputs when weights are applied. Add a small _test.py (similar to src/mobius/models/t5_test.py) that asserts key HF→Mobius name mappings for BERT/ESM/RoBERTa MLM heads and encoder weights.

Copilot generated this review using guidance from repository custom instructions.
justinchuby added a commit that referenced this pull request Apr 2, 2026
Bug fixes for PR #86 review:

1. (HIGH) Register BertForMaskedLM in the registry as bert_masked_lm,
   esm_masked_lm, roberta_masked_lm with task='masked-lm' and
   test_model_ids. Tests now use registry.get() instead of hardcoding.

2. (MEDIUM) Fix cls.predictions.bias mapping from 'lm_head.bias' to
   'lm_head.decoder.bias' — the HF BERT output bias is shared with
   the decoder layer bias.

3. (LOW) Extract _rename_bert_encoder_weight() helper to eliminate
   duplicated encoder rename logic between _rename_bert_weight and
   _rename_masked_lm_weight.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
Bug fixes for PR #86 review:

1. (HIGH) Register BertForMaskedLM in the registry as bert_masked_lm,
   esm_masked_lm, roberta_masked_lm with task='masked-lm' and
   test_model_ids. Tests now use registry.get() instead of hardcoding.

2. (MEDIUM) Fix cls.predictions.bias mapping from 'lm_head.bias' to
   'lm_head.decoder.bias' — the HF BERT output bias is shared with
   the decoder layer bias.

3. (LOW) Extract _rename_bert_encoder_weight() helper to eliminate
   duplicated encoder rename logic between _rename_bert_weight and
   _rename_masked_lm_weight.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the justinchu/bio-models branch from 99de4a0 to 755b34d Compare April 2, 2026 01:27
justinchuby and others added 2 commits April 1, 2026 18:31
- Add cls.* skip in _rename_masked_lm_weight for NSP head weights
- Pin ESM-2 YAML revision to SHA c731040f (was 'main')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
Add _generate_masked_lm() to scripts/generate_golden.py and
load_torch_masked_lm_model()/torch_masked_lm_forward() to
torch_reference.py.

The generator uses AutoModelForMaskedLM and extracts logits at
the first mask token position (falls back to position 1 if no
mask token is found).

Generate golden JSON files for all 4 cases lacking them:
- testdata/golden/encoder/esm2-8m.json (feature-extraction)
- testdata/golden/masked-lm/bert-base-uncased.json
- testdata/golden/masked-lm/esm2-8m.json
- testdata/golden/masked-lm/roberta-base.json

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
@justinchuby justinchuby added the ai Created by an AI agent label Apr 2, 2026
@justinchuby justinchuby self-assigned this Apr 2, 2026
@justinchuby
justinchuby marked this pull request as draft April 2, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Created by an AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants