Skip to content

feat(recipes): add evo2_megatron_quant — INT8 weight compression + ModelOpt PTQ harness#1682

Draft
huthvincent wants to merge 3 commits into
NVIDIA-BioNeMo:mainfrom
huthvincent:feat/evo2-quant-int8-recipe
Draft

feat(recipes): add evo2_megatron_quant — INT8 weight compression + ModelOpt PTQ harness#1682
huthvincent wants to merge 3 commits into
NVIDIA-BioNeMo:mainfrom
huthvincent:feat/evo2-quant-int8-recipe

Conversation

@huthvincent

@huthvincent huthvincent commented Jul 12, 2026

Copy link
Copy Markdown

Description

Adds a new inference-side recipe, recipes/evo2_megatron_quant/, for BioNeMo biological foundation models (ESM-2, Geneformer, Evo2). Upstream already covers low-precision training (TE FP8/MXFP8/NVFP4); this adds an inference path:

  • Real INT8 weight compression — a drop-in nn.Linear replacement (INT8Linear, per-channel symmetric) that stores weights in int8 and dequantizes on the fly. Measured on Evo2 7B: ~15% GPU-memory reduction, cosine sim ≈ 0.998, top-1 100% vs BF16.
  • ModelOpt PTQ harness — bridges BioNeMo model loading to mtq.quantize() with an automated quality report (cosine sim / top-k agreement / MSE).
  • CPU-only L0 sanity test (tests/test_compressed_linear_roundtrip.py) — no GPU or checkpoint required, so it runs in fast CI.

This is a Draft opened to get maintainer direction on the questions in #1681:

  1. Do you want an inference PTQ / weight-compression recipe in this repo, or should low-precision inference go through NeMo + TensorRT-LLM?
  2. Is recipes/evo2_megatron_quant/ the right home, or should this fold into recipes/evo2_megatron/?
  3. I intentionally scoped this first PR to the parts that hold up (INT8 + PTQ harness). FP8/INT4 real weight compression and KV-cache experiments are held back for a follow-up once their GPU quality is re-measured.

Note: the INT8 quantization math is verified numerically and the CPU L0 test is ready, but I have not yet executed the pytest in a torch environment — it should run under CI once authorized. Happy to align file layout (e.g. flatten src/), pin requirements.txt to the target container, and add a Dockerfile/hydra config to match your recipe conventions.

Follows up on #1681.

Usage

from src.compress_model import compress_model

stats = compress_model(evo2_model, precision="int8")  # swaps Linear -> INT8Linear
# -> ~15% GPU memory reduction on Evo2 7B, cosine sim ~0.998 vs BF16

Type of changes

  • New feature (non-breaking change which adds functionality)

CI Pipeline Configuration

Only the CPU-only L0 sanity test is intended for the default PR pipeline. The GPU benchmark and tests/test_single_model.py need NGC checkpoints and are meant to be run manually.

Pre-submit Checklist

  • I have tested these changes locally (INT8 math verified numerically; pytest pending a torch/CI run)
  • I have updated the documentation accordingly (recipe README)
  • I have added/updated tests as needed (CPU-only L0 sanity test)
  • All existing tests pass successfully (pending CI /ok to test)

Summary by CodeRabbit

  • New Features

    • Added an INT8 weight-compression quantization recipe for Evo2, ESM-2, and Geneformer using ModelOpt methods.
    • Introduced a benchmarking workflow that measures latency, GPU memory savings, and logit quality, and exports results to CSV.
    • Added container startup and pretrained model download scripts.
    • Added quantization configuration definitions covering multiple precision families (FP8/INT8/INT4/NVFP4/microscaling/mixed).
  • Documentation

    • Added recipe documentation with setup, supported checkpoints, and quick-start/benchmark guidance.
  • Tests

    • Added CPU-only INT8 linear roundtrip tests and a single-model quantization validation script.

…lOpt PTQ harness

Adds an inference-side recipe: real INT8 per-channel weight compression
(INT8Linear, ~15% GPU-mem saving on Evo2 7B, cosine sim ~0.998) plus a
ModelOpt mtq.quantize PTQ analysis harness for ESM-2/Geneformer/Evo2.
Includes a CPU-only L0 sanity test (no GPU/checkpoint needed).

Follows up on NVIDIA-BioNeMo#1681.

Signed-off-by: Rui Zhu <huthruiz@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9159d72b-bf79-4a3f-9e70-fd855ed471c6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a BioNeMo quantization recipe with model adapters, ModelOpt PTQ support, real INT8 linear compression, evaluation metrics, benchmarking, model download utilities, and container setup for ESM-2, Geneformer, and Evo2.

Changes

BioNeMo quantization recipe

Layer / File(s) Summary
Model adapters and quantization catalog
recipes/evo2_megatron_quant/src/__init__.py, recipes/evo2_megatron_quant/src/adapters.py, recipes/evo2_megatron_quant/configs/quant_methods.yaml
Defines model-specific loading, synthetic inputs, forward arguments, adapter discovery, and metadata for FP8, INT8, INT4, NVFP4, MX, and mixed-precision methods.
Real INT8 linear compression
recipes/evo2_megatron_quant/src/compressed_linear.py, recipes/evo2_megatron_quant/src/compress_model.py, recipes/evo2_megatron_quant/tests/test_compressed_linear_roundtrip.py
Stores linear weights as INT8 values with per-row scales, replaces eligible model linear layers, and validates reconstruction and storage behavior.
ModelOpt PTQ integration
recipes/evo2_megatron_quant/src/quantize.py
Resolves ModelOpt configurations, builds adapter-based BF16 calibration loops, and applies in-place quantization.
Quality evaluation and benchmarking
recipes/evo2_megatron_quant/src/metrics.py, recipes/evo2_megatron_quant/tests/test_single_model.py, recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py
Adds logits comparison metrics, single-model quantization tests, subprocess benchmarking, CSV output, and optional maximum-sequence-length searches.
Recipe setup and operational entrypoints
recipes/evo2_megatron_quant/.ruff.toml, recipes/evo2_megatron_quant/README.md, recipes/evo2_megatron_quant/requirements.txt, recipes/evo2_megatron_quant/docker/start_container.sh, recipes/evo2_megatron_quant/scripts/download_models.sh
Adds inherited Ruff settings, dependencies, usage documentation, container startup, and NGC model download commands.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as test_single_model
  participant Adapter as ModelAdapter
  participant Model as BioNeMo model
  participant Quantizer as quantize_model
  participant Metrics as compute_metrics
  CLI->>Adapter: load model and generate test data
  Adapter->>Model: run BF16 inference
  CLI->>Quantizer: quantize a fresh model
  Quantizer->>Model: apply ModelOpt configuration
  CLI->>Model: run quantized inference
  CLI->>Metrics: compare baseline and quantized logits
  Metrics-->>CLI: return quality metrics
Loading

Possibly related issues

Suggested reviewers: jwilber

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a new evo2_megatron_quant inference recipe with INT8 compression and a ModelOpt PTQ harness.
Description check ✅ Passed The description covers the required sections, usage, change type, CI intent, and checklist, with only minor CI-label detail omitted.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@huthvincent

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 16

🧹 Nitpick comments (3)
recipes/evo2_megatron_quant/src/adapters.py (2)

260-276: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate run_forward/build_calibration_forward_args between ESM2Adapter and GeneformerAdapter.

Both classes have byte-for-byte identical run_forward (Lines 270-273, 348-351) and build_calibration_forward_args (Lines 275-276, 353-354) bodies. Consider hoisting default implementations into ModelAdapter (only Evo2Adapter needs to override them for position_ids), removing the duplication.

♻️ Proposed refactor sketch
 class ModelAdapter(ABC):
     ...
+    def run_forward(self, model, input_ids, attention_mask):
+        """Default forward pass shared by transformer-style adapters."""
+        with torch.no_grad(), torch.cuda.amp.autocast(dtype=torch.bfloat16):
+            output = model(input_ids=input_ids, attention_mask=attention_mask)
+        return self.extract_logits(output)
+
+    def build_calibration_forward_args(self, input_ids, attention_mask):
+        return {"input_ids": input_ids, "attention_mask": attention_mask}

Then remove the now-redundant overrides from ESM2Adapter and GeneformerAdapter, keeping Evo2Adapter's overrides (it needs position_ids).

Also applies to: 343-354

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/adapters.py` around lines 260 - 276, Hoist
the shared run_forward and build_calibration_forward_args implementations into
ModelAdapter, preserving their current behavior and signatures. Remove the
redundant overrides from ESM2Adapter and GeneformerAdapter, while retaining
Evo2Adapter’s overrides because it requires position_ids.

271-271: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace deprecated torch.cuda.amp.autocast calls

PyTorch 2.4 deprecates torch.cuda.amp.autocast; switch these four sites in recipes/evo2_megatron_quant/src/adapters.py and recipes/evo2_megatron_quant/src/quantize.py to torch.amp.autocast("cuda", dtype=torch.bfloat16) in one pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/adapters.py` at line 271, Replace all four
deprecated torch.cuda.amp.autocast usages in the relevant adapter and
quantization code with torch.amp.autocast("cuda", dtype=torch.bfloat16),
preserving the surrounding torch.no_grad contexts and autocast behavior.
recipes/evo2_megatron_quant/src/compressed_linear.py (1)

86-93: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Align the INT8 aliases with the public method list

INT8_KV_CFG and MX_INT8_CFG only exist in METHOD_TO_PRECISION; they’re not listed in quant_methods.yaml or ALL_QUANT_METHODS. If they’re intentional aliases, add them to the catalog/docs; otherwise drop them or switch to the canonical MXINT8_DEFAULT_CFG name to keep the supported set consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/compressed_linear.py` around lines 86 - 93,
Update METHOD_TO_PRECISION so its INT8 aliases match the supported methods in
quant_methods.yaml and ALL_QUANT_METHODS: remove INT8_KV_CFG and MX_INT8_CFG
unless they are formally added to those catalogs, and use the canonical
MXINT8_DEFAULT_CFG name where applicable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@recipes/evo2_megatron_quant/docker/start_container.sh`:
- Line 22: Update the LOCAL_WORKSPACE default in start_container.sh to mount the
current recipe root rather than appending /quantization to the working
directory. Derive it from the script’s parent directory or use the current
directory when the script is run from the recipe root, while preserving any
explicitly provided LOCAL_WORKSPACE value.
- Line 44: Update the start_container.sh Docker launch options so --net=host is
applied only when an explicit networking flag is enabled, while preserving host
networking for NCCL/distributed runs that require it. Keep the default
invocation on isolated container networking.

In `@recipes/evo2_megatron_quant/README.md`:
- Line 44: Update the fenced layout block in the README to specify the text
language identifier, preserving its existing tree-style contents so markdownlint
MD040 passes.

In `@recipes/evo2_megatron_quant/requirements.txt`:
- Around line 9-14: Pin the dependencies in
recipes/evo2_megatron_quant/requirements.txt, specifically
nvidia-modelopt[torch], pyyaml, and pytest, to exact versions matching the
target BioNeMo image; alternatively, add and reference a matching constraints
file so installations remain reproducible.

In `@recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py`:
- Around line 167-203: Replace the fixed candidate sweep in the
max_seqlen_search block with a bounded binary search that records the highest
successful length and the first failing upper bound, then probes between them
until the maximum supported length is identified. Preserve the existing result
fields and peak-memory reporting, or rename the option/output consistently if
retaining a candidate sweep instead.
- Around line 263-264: Update the argparse definition for --max-seqlen so the
expensive max sequence-length search can be explicitly disabled, using a paired
opt-out flag or equivalent boolean configuration while preserving the current
enabled-by-default behavior and the existing args.max_seqlen consumer.
- Around line 118-120: Update the sequence-probe flow around the warmup call and
its try/finally cleanup: initialize ids, pos, and mask before entering try so
cleanup remains safe when allocation fails, explicitly delete the warmup output
and each probe output instead of retaining them in _, and perform all
probe-variable cleanup in finally before empty_cache() to preserve the original
allocation error and release memory.
- Around line 217-254: Update run_test() to delete any existing output_file
before launching the worker, preventing stale JSON from being reused when the
subprocess fails. Wrap subprocess.run() to catch subprocess.TimeoutExpired and
return the existing CRASH-style fallback result with an appropriate timeout
error and elapsed wall time, while preserving normal result loading and failure
handling.

In `@recipes/evo2_megatron_quant/scripts/download_models.sh`:
- Around line 54-57: Update the unknown-model branch in the model validation
loop to fail with a nonzero status instead of continuing successfully. Preserve
the existing warning, and ensure an invalid requested model prevents the script
from reporting success or proceeding with downloads.

In `@recipes/evo2_megatron_quant/src/__init__.py`:
- Around line 16-20: Update the package docstring in __init__.py to remove
AMPLIFY from the supported-model list, matching the models registered by
ADAPTER_REGISTRY and the adapters.py documentation; do not add adapter support.

In `@recipes/evo2_megatron_quant/src/compress_model.py`:
- Line 31: Update the import in compress_model.py to use a package-relative
import from the sibling compressed_linear module, while preserving the existing
METHOD_TO_PRECISION and compress_linear names.

In `@recipes/evo2_megatron_quant/src/quantize.py`:
- Around line 179-186: Update forward_loop to log each exception raised by
model(**kwargs), including useful error details and batch context, instead of
silently passing. Preserve the existing behavior of continuing calibration after
individual batch failures.
- Around line 227-234: Update the result construction after mtq.quantize in the
quantization flow so num_quantizers is not derived from the returned model via
the isinstance check. Remove the num_quantizers field unless an existing,
authoritative quantizer-count source is available, in which case populate it
from that source.

In `@recipes/evo2_megatron_quant/tests/test_compressed_linear_roundtrip.py`:
- Around line 48-52: Update the cosine-similarity test around
INT8Linear.from_linear so both forward paths use equivalent bias behavior:
either enable the quantized bias by removing return_bias=False or construct a
bias-free reference matching the quantized layer. Keep y_ref, y_q, and the
cosine comparison otherwise unchanged.

In `@recipes/evo2_megatron_quant/tests/test_single_model.py`:
- Around line 211-221: Update main() to return a nonzero status when the
quantization results contain any FAIL or ERROR outcomes, while retaining zero
for successful runs. Propagate that return value through the __main__ entry
point using the process exit mechanism so CI observes failures.
- Around line 53-56: Rename the helper function test_single_method to
run_single_method so pytest does not collect it, and update its call site in the
same module to use the new name.

---

Nitpick comments:
In `@recipes/evo2_megatron_quant/src/adapters.py`:
- Around line 260-276: Hoist the shared run_forward and
build_calibration_forward_args implementations into ModelAdapter, preserving
their current behavior and signatures. Remove the redundant overrides from
ESM2Adapter and GeneformerAdapter, while retaining Evo2Adapter’s overrides
because it requires position_ids.
- Line 271: Replace all four deprecated torch.cuda.amp.autocast usages in the
relevant adapter and quantization code with torch.amp.autocast("cuda",
dtype=torch.bfloat16), preserving the surrounding torch.no_grad contexts and
autocast behavior.

In `@recipes/evo2_megatron_quant/src/compressed_linear.py`:
- Around line 86-93: Update METHOD_TO_PRECISION so its INT8 aliases match the
supported methods in quant_methods.yaml and ALL_QUANT_METHODS: remove
INT8_KV_CFG and MX_INT8_CFG unless they are formally added to those catalogs,
and use the canonical MXINT8_DEFAULT_CFG name where applicable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 145379d8-da58-44a0-8994-e3656515d582

📥 Commits

Reviewing files that changed from the base of the PR and between b35c255 and f3bf8f5.

📒 Files selected for processing (15)
  • recipes/evo2_megatron_quant/.ruff.toml
  • recipes/evo2_megatron_quant/README.md
  • recipes/evo2_megatron_quant/configs/quant_methods.yaml
  • recipes/evo2_megatron_quant/docker/start_container.sh
  • recipes/evo2_megatron_quant/requirements.txt
  • recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py
  • recipes/evo2_megatron_quant/scripts/download_models.sh
  • recipes/evo2_megatron_quant/src/__init__.py
  • recipes/evo2_megatron_quant/src/adapters.py
  • recipes/evo2_megatron_quant/src/compress_model.py
  • recipes/evo2_megatron_quant/src/compressed_linear.py
  • recipes/evo2_megatron_quant/src/metrics.py
  • recipes/evo2_megatron_quant/src/quantize.py
  • recipes/evo2_megatron_quant/tests/test_compressed_linear_roundtrip.py
  • recipes/evo2_megatron_quant/tests/test_single_model.py

Comment thread recipes/evo2_megatron_quant/docker/start_container.sh Outdated
Comment thread recipes/evo2_megatron_quant/docker/start_container.sh Outdated
Comment thread recipes/evo2_megatron_quant/README.md Outdated
Comment on lines +9 to +14
nvidia-modelopt[torch] # ModelOpt PTQ bridge used by src/quantize.py
pyyaml # reads configs/quant_methods.yaml

# Test-only (the L0 sanity test tests/test_compressed_linear_roundtrip.py is
# CPU-only and needs just torch + pytest):
pytest

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant area first.
git ls-files recipes/evo2_megatron_quant

echo
echo "== requirements.txt =="
cat -n recipes/evo2_megatron_quant/requirements.txt

echo
echo "== nearby files =="
git ls-files recipes/evo2_megatron_quant | sed 's#^`#-` #'

echo
echo "== search for constraints / pins / matching version files =="
rg -n --hidden --glob '!**/.git/**' \
  -e 'constraints' \
  -e 'requirement' \
  -e 'nvidia-modelopt\[torch\]' \
  -e '^pytest([<=>~!].*)?$' \
  -e '^pyyaml([<=>~!].*)?$' \
  recipes . || true

Repository: NVIDIA-BioNeMo/bionemo-recipes

Length of output: 27633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== recipes/evo2_megatron_quant/README.md (install guidance) =="
sed -n '1,140p' recipes/evo2_megatron_quant/README.md

echo
echo "== recipes/evo2_megatron_quant/docker/start_container.sh =="
cat -n recipes/evo2_megatron_quant/docker/start_container.sh

echo
echo "== recipes/README.md (requirements guidance) =="
sed -n '90,115p' recipes/README.md

echo
echo "== .github/workflows/unit-tests-recipes.yml install snippet =="
sed -n '160,185p' .github/workflows/unit-tests-recipes.yml

Repository: NVIDIA-BioNeMo/bionemo-recipes

Length of output: 6588


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== exact paths for recipe guidance files =="
git ls-files | rg '(^|/)recipes/README\.md$|(^|/)evo2_megatron_quant/requirements\.txt$|(^|/)requirements\.txt$' || true

echo
echo "== top-level recipe guidance =="
sed -n '95,115p' bionemo-recipes/recipes/README.md

echo
echo "== recipe install paths in CI =="
sed -n '165,182p' .github/workflows/unit-tests-recipes.yml

echo
echo "== comparable recipe requirements pins =="
for f in \
  bionemo-recipes/recipes/esm2_native_te/requirements.txt \
  bionemo-recipes/recipes/codonfm_ptl_te/requirements.txt \
  bionemo-recipes/recipes/evo2_megatron/requirements.txt
do
  echo "--- $f ---"
  sed -n '1,40p' "$f"
  echo
done

Repository: NVIDIA-BioNeMo/bionemo-recipes

Length of output: 3667


Pin the recipe dependencies recipes/evo2_megatron_quant/requirements.txt still leaves nvidia-modelopt[torch], pyyaml, and pytest unconstrained, so pip install -r requirements.txt can drift from the target BioNeMo image and hurt reproducibility. Pin exact versions here or add a matching constraints file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/requirements.txt` around lines 9 - 14, Pin the
dependencies in recipes/evo2_megatron_quant/requirements.txt, specifically
nvidia-modelopt[torch], pyyaml, and pytest, to exact versions matching the
target BioNeMo image; alternatively, add and reference a matching constraints
file so installations remain reproducible.

Comment thread recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py Outdated
Comment thread recipes/evo2_megatron_quant/src/quantize.py Outdated
Comment thread recipes/evo2_megatron_quant/src/quantize.py
Comment on lines +48 to +52
q = INT8Linear.from_linear(lin, return_bias=False)
x = torch.randn(4, in_f)
y_ref = lin(x)
y_q = q(x)
cos = torch.cosine_similarity(y_ref.flatten(), y_q.flatten(), dim=0).item()

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare equivalent forward paths in the cosine test.

return_bias=False removes the quantized bias, while lin(x) includes it. The test therefore compares different computations; keep the bias enabled or make the reference bias-free.

Proposed fix
-    q = INT8Linear.from_linear(lin, return_bias=False)
+    q = INT8Linear.from_linear(lin)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
q = INT8Linear.from_linear(lin, return_bias=False)
x = torch.randn(4, in_f)
y_ref = lin(x)
y_q = q(x)
cos = torch.cosine_similarity(y_ref.flatten(), y_q.flatten(), dim=0).item()
q = INT8Linear.from_linear(lin)
x = torch.randn(4, in_f)
y_ref = lin(x)
y_q = q(x)
cos = torch.cosine_similarity(y_ref.flatten(), y_q.flatten(), dim=0).item()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/tests/test_compressed_linear_roundtrip.py` around
lines 48 - 52, Update the cosine-similarity test around INT8Linear.from_linear
so both forward paths use equivalent bias behavior: either enable the quantized
bias by removing return_bias=False or construct a bias-free reference matching
the quantized layer. Keep y_ref, y_q, and the cosine comparison otherwise
unchanged.

Comment thread recipes/evo2_megatron_quant/tests/test_single_model.py Outdated
Comment thread recipes/evo2_megatron_quant/tests/test_single_model.py Outdated
- test_single_model: rename helper so pytest doesn't collect it; propagate
  FAIL/ERROR via exit code; default to INT8 methods
- compress_model: package-relative import with top-level fallback
- quantize: surface (not swallow) calibration forward failures; drop the
  always-zero num_quantizers field
- benchmark_real_quant: exception-safe seq-len sweep, free warmup output,
  make --max-seqlen disableable, drop stale result file, handle timeout,
  stop calling the linear sweep a binary search
- start_container: mount the recipe root (not empty ./quantization), make
  --net=host opt-in via USE_HOST_NET
- download_models: fail fast (nonzero) on an unknown model name
- __init__: drop AMPLIFY from the supported-models docstring
- README: language-tag the layout fence (MD040); requirements: pin pyyaml/pytest

Signed-off-by: Rui Zhu <huthruiz@gmail.com>
@huthvincent

Copy link
Copy Markdown
Author

Thanks for the thorough review! Addressed the findings in 46eb3bde:

Fixed

  • tests/test_single_model.py — renamed test_single_methodrun_single_method so pytest won't collect it as a test; main() now returns a non-zero exit code when any method FAILs/ERRORs.
  • src/compress_model.py — switched to a package-relative import (from .compressed_linear import ...) with a top-level fallback for the sys.path-insert case.
  • src/quantize.py — calibration forward-pass failures are now logged (with a warning if all passes fail) instead of being silently swallowed; dropped the always-zero num_quantizers field.
  • scripts/benchmark_real_quant.py — exception-safe sequence-length sweep (vars initialized before try, cleaned up in finally), free the warmup output, --max-seqlen is now disableable (BooleanOptionalAction--no-max-seqlen), stale result file removed before each worker run, TimeoutExpired handled, and stopped labeling the linear sweep a "binary search".
  • docker/start_container.sh — default mount is now the recipe root (was an empty ./quantization subdir); --net=host is opt-in via USE_HOST_NET=1.
  • scripts/download_models.sh — unknown model names now fail fast with a non-zero exit.
  • src/__init__.py — dropped AMPLIFY from the supported-models docstring (only ESM-2/Geneformer/Evo2 are registered).
  • README.md — language-tagged the layout fence (MD040); requirements.txt — pinned pyyaml/pytest (torch and modelopt come from the BioNeMo base image, so they're intentionally not re-pinned).

One I believe is a false positive

  • tests/test_compressed_linear_roundtrip.py (cosine test): return_bias=False does not drop the bias from the computation — INT8Linear.forward always applies it via F.linear(x, w, self.bias); return_bias only controls whether forward returns (out, None) vs a bare tensor. So the test compares equivalent paths. Happy to add an explicit bias-enabled case as well if you'd prefer.

@huthvincent

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
recipes/evo2_megatron_quant/src/quantize.py (2)

135-142: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle ModelOpt’s list-based quant_cfg here.
ModelOpt 0.45+ stores quant_cfg as an ordered list of entries, so if "default" in quant_cfg never matches and enable_all_mlp becomes a no-op. Remove the wildcard disable entry (quantizer_name: "*") or add an explicit MLP override instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/quantize.py` around lines 135 - 142, Update
the enable_all_mlp quant_cfg handling to support ModelOpt’s ordered list format
in addition to the existing mapping format. Locate and remove the list entry
whose quantizer_name is "*" and whose configuration disables matching modules,
so enabling all MLP layers remains effective; preserve unrelated quant_cfg
entries and existing behavior for mapping-based configurations.

123-131: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trim and whitelist quant method tokens before lookup. --quant is split on commas without stripping, so values like INT8_DEFAULT_CFG, INT8_SMOOTHQUANT_CFG fail on the leading space. Gate the resolved name against ALL_QUANT_METHODS before calling getattr() so only supported configs reach ModelOpt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/quantize.py` around lines 123 - 131, Update
the quantization-method resolution flow around FRIENDLY_NAMES,
ALL_QUANT_METHODS, and getattr(mtq, resolved, None) to trim whitespace from each
method token before lookup, then validate the resolved name against
ALL_QUANT_METHODS before calling getattr. Reject unsupported names using the
existing ValueError path, and allow only whitelisted configs to reach ModelOpt.
🧹 Nitpick comments (1)
recipes/evo2_megatron_quant/src/quantize.py (1)

183-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the non-deprecated AMP API.

torch.cuda.amp.autocast is deprecated; use torch.autocast(device_type="cuda", dtype=torch.bfloat16) to avoid deprecation warnings and future compatibility issues. (docs.pytorch.org)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/src/quantize.py` at line 183, Replace the
deprecated torch.cuda.amp.autocast context in the quantization flow with
torch.autocast using device_type="cuda" and dtype=torch.bfloat16, while
preserving the surrounding torch.no_grad() behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py`:
- Around line 261-269: Update the output-file loading path in run_test to catch
JSONDecodeError from json.load when output_file is truncated or partially
written. Treat that case as a failed test using the existing CRASH result
structure, including method, precision, error details, and wall_time_s, so the
exception does not escape into main’s benchmark loop.

In `@recipes/evo2_megatron_quant/tests/test_single_model.py`:
- Around line 219-225: Add focused tests around main() covering the exit-code
contract: assert it returns 0 when all result statuses are PASS and 1 when any
method reports FAIL or ERROR. Mock model loading and quantization dependencies
so the tests remain independent of CPUs and checkpoints.

---

Outside diff comments:
In `@recipes/evo2_megatron_quant/src/quantize.py`:
- Around line 135-142: Update the enable_all_mlp quant_cfg handling to support
ModelOpt’s ordered list format in addition to the existing mapping format.
Locate and remove the list entry whose quantizer_name is "*" and whose
configuration disables matching modules, so enabling all MLP layers remains
effective; preserve unrelated quant_cfg entries and existing behavior for
mapping-based configurations.
- Around line 123-131: Update the quantization-method resolution flow around
FRIENDLY_NAMES, ALL_QUANT_METHODS, and getattr(mtq, resolved, None) to trim
whitespace from each method token before lookup, then validate the resolved name
against ALL_QUANT_METHODS before calling getattr. Reject unsupported names using
the existing ValueError path, and allow only whitelisted configs to reach
ModelOpt.

---

Nitpick comments:
In `@recipes/evo2_megatron_quant/src/quantize.py`:
- Line 183: Replace the deprecated torch.cuda.amp.autocast context in the
quantization flow with torch.autocast using device_type="cuda" and
dtype=torch.bfloat16, while preserving the surrounding torch.no_grad() behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 91951a2e-392b-49d3-91dc-b6f3bbcc8567

📥 Commits

Reviewing files that changed from the base of the PR and between f3bf8f5 and 46eb3bd.

📒 Files selected for processing (9)
  • recipes/evo2_megatron_quant/README.md
  • recipes/evo2_megatron_quant/docker/start_container.sh
  • recipes/evo2_megatron_quant/requirements.txt
  • recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py
  • recipes/evo2_megatron_quant/scripts/download_models.sh
  • recipes/evo2_megatron_quant/src/__init__.py
  • recipes/evo2_megatron_quant/src/compress_model.py
  • recipes/evo2_megatron_quant/src/quantize.py
  • recipes/evo2_megatron_quant/tests/test_single_model.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • recipes/evo2_megatron_quant/docker/start_container.sh
  • recipes/evo2_megatron_quant/src/init.py
  • recipes/evo2_megatron_quant/scripts/download_models.sh
  • recipes/evo2_megatron_quant/README.md
  • recipes/evo2_megatron_quant/src/compress_model.py

Comment on lines +261 to +269
if os.path.exists(output_file):
with open(output_file) as f:
result = json.load(f)
result["wall_time_s"] = elapsed
return result
return {
"method": method_name, "precision": precision,
"status": "CRASH", "error": "No output file produced",
"wall_time_s": elapsed,

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard json.load against a partially-written result file.

If the worker is killed while writing output_file (line 217 in the worker script), it can be left truncated. json.load will then raise JSONDecodeError uncaught here, propagating out of run_test() into main()'s loop and aborting the entire benchmark run before the CSV (written only after all configs complete) is ever saved — losing every previously collected result.

🐛 Proposed fix
     if os.path.exists(output_file):
-        with open(output_file) as f:
-            result = json.load(f)
+        try:
+            with open(output_file) as f:
+                result = json.load(f)
+        except (json.JSONDecodeError, OSError) as e:
+            return {
+                "method": method_name, "precision": precision,
+                "status": "CRASH", "error": f"Malformed result file: {e}",
+                "wall_time_s": elapsed,
+            }
         result["wall_time_s"] = elapsed
         return result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if os.path.exists(output_file):
with open(output_file) as f:
result = json.load(f)
result["wall_time_s"] = elapsed
return result
return {
"method": method_name, "precision": precision,
"status": "CRASH", "error": "No output file produced",
"wall_time_s": elapsed,
if os.path.exists(output_file):
try:
with open(output_file) as f:
result = json.load(f)
except (json.JSONDecodeError, OSError) as e:
return {
"method": method_name, "precision": precision,
"status": "CRASH", "error": f"Malformed result file: {e}",
"wall_time_s": elapsed,
}
result["wall_time_s"] = elapsed
return result
return {
"method": method_name, "precision": precision,
"status": "CRASH", "error": "No output file produced",
"wall_time_s": elapsed,
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 261-261: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(output_file)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/scripts/benchmark_real_quant.py` around lines 261
- 269, Update the output-file loading path in run_test to catch JSONDecodeError
from json.load when output_file is truncated or partially written. Treat that
case as a failed test using the existing CRASH result structure, including
method, precision, error details, and wall_time_s, so the exception does not
escape into main’s benchmark loop.

Comment on lines +219 to +225
# 8. Reflect outcome in the exit code so CI/automation can detect failures.
n_not_pass = sum(1 for r in results if r["status"] != "PASS")
return 1 if n_not_pass else 0


if __name__ == "__main__":
sys.exit(main())

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add focused tests for the new exit-code contract.

Cover main() returning 0 when every method passes and 1 when any method is FAIL or ERROR; mock model loading and quantization so the tests remain CPU/checkpoint independent. As per coding guidelines, **/*test*.py requires focused tests when changing CI checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recipes/evo2_megatron_quant/tests/test_single_model.py` around lines 219 -
225, Add focused tests around main() covering the exit-code contract: assert it
returns 0 when all result statuses are PASS and 1 when any method reports FAIL
or ERROR. Mock model loading and quantization dependencies so the tests remain
independent of CPUs and checkpoints.

Source: Coding guidelines

…locate GPU runner

- quantize: strip + whitelist method names against ALL_QUANT_METHODS before
  getattr(); fail loudly instead of silently no-op'ing when --all-mlp cannot
  find the 'default' quant_cfg entry (a silent no-op would report all-MLP
  numbers for a default-scope run); drop deprecated torch.cuda.amp.autocast
- benchmark_real_quant: guard json.load against a truncated worker result so
  one crashed config cannot abort the sweep before any CSV is written
- adapters: drop deprecated torch.cuda.amp.autocast
- move tests/test_single_model.py -> scripts/run_single_model.py: it is a
  manual GPU runner, not a pytest test. tests/ now holds only the CPU-only
  L0 gate, which also removes the pytest-collection hazard at the root
- run_single_model: strip whitespace around comma-separated --quant tokens

Signed-off-by: Rui Zhu <huthruiz@gmail.com>
@huthvincent

Copy link
Copy Markdown
Author

Second pass addressed in 6f0f5e48:

Fixed as suggested

  • scripts/benchmark_real_quant.pyjson.load is now guarded against a truncated worker result: a config whose worker was killed mid-write returns a CRASH row instead of raising JSONDecodeError out of run_test() and aborting the whole sweep before any CSV is written. Good catch.
  • src/quantize.py--quant tokens are stripped, and the resolved name is whitelisted against ALL_QUANT_METHODS before it reaches getattr(mtq, ...).
  • src/quantize.py, src/adapters.py, scripts/benchmark_real_quant.py — replaced the deprecated torch.cuda.amp.autocast with torch.autocast(device_type="cuda", ...).

Two I solved differently — reasoning below, happy to revisit

  1. quant_cfg list layout (quantize.py). I agree with the risk you identified — a silent no-op for --all-mlp is the real danger here. But I couldn't verify the "ModelOpt 0.45+ stores quant_cfg as an ordered list" claim from my environment, and writing speculative code against an unverified internal layout seemed worse than the bug itself. So instead of guessing at the list shape, I made the failure loud: if quant_cfg isn't a mapping containing a default entry, it now raises with the offending type rather than silently doing nothing. For a measurement harness, erroring out beats quietly reporting default-scope numbers as "all-MLP". If you can point me at the ModelOpt version and the concrete entry shape, I'll happily add explicit list handling.

  2. Focused tests for the exit-code contract (test_single_model.py). The root problem was that the file was misnamed: it is a manual GPU runner (needs a CUDA device and an NGC checkpoint), not a pytest test — and its tests/test_*.py name is exactly what made pytest try to collect it in your first review. Rather than bolt mocks onto a GPU harness to make it look like a unit test, I moved it to scripts/run_single_model.py. tests/ now contains only the genuine CPU-only L0 gate (test_compressed_linear_roundtrip.py), which needs no GPU or checkpoint and is what CI should actually run. That removes the collection hazard at the root rather than papering over it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant