Skip to content

fix: tighter memory checks between training and generate - #598

Closed
mckornfield wants to merge 1 commit into
mainfrom
low-memory-training-fixes/mck
Closed

fix: tighter memory checks between training and generate#598
mckornfield wants to merge 1 commit into
mainfrom
low-memory-training-fixes/mck

Conversation

@mckornfield

@mckornfield mckornfield commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Pre-Review Checklist

Ensure that the following pass:

  • mise run format && mise run check or via prek validation.
  • mise run test passes locally
  • mise run test:e2e passes locally
  • mise run test:ci-container passes locally (recommended)
  • GPU CI status check passes -- comment /sync on this PR to trigger a run (auto-triggers on ready-for-review)

Pre-Merge Checklist

  • New or updated tests for any fix or new behavior
  • Updated documentation for new features and behaviors, including docstrings for API docs.

Other Notes

  • Closes #

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced GPU memory management with improved cleanup and synchronization during model training and generation.
    • Improved trainer resource cleanup and deallocation after training completion.
    • Added validation for GPU memory configuration parameters.
  • Tests

    • Added test validation for GPU memory configuration settings.

@mckornfield
mckornfield requested a review from a team as a code owner June 12, 2026 22:50
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

VllmBackend initialization now enforces VRAM utilization from config instead of runtime queries, calling cleanup_memory with enhanced CUDA operations beforehand. SafeSynthesizer tracks training elapsed time across lifecycle methods and explicitly tears down the trainer instance with dedicated state management.

Changes

Resource Management Refactoring: VRAM Configuration and Training Lifecycle

Layer / File(s) Summary
VRAM fraction configuration and cleanup
src/nemo_safe_synthesizer/generation/vllm_backend.py, src/nemo_safe_synthesizer/llm/utils.py
VllmBackend.initialize() validates and applies config.training.max_vram_fraction to vLLM's gpu_memory_utilization, raising ParameterError for invalid values. cleanup_memory() is enhanced to check CUDA availability, synchronize threads, clear cache, and perform IPC collection when CUDA is available.
VRAM configuration and cleanup test coverage
tests/generation/test_vllm_backend.py
Tests verify vLLM receives gpu_memory_utilization from configuration, validate that ParameterError is raised for max_vram_fraction <= 0, and remove deprecated get_max_vram patches.
Training time tracking and trainer lifecycle
src/nemo_safe_synthesizer/sdk/library_builder.py
SafeSynthesizer adds _training_time_sec field to track elapsed training time captured during train(), stores and re-captures it in generate() while explicitly calling trainer.teardown() and deleting the trainer reference, then uses the stored value in evaluate() instead of repeated lookups.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Possibly related PRs

  • NVIDIA-NeMo/Safe-Synthesizer#480: Refactors VRAM utilities and max_vram_fraction interpretation, directly overlapping with this PR's removal of get_max_vram() and configuration-driven VRAM application.

Suggested labels

bug, test


Suggested reviewers

  • kendrickb-nvidia
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: tighter memory checks between training and generate' directly relates to the main changes in the PR, which involve enforcing stricter memory management through vLLM's gpu_memory_utilization, improving cleanup_memory() with CUDA synchronization, and better tracking of training time across training and generation phases.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch low-memory-training-fixes/mck

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

@coderabbitai coderabbitai Bot added bug Defects in shipped behavior test Test-only addition or change labels Jun 12, 2026
self._training_time_sec = result.elapsed_time
trainer.teardown()
del self.trainer
trainer = None
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses two memory-management problems that surface in train-then-generate pipelines: get_max_vram() could return 0.0 immediately after training teardown on some CUDA nodes, and training_time was always None in evaluate() because the trainer had already been cleaned up by then.

  • vllm_backend.py: Replaces the dynamic get_max_vram() call with config.training.max_vram_fraction and adds a hard ParameterError guard for zero/negative values; cleanup_memory() is now called at the top of initialize() to flush residual training state before vLLM claims memory.
  • llm/utils.py: cleanup_memory() gains a CUDA-availability early-return, torch.cuda.synchronize() to ensure all in-flight kernels complete, and torch.cuda.ipc_collect() to release IPC shared buffers; all controlled by the existing no_grad context.
  • library_builder.py: _training_time_sec is captured from trainer.results in both train() and (redundantly, as a safety net) at the start of generate() before teardown, then used as the seed value for training_time in evaluate() instead of hard-coding None.

Confidence Score: 4/5

The change is safe to merge; all three fixes address real, reproducible failure modes in the train→generate→evaluate pipeline.

The core logic — using a config-backed memory fraction instead of a dynamically computed one, capturing training time before trainer teardown, and hardening cleanup_memory — is straightforward and well-covered by the new test. The only open question is whether torch.cuda.synchronize() should target all devices rather than just the default, which matters only on multi-GPU nodes and leaves the memory-release intent intact even when only partially fulfilled.

src/nemo_safe_synthesizer/llm/utils.py — the synchronize() call targets only the current CUDA device.

Important Files Changed

Filename Overview
src/nemo_safe_synthesizer/generation/vllm_backend.py Replaces dynamic get_max_vram() call (which could return 0.0 post-training teardown) with config-backed max_vram_fraction; adds cleanup_memory() call and explicit ParameterError guard for zero/negative values.
src/nemo_safe_synthesizer/llm/utils.py Strengthens cleanup_memory() with a CUDA-availability guard, torch.cuda.synchronize(), and ipc_collect(); synchronize() has no device arg and only targets the default device.
src/nemo_safe_synthesizer/sdk/library_builder.py Captures training elapsed time before trainer teardown to fix the lost-training-time bug in evaluate(); adds del self.trainer to avoid stale attribute access after teardown.
tests/generation/test_vllm_backend.py Removes get_max_vram mock, adds assertion for gpu_memory_utilization, and adds new test for zero-fraction rejection; coverage looks correct.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant SS as SafeSynthesizer
    participant TR as Trainer
    participant CM as cleanup_memory()
    participant VB as VllmBackend

    U->>SS: .train()
    SS->>TR: load_model() / train()
    TR-->>SS: results.elapsed_time
    SS->>SS: "_training_time_sec = elapsed_time"

    U->>SS: .generate()
    SS->>SS: capture _training_time_sec from trainer.results (safety net)
    SS->>TR: teardown()
    SS->>SS: del self.trainer
    SS->>VB: initialize()
    VB->>CM: cleanup_memory()
    CM->>CM: gc.collect()
    CM->>CM: synchronize() / empty_cache() / ipc_collect()
    VB->>VB: "max_vram = config.training.max_vram_fraction"
    VB->>VB: "raise ParameterError if max_vram <= 0"
    VB->>VB: "vLLM(gpu_memory_utilization=max_vram)"
    VB-->>SS: initialized

    U->>SS: .evaluate()
    SS->>SS: "training_time = _training_time_sec"
    SS-->>U: results with correct training_time
Loading

Reviews (1): Last reviewed commit: "fix: tighter memory checks between train..." | Re-trigger Greptile

Comment on lines 412 to +415
with torch.no_grad():
torch.cuda.synchronize()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()

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.

P2 torch.cuda.synchronize() called without a device argument synchronizes only the currently selected CUDA device (device 0 by default). After multi-GPU training, streams on other devices remain unsynchronized, so their work may not be fully flushed before empty_cache() runs, potentially leaving memory unreleased on non-default devices.

Suggested change
with torch.no_grad():
torch.cuda.synchronize()
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
with torch.no_grad():
for i in range(torch.cuda.device_count()):
torch.cuda.synchronize(i)
torch.cuda.empty_cache()
torch.cuda.ipc_collect()

Comment on lines 570 to +572
trainer.teardown()
del self.trainer
trainer = None

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.

P2 After del self.trainer, assigning trainer = None reassigns the local variable but has no effect — the local is never read again and the attribute has already been removed from the instance. The assignment is a no-op that may imply to readers that trainer is used after this point.

Suggested change
trainer.teardown()
del self.trainer
trainer = None
trainer.teardown()
del self.trainer

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ea827943-c74d-49b3-b00b-40ecfbde6e53

📥 Commits

Reviewing files that changed from the base of the PR and between 64a3170 and 1c76bd0.

📒 Files selected for processing (4)
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Unit Tests (3.12)
  • GitHub Check: Unit Tests (3.11)
  • GitHub Check: Unit Tests (3.13)
  • GitHub Check: Smoke Tests
  • GitHub Check: Analyze (Python)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{md,markdown,py}

📄 CodeRabbit inference engine (.cursor/rules/agent-markdown-style.mdc)

**/*.{md,markdown,py}: Avoid decorative bold (**text**) in list items, body text, and docstrings; use structural cues (headers, list markers, colons, backticks) for emphasis instead
Use backticks for code identifiers, paths, and CLI commands in markdown and docstrings

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Place durable implementation guidance in function and class docstrings for public contracts and source comments for local invariants
Target Python 3.11–3.13 with modern syntax (X | Y, list[str], Self). Python 3.14+ is not supported

**/*.py: Use Field(description=...) as the canonical field docstring for Pydantic models.
Use assignment style (type = Field(default=..., description="...")) as the default for Pydantic model fields rather than Annotated-only patterns.
Use Annotated only when the field carries additional metadata beyond Field() -- ValueValidator, AutoParam, DependsOnValidator, reusable constrained type aliases, nested-type constraints, or discriminated unions.
Use @dataclass(frozen=True) for immutable value objects and validators. Mutable @dataclass acceptable for builders, accumulators, and pipeline state.
Use field(default_factory=list) for mutable defaults in dataclasses, never = [].
Use StrEnum for string-valued enums used in configs/serialization. Plain Enum for internal-only named constants.
Use X | Y not Optional[X] or Union[X, Y] in type hints.
Use list[str] not List[str], dict[str, int] not Dict[str, int] in type hints.
Use Self for fluent method returns in type hints.
Use collection ABCs for function arguments (Sequence, Mapping, Iterable) so callers can pass any compatible container; concrete types for return values.
Use Protocol for structural subtyping when you need duck-typing boundaries.
Avoid Any in type hints -- prefer object, generics, or Protocol.
Use TYPE_CHECKING guards for heavy imports (pandas, torch, transformers); not needed for stdlib or lightweight imports.
Prefer match/case for dispatch on types or tagged values. if/elif is fine for simple boolean predicates.
Use comprehensions over imperative loops where intent is clearer. No multiple for clauses -- optimize for readability, not conciseness.
Use builder pattern with with_* methods...

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
src/**/*.py

📄 CodeRabbit inference engine (STYLE_GUIDE.md)

src/**/*.py: Use observability.get_logger(__name__) -- never logging.getLogger() or structlog.get_logger() directly.
Use category loggers: .runtime for internals, .user for progress/results, .system for system events.
Never use print() for operational output. Use click.echo() for CLI output or sys.stdout.write() for raw output in tools.
Use extra={} for data that downstream tools should query or aggregate in log calls (metrics, counts, durations). f-strings are fine for human-readable context.
Raise from the custom error hierarchy with dual inheritance: SafeSynthesizerError (base), UserError, DataError, ParameterError, GenerationError, InternalError.
Prefer clamping/saturation over raising when out-of-range inputs shouldn't crash the system -- return a bounded value with a log warning.
Error messages must precisely match the actual error condition. Use !r for repr of interpolated pieces to clearly identify them.
Use relative imports in src/ (from ..observability import get_logger), absolute imports in tests/ (from nemo_safe_synthesizer.observability import get_logger).
Use pathlib.Path instead of os.path. Tolerate os.path only in vendored/tooling scripts.
Do not use print() statements in library code. Use get_logger(__name__) from observability.py or click.echo() for CLI. print() is fine in tests, standalone scripts, and tooling.
Do not use assert for validation in library code. Use if/raise for input validation. assert is fine in tests where pytest relies on it.

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py

⚙️ CodeRabbit configuration file

Review library code against STYLE_GUIDE.md. Focus on behavior, API contracts, error handling, resource cleanup, typing, logging, and user-facing failures. Public APIs and nontrivial functions need Google-style docstrings.

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
**/*.{py,sh,yaml,yml}

📄 CodeRabbit inference engine (STYLE_GUIDE.md)

Include SPDX copyright headers in all source files using hash-comments for .py, .sh, .yaml, .yml files.

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
**/*.{py,sh,yaml,yml,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

All source files (.py, .sh, .yaml, .yml, .md) require SPDX copyright headers; mise run format adds them automatically

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
src/nemo_safe_synthesizer/**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

API reference pages are auto-generated from Python docstrings using Google-style format; write docstrings in src/nemo_safe_synthesizer/ and they will appear in the reference/

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
**/*

⚙️ CodeRabbit configuration file

**/*: Review as a senior maintainer for NeMo Safe Synthesizer. Prioritize issues that can change behavior, break user workflows, weaken privacy guarantees, hide failures, make tests unreliable, or create maintenance risk. Avoid generic style commentary unless it points to a concrete project convention that automated tools will not catch.
Comment only when the finding is actionable and tied to changed code. For each finding, state the impact, the condition that triggers it, and the smallest practical fix. Prefer one precise comment over broad advice. Do not ask for refactors outside the PR scope unless the changed code creates the problem.
Review type guidance: - Potential issue: use for correctness bugs, data loss, privacy leaks,
security risks, broken public APIs, invalid config behavior, missing
validation, hidden failures, nondeterministic tests, or CI breakage.

  • Refactor suggestion: use for local maintainability problems introduced
    by the diff when they have clear future cost, such as duplicated setup,
    unclear boundaries, over-mocking, avoidable complexity, or opaque test
    helpers.
  • Nitpick: avoid in chill mode. Do not emit formatting, import-order,
    wording, or style-only comments unless automated tools cannot catch the
    issue and it affects maintainability.

Severity guidance: - Critical: security/privacy leaks, data loss, training/test/holdout
contamination, or broken release/package/core pipeline execution.

  • Major: incorrect generation/training/evaluation behavior, broken
    CLI/SDK public API, invalid config defaults or validators, or GPU/vLLM
    cleanup and process-isolation bugs likely to fail CI or production
    runs.
  • Minor: localized bugs, missing focused tests for changed behavior, or
    bad test patterns that weaken regression coverage.
  • Trivial: small cleanup with no behavior impact. Usually suppress in
    chill mode.
  • Info: context only. Avoid unless it helps reviewers understand risk.
    Safe-Synthesizer-specific review focus: - Data ...

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
**

⚙️ CodeRabbit configuration file

**:

AGENTS.md

Guide for AI agents (Cursor, Windsurf, Claude Code, etc.) working in the Safe-Synthesizer repo.

This project loads local developer preferences from @AGENTS.local.md. You MUST read this file if it exists and give its instructions top priority.

Skills

Repo-specific skills live in .agents/skills/; see .agents/README.md for the catalog. Read a skill when the task matches its scope instead of copying workflow details into this file.

Durable implementation guidance belongs with the code it describes: function and class docstrings for public contracts and source comments for local invariants. Test-suite guidance belongs in tests/TESTING.md.

Repo Conventions

See STYLE_GUIDE.md for detailed code style conventions (Python, markdown, Dockerfiles, shell scripts, testing, config files, docstrings).

Use uv for everything -- never pip or raw python. Python 3.11–3.13 with modern syntax (X | Y, list[str], Self). Python 3.14+ is not supported.

Common commands: mise run test (unit tests), mise run format (auto-fix formatting + lint + copyright), mise run check (read-only local quality checks), mise run validate (pre-PR quality, lock, and CI unit checks), mise run typecheck (ty only). Always use mise tasks or the wrapper scripts in tools/ instead of running ruff or ty directly. Use uv run for Python execution. When in doubt, inspect mise tasks and pytest --markers.

The canonical uv sync command for a full GPU/dev environment is:

uv sync --frozen --extra cu129 --extra engine --group dev

Bare uv sync --frozen (without extras) installs an incomplete environment -- ty, import checks, and GPU tests will fail.

Feature branches off main. Branch names often include an issue number prefix (e.g., <author>/123-short-name).

Do ...

Files:

  • src/nemo_safe_synthesizer/llm/utils.py
  • src/nemo_safe_synthesizer/generation/vllm_backend.py
  • src/nemo_safe_synthesizer/sdk/library_builder.py
  • tests/generation/test_vllm_backend.py
src/nemo_safe_synthesizer/generation/**/*.py

⚙️ CodeRabbit configuration file

Review generation changes for retry loops, stopping conditions, invalid record handling, regex/structured output contracts, backend teardown, memory cleanup, and vLLM assumptions.

Files:

  • src/nemo_safe_synthesizer/generation/vllm_backend.py
**/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use the unit marker instead of the deprecated unit_test marker for test identification

Files:

  • tests/generation/test_vllm_backend.py
tests/**

📄 CodeRabbit inference engine (.cursor/rules/repo-navigation.mdc)

tests/**: Mirror src/ directory structure in tests/ directory for test organization
Auto-mark tests by directory: tests/e2e/e2e, tests/smoke/smoke, otherwise default to unit

Mirror source code directory structure in tests directory (e.g., tests/training/, tests/generation/ parallel to source structure)

Files:

  • tests/generation/test_vllm_backend.py

⚙️ CodeRabbit configuration file

tests/**:

Testing Guide

Comprehensive testing reference for Safe-Synthesizer developers. Covers commands, markers, test data, fixtures, and gotchas.

Read First

  1. tests/conftest.py -- auto-marking, load_test_dataset/load_test_dataframe, fixture_mock_processor pattern
  2. pytest.ini -- markers, asyncio, timeout
  3. tests/evaluation/conftest.py -- most complex: Faker-based make_df, nullable dtype conversion
  4. tests/generation/conftest.py -- JSONL/schema fixtures, fixture_valid_iris_dataset_jsonl_and_schema

Running Tests

All mise test tasks, grouped by scope:

mise run test                              # Unit (excludes slow, e2e, and smoke)
mise run test:unit-slow                    # Unit tests including slow (excludes e2e and smoke)
mise run test:smoke                        # CPU smoke tests (~few min, no GPU required)
mise run test:smoke:gpu                    # All staged GPU smoke tests (requires CUDA)
mise run test:smoke:gpu:train-only
mise run test:smoke:gpu:generation
mise run test:smoke:gpu:resume
mise run test:smoke:gpu:structured-generation
mise run test:smoke:gpu:timeseries
mise run test:smoke:gpu:smollm2
mise run test:e2e                          # All e2e (requires CUDA) -- runs default + dp
mise run test:e2e:default                  # e2e default (no-DP) tests only
mise run test:e2e:dp                       # e2e DP tests only
mise run test:ci                           # CI unit tests with coverage (excludes slow, e2e, gpu, smoke)
mise run test:ci-slow                      # CI slow tests with coverage
mise run test:ci-container                 # CI tests in a Linux container (Docker/Podman)

Run a single test:

uv run --frozen pytest tests/path/test_file.py::test_name -vvs -n0

Test runner: uv run --frozen pytest -n auto --dist loadscope -vv...

Files:

  • tests/generation/test_vllm_backend.py
tests/**/*.py

📄 CodeRabbit inference engine (STYLE_GUIDE.md)

tests/**/*.py: Use file naming test_*.py, class naming Test*, function naming test_<module>_<expected_behavior> for test files.
Use function-scoped fixtures by default. Session scope only when empirically justified by test runtime.
Use bare assert as primary assertion style; pytest.raises() with match= for exceptions; pytest.approx() for floating-point comparisons.
Docstrings are optional for simple tests, recommended for complex/e2e tests explaining purpose.
Markers are auto-assigned by path via pytest_collection_modifyitems (/e2e/ -> e2e, /smoke/ -> smoke, default -> unit). Use explicit markers: @pytest.mark.slow, @pytest.mark.requires_gpu, @pytest.mark.timeout().
Use tmp_path fixture for file operations in tests, never write to the repo tree.
Mark CUDA-dependent tests with @pytest.mark.e2e, @pytest.mark.smoke, or @pytest.mark.requires_gpu.
Mock only external boundaries in tests, not internal implementation details.
Ensure test isolation: no shared mutable state or execution-order dependencies between tests. Include required setup in the test or a fixture.
Use @pytest.mark.parametrize for testing multiple input combinations rather than copy-pasting similar tests.

tests/**/*.py: Auto-mark tests based on file path: tests under /e2e/ get e2e marker, tests under /smoke/ get smoke marker, all others get unit marker (only if no category marker already present)
Every test should have exactly one category marker: unit, smoke, or e2e
Use pytest.mark.requires_gpu modifier on tests that need CUDA hardware
Use pytest.mark.vllm on tests using vLLM generation backend and ensure each vLLM test file runs in its own process for GPU memory isolation
Use pytest.mark.slow on long-running tests
Use pytest.mark.smollm2 for SmolLM2 Hub download tests to enable process isolation
Use pytest.mark.noautouse to skip autouse fixtures for specific tests
Use load_test_dataset(filename) helper to load test datasets from `...

Files:

  • tests/generation/test_vllm_backend.py

⚙️ CodeRabbit configuration file

Review tests against tests/TESTING.md. Check marker usage, fixture naming, tmp_path usage, determinism, and GPU/vLLM process-isolation requirements. Flag slop tests that only check that code runs, assert result is not None when stronger invariants exist, over-mock internal implementation details, patch around the bug instead of reproducing it, or add broad snapshot/golden churn without a clear contract. Flag change detector tests that fail on harmless refactors, formatting, record ordering, incidental wording, or private implementation details without demonstrating a behavior regression. Prefer existing fixtures or focused new fixtures for repeated setup; keep tests DRY when reasonable without making the behavior under test opaque. print() is allowed in tests.

Files:

  • tests/generation/test_vllm_backend.py
🧠 Learnings (2)
📚 Learning: 2026-05-27T22:20:37.354Z
Learnt from: kendrickb-nvidia
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 520
File: tests/generation/test_vllm_backend.py:556-587
Timestamp: 2026-05-27T22:20:37.354Z
Learning: In NVIDIA-NeMo/Safe-Synthesizer, `tests/conftest.py`’s `pytest_collection_modifyitems` hook applies pytest category markers automatically based on each test file’s path: tests under `/e2e/` get `pytest.mark.e2e`, tests under `/smoke/` get `pytest.mark.smoke`, and all other tests get `pytest.mark.unit`. Therefore, when reviewing pytest tests outside `tests/e2e/` and `tests/smoke/`, do not flag missing explicit `pytest.mark.unit` decorators on test classes/functions as an issue (the hook will add them during collection). If a new test directory/category is introduced, ensure the hook is updated so it’s categorized correctly.

Applied to files:

  • tests/generation/test_vllm_backend.py
📚 Learning: 2026-06-04T16:14:09.868Z
Learnt from: binaryaaron
Repo: NVIDIA-NeMo/Safe-Synthesizer PR: 526
File: tests/generation/test_vllm_backend.py:399-509
Timestamp: 2026-06-04T16:14:09.868Z
Learning: In this repo, only apply `pytest.mark.vllm` to smoke tests under `tests/smoke/` that actually run real vLLM GPU generation and therefore require per-file process isolation (e.g., `test-smoke-gpu-*` Makefile targets). Do not apply `pytest.mark.vllm` to unit-style tests under `tests/generation/` that merely import `vllm_backend` but never instantiate a real vLLM engine and never call `.generate()` (GPU not required). Note that `tests/conftest.py` auto-marks these as `unit` via `pytest_collection_modifyitems`, and `vllm` is not among the auto-mark categories—so if a test in `tests/generation/` has `vllm`, it should be treated as a review issue unless it meets the real GPU generation criteria above.

Applied to files:

  • tests/generation/test_vllm_backend.py
🔇 Additional comments (4)
src/nemo_safe_synthesizer/generation/vllm_backend.py (1)

287-293: LGTM!

src/nemo_safe_synthesizer/llm/utils.py (1)

409-415: LGTM!

tests/generation/test_vllm_backend.py (2)

397-397: LGTM!


399-412: LGTM!

Comment on lines 567 to +572
if trainer is not None:
if result := getattr(trainer, "results", None):
self._training_time_sec = result.elapsed_time
trainer.teardown()
del self.trainer
trainer = None

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

Clear self.trainer in a finally block.

If trainer.teardown() raises here, generate() exits before the generation backend is initialized and the trainer object stays attached to self, which defeats the memory-release step this PR is adding. Drop the attribute in finally so the large trainer graph is released even on teardown failures.

Suggested fix
         trainer = getattr(self, "trainer", None)
         if trainer is not None:
             if result := getattr(trainer, "results", None):
                 self._training_time_sec = result.elapsed_time
-            trainer.teardown()
-            del self.trainer
-            trainer = None
+            try:
+                trainer.teardown()
+            finally:
+                del self.trainer
+                trainer = None

As per coding guidelines, use try/finally for resource cleanup and keep teardown failures from blocking subsequent work.

Source: Coding guidelines

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

Labels

area:generation area:llm area:sdk-cli area:tests bug Defects in shipped behavior test Test-only addition or change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants