Skip to content

feat: Add EXAONE 4.0 model bridge (LG AI Research) - #2532

Merged
yaoyu-33 merged 18 commits into
NVIDIA-NeMo:mainfrom
Bias92:feat/exaone4-bridge
Jun 19, 2026
Merged

feat: Add EXAONE 4.0 model bridge (LG AI Research)#2532
yaoyu-33 merged 18 commits into
NVIDIA-NeMo:mainfrom
Bias92:feat/exaone4-bridge

Conversation

@Bias92

@Bias92 Bias92 commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

feat: Add EXAONE 4.0 model bridge (LG AI Research)


Summary

Add HuggingFace ↔ Megatron checkpoint conversion support for EXAONE 4.0 (LG AI Research).

Architecture

EXAONE 4.0 uses a pure Post-LayerNorm architecture that differs from standard Pre-LN models:

h = PostAttnNorm(x + Attn(x))      # no pre-norm before attention
o = PostFFNNorm(h + MLP(h))         # no pre-norm before MLP

Key features:

  • Pure Post-LN via custom layer spec (TERowParallelLinearLayerNorm, following Gemma2 pattern)
  • QK RMSNorm (following Qwen3 pattern)
  • GQA (32 heads / 8 KV heads), SwiGLU, RMSNorm
  • RoPE with llama3-style scaling (factor=16, theta=1M)
  • Tied word embeddings (share_embeddings_and_output_weights=True)

Files

File Description
exaone4_provider.py Model config + custom Post-LN TransformerLayerSubmodules spec
exaone4_bridge.py Weight mapping registry (10 mappings: Auto + QKV + GatedMLP)
init.py Module exports
models/init.py Registration in bridge model index

Tested with

  • LGAI-EXAONE/EXAONE-4.0-1.2B (30 layers, 2048 hidden)
  • AutoBridge.from_hf_pretrained() → provider loads correctly
  • to_megatron_provider()num_layers=30, hidden_size=2048, qk_layernorm=True
  • mapping_registry() → 10 mappings verified
  • Colab T4 GPU, megatron-core 0.15.3+

Future work

  • EXAONE 4.0 32B: Hybrid attention (LLLG pattern — 3 local + 1 global layers) with sliding window
  • K-EXAONE 236B MoE: Expert parallel + MoE bridge support
  • Training recipes
feat: Add EXAONE 4.0 model bridge (LG AI Research)

Summary

Add HuggingFace ↔ Megatron checkpoint conversion support for [EXAONE 4.0](https://huggingface.co/LGAI-EXAONE/EXAONE-4.0-1.2B) (LG AI Research).

Architecture

EXAONE 4.0 uses a pure Post-LayerNorm architecture that differs from standard Pre-LN models:

h = PostAttnNorm(x + Attn(x))      # no pre-norm before attention
o = PostFFNNorm(h + MLP(h))         # no pre-norm before MLP

Key features:

  • Pure Post-LN via custom layer spec (TERowParallelLinearLayerNorm, following Gemma2 pattern)
  • QK RMSNorm (following Qwen3 pattern)
  • GQA (32 heads / 8 KV heads), SwiGLU, RMSNorm
  • RoPE with llama3-style scaling (factor=16, theta=1M)
  • Tied word embeddings (share_embeddings_and_output_weights=True)

Files

File Description
exaone4_provider.py Model config + custom Post-LN TransformerLayerSubmodules spec
exaone4_bridge.py Weight mapping registry (10 mappings: Auto + QKV + GatedMLP)
__init__.py Module exports
models/__init__.py Registration in bridge model index

Tested with

  • LGAI-EXAONE/EXAONE-4.0-1.2B (30 layers, 2048 hidden)
  • AutoBridge.from_hf_pretrained() → provider loads correctly
  • to_megatron_provider()num_layers=30, hidden_size=2048, qk_layernorm=True
  • mapping_registry() → 10 mappings verified
  • Colab T4 GPU, megatron-core 0.15.3+

Future work

  • EXAONE 4.0 32B: Hybrid attention (LLLG pattern — 3 local + 1 global layers) with sliding window
  • K-EXAONE 236B MoE: Expert parallel + MoE bridge support
  • Training recipes

Summary by CodeRabbit

  • New Features
    • Added support for EXAONE 4.0 models within the Megatron framework.
    • Enabled bidirectional conversion between HuggingFace EXAONE 4.0 and Megatron formats.
    • Introduced a pre-configured 1.2B variant with optimized hyperparameters and RoPE scaling.

@copy-pr-bot

copy-pr-bot Bot commented Feb 25, 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.

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
@Bias92
Bias92 force-pushed the feat/exaone4-bridge branch from 2df4d8d to fc798ba Compare February 25, 2026 19:17
@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Introduces EXAONE 4.0 model support to Megatron's bridge infrastructure by adding new provider classes, transformer layer specifications, and bidirectional configuration conversion between HuggingFace and Megatron formats.

Changes

Cohort / File(s) Summary
API Exports
src/megatron/bridge/models/__init__.py, src/megatron/bridge/models/exaone/__init__.py
Exposes Exaone4Bridge, Exaone4ModelProvider, and Exaone4ModelProvider1P2B as public API through module-level imports and all declarations.
Bridge Implementation
src/megatron/bridge/models/exaone/exaone4_bridge.py
Implements Exaone4Bridge class for bidirectional conversion between HuggingFace EXAONE 4.0 configs and Megatron-Core GPTModel, with comprehensive parameter mapping registry supporting QKV and GatedMLP projection merging.
Provider Implementation
src/megatron/bridge/models/exaone/exaone4_provider.py
Defines EXAONE 4.0 Post-LayerNorm transformer configuration including custom TERowParallelLinearLayerNorm layer, exaone4_layer_spec function, and model provider classes with RMSNorm, RoPE, and QK layernorm support; includes 1.2B variant with specific hyperparameters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested labels

enhancement

Suggested reviewers

  • yashaswikarnati
  • yaoyu-33
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Results For Major Changes ⚠️ Warning PR adds 461 lines of new model bridge code without corresponding unit or functional test files, breaking established project pattern where all model bridges include comprehensive test suites. Add formal unit and functional test files to tests/unit_tests/models/exaone/ and tests/functional_tests/models/exaone/ following existing model bridge patterns, including provider instantiation, parameter mapping, and checkpoint conversion tests.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding EXAONE 4.0 model bridge support to the Megatron codebase.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

🧹 Nitpick comments (3)
src/megatron/bridge/models/exaone/exaone4_provider.py (1)

58-58: Unused logger — dead code.

logger = logging.getLogger(__name__) is defined but there are no logging calls in this file. Remove it (and the import logging) unless you intend to add structured logging, in which case use print_rank_0 per the project convention.

♻️ Proposed fix
-import logging
 from dataclasses import dataclass
 ...
-
-logger = logging.getLogger(__name__)

As per coding guidelines: "Use print_rank_0 for logging in model bridge to avoid duplicate output across ranks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` at line 58, Remove the
unused logger and its import: delete the standalone "logger =
logging.getLogger(__name__)" and the "import logging" statement in
exaone4_provider.py; if you need any runtime messages in functions/classes in
this file, replace uses of logger with the project convention print_rank_0(...)
instead (search for "logger" and the top-level import to update accordingly).
src/megatron/bridge/models/__init__.py (1)

296-298: __all__ entries are out of alphabetical model-family order.

The EXAONE entries are inserted between the Qwen3 and DeepSeek blocks. Moving them before the DeepSeek group (or after all the Qwen entries) would match the alphabetical ordering followed by the rest of __all__.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/__init__.py` around lines 296 - 298, The __all__
list is not alphabetically ordered: the entries "Exaone4Bridge",
"Exaone4ModelProvider", and "Exaone4ModelProvider1P2B" are inserted between
Qwen3 and DeepSeek blocks; move these three EXAONE entries so they appear in
alphabetical model-family order (i.e., place them before the DeepSeek group or
immediately after the Qwen entries) to restore consistent ordering in __all__.
src/megatron/bridge/models/exaone/exaone4_bridge.py (1)

37-53: Dead code: logger is defined but never used.

logging is imported and logger = logging.getLogger(__name__) is created, but there are no logging calls in this file. Remove both if logging is not needed; otherwise add relevant print_rank_0 calls for key bridge events per the project convention for model bridges.

♻️ Proposed fix
-import logging
-
 import torch
 ...
-
-logger = logging.getLogger(__name__)

As per coding guidelines: "Use print_rank_0 for logging in model bridge to avoid duplicate output across ranks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_bridge.py` around lines 37 - 53,
The file defines logger and imports logging but never uses them; either remove
the unused import and the logger = logging.getLogger(__name__) line, or replace
any intended log calls with the project convention print_rank_0. Specifically,
remove the unused logging import and the logger symbol from exaone4_bridge.py if
no runtime messages are needed, or add print_rank_0 calls (not logger) inside
relevant bridge functions/classes such as MegatronModelBridge-related methods,
Exaone4ModelProvider hooks, or PreTrainedCausalLM integration points to report
key bridge events.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/megatron/bridge/models/__init__.py`:
- Around line 28-32: The bridge implementation file must be renamed to follow
project conventions: move/rename the current exaone4_bridge.py into
model_bridge.py and update any imports to import Exaone4Bridge,
Exaone4ModelProvider, and Exaone4ModelProvider1P2B from model_bridge instead of
exaone4_bridge; also fix the import ordering in __init__.py so the Exaone*
import block appears in strict alphabetical order among other model imports
(place the Exaone imports in the correct spot relative to Deepseek and others)
to match the established import ordering convention.

In `@src/megatron/bridge/models/exaone/exaone4_bridge.py`:
- Line 115: The code uses single-quoted string literals in the getattr call for
rotary_base; change the single quotes to double quotes so the line reads using
double-quoted strings for the attribute names (update the getattr(hf_config,
"rope_theta", getattr(hf_config, "rotary_base", 1000000.0)) invocation),
preserving the same logic and default value and leaving variable names like
rotary_base and hf_config unchanged.
- Around line 150-157: The code unconditionally emits hard-coded RoPE scaling
keys in the hf_config block (low_freq_factor, high_freq_factor,
original_max_position_embeddings) causing lossy megatron_to_hf_config
round-trips; update the logic to persist and reuse the full rope_scaling dict on
the provider (e.g., add a provider.rope_scaling_dict or store
provider.rope_scaling as the complete dict) and change the hf_config assignment
in exaone4_bridge (where hf_config["rope_scaling"] is set) to read and copy
values from that persisted dict instead of using fixed literals; also update the
provider_bridge/megatron_to_hf_config code path to populate
provider.rope_scaling from incoming model config so the round-trip restores
original low_freq_factor, high_freq_factor, and
original_max_position_embeddings.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py`:
- Line 35: Remove the unused Optional import from the typing import in
exaone4_provider.py: update the import line that currently reads "from typing
import Callable, Optional, Union" to drop Optional so it becomes "from typing
import Callable, Union" (or equivalent import ordering used in the file) to
satisfy static analysis and avoid the unused-import warning; ensure no other
occurrences of Optional are referenced in functions or type hints such as any in
ExaOne4Provider-related methods before committing.
- Line 175: Change the type annotation for transformer_layer_spec to use PEP 604
union syntax: replace Union[ModuleSpec, Callable[["GPTModelProvider"],
ModuleSpec]] with ModuleSpec | Callable[["GPTModelProvider"], ModuleSpec]
(symbol: transformer_layer_spec, types: ModuleSpec, Callable, GPTModelProvider,
exaone4_layer_spec); also remove any now-unused Union import from typing.
- Around line 91-94: TERowParallelLinearLayerNorm.forward currently calls
super().forward(x) -> (output, bias) then applies self.post_layernorm(output)
before bias is added; add a guard to prevent silently incorrect behavior by
checking the returned bias: either assert bias is None with a clear message
(e.g., raise AssertionError("TERowParallelLinearLayerNorm expects no deferred
bias; set add_bias_linear=False or handle bias before layernorm")), or if you
want to support bias, add bias to output before calling self.post_layernorm
(i.e., output = output + bias) and return (normalized_output, None); make the
change inside TERowParallelLinearLayerNorm.forward and reference
super().forward, self.post_layernorm, and the bias variable.
- Line 102: The function exaone4_layer_spec currently declares a parameter
config but never uses it; either remove the unused parameter or clearly mark it
as an intentional placeholder: if you intend future config-dependent specs,
retain the parameter and add a comment above exaone4_layer_spec explaining that
config is intentionally unused for now (e.g., "placeholder for future
config-dependent layer specs such as 32B hybrid attention"), otherwise change
the signature to def exaone4_layer_spec() -> ModuleSpec and update all call
sites that pass a config to call the new no-arg function.
- Line 126: Import DotProductAttention from
megatron.core.transformer.dot_product_attention and explicitly assign it to the
core_attention argument instead of leaving core_attention=None in ExaOne4
provider; update the import list in
src/megatron/bridge/models/exaone/exaone4_provider.py (matching the pattern used
in gpt_provider.py) and set core_attention=DotProductAttention where the ExaOne4
model is constructed so the provider uses the same explicit attention class as
Gemma2/Gemma3/MCore-based providers.

---

Nitpick comments:
In `@src/megatron/bridge/models/__init__.py`:
- Around line 296-298: The __all__ list is not alphabetically ordered: the
entries "Exaone4Bridge", "Exaone4ModelProvider", and "Exaone4ModelProvider1P2B"
are inserted between Qwen3 and DeepSeek blocks; move these three EXAONE entries
so they appear in alphabetical model-family order (i.e., place them before the
DeepSeek group or immediately after the Qwen entries) to restore consistent
ordering in __all__.

In `@src/megatron/bridge/models/exaone/exaone4_bridge.py`:
- Around line 37-53: The file defines logger and imports logging but never uses
them; either remove the unused import and the logger =
logging.getLogger(__name__) line, or replace any intended log calls with the
project convention print_rank_0. Specifically, remove the unused logging import
and the logger symbol from exaone4_bridge.py if no runtime messages are needed,
or add print_rank_0 calls (not logger) inside relevant bridge functions/classes
such as MegatronModelBridge-related methods, Exaone4ModelProvider hooks, or
PreTrainedCausalLM integration points to report key bridge events.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py`:
- Line 58: Remove the unused logger and its import: delete the standalone
"logger = logging.getLogger(__name__)" and the "import logging" statement in
exaone4_provider.py; if you need any runtime messages in functions/classes in
this file, replace uses of logger with the project convention print_rank_0(...)
instead (search for "logger" and the top-level import to update accordingly).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0b9a00 and fc798ba.

📒 Files selected for processing (4)
  • src/megatron/bridge/models/__init__.py
  • src/megatron/bridge/models/exaone/__init__.py
  • src/megatron/bridge/models/exaone/exaone4_bridge.py
  • src/megatron/bridge/models/exaone/exaone4_provider.py

Comment on lines +28 to +32
from megatron.bridge.models.exaone import (
Exaone4Bridge,
Exaone4ModelProvider,
Exaone4ModelProvider1P2B,
)

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.

🛠️ Refactor suggestion | 🟠 Major

Bridge file should be named model_bridge.py per project conventions.

The bridge implementation is in exaone4_bridge.py, but project conventions (established for all existing model bridges) require the file to be model_bridge.py. This import transitively surfaces that naming deviation.

Additionally, the exaone import block is inserted at lines 28–32 — before the deepseek block at line 33 — breaking the alphabetical ordering used across all other model imports in this file.

♻️ Suggested import ordering fix
-from megatron.bridge.models.exaone import (
-    Exaone4Bridge,
-    Exaone4ModelProvider,
-    Exaone4ModelProvider1P2B,
-)
 from megatron.bridge.models.deepseek import (
     ...
 )
+from megatron.bridge.models.exaone import (
+    Exaone4Bridge,
+    Exaone4ModelProvider,
+    Exaone4ModelProvider1P2B,
+)

Based on learnings: "Create model bridge implementation in 'model_bridge.py' when adding new model bridges" (applies to src/megatron/bridge/models/*/model_bridge.py).

📝 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
from megatron.bridge.models.exaone import (
Exaone4Bridge,
Exaone4ModelProvider,
Exaone4ModelProvider1P2B,
)
from megatron.bridge.models.deepseek import (
Deepseek4Bridge,
Deepseek4ModelProvider,
Deepseek4ModelProvider1P2B,
)
from megatron.bridge.models.exaone import (
Exaone4Bridge,
Exaone4ModelProvider,
Exaone4ModelProvider1P2B,
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/__init__.py` around lines 28 - 32, The bridge
implementation file must be renamed to follow project conventions: move/rename
the current exaone4_bridge.py into model_bridge.py and update any imports to
import Exaone4Bridge, Exaone4ModelProvider, and Exaone4ModelProvider1P2B from
model_bridge instead of exaone4_bridge; also fix the import ordering in
__init__.py so the Exaone* import block appears in strict alphabetical order
among other model imports (place the Exaone imports in the correct spot relative
to Deepseek and others) to match the established import ordering convention.

seq_length=hf_config.max_position_embeddings,
init_method_std=hf_config.initializer_range,
layernorm_epsilon=hf_config.rms_norm_eps,
rotary_base=getattr(hf_config, 'rope_theta', getattr(hf_config, 'rotary_base', 1000000.0)),

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.

⚠️ Potential issue | 🟡 Minor

Single-quote string literals violate the double-quote coding guideline.

Line 115 uses 'rope_theta' and 'rotary_base' with single quotes.

♻️ Proposed fix
-            rotary_base=getattr(hf_config, 'rope_theta', getattr(hf_config, 'rotary_base', 1000000.0)),
+            rotary_base=getattr(hf_config, "rope_theta", getattr(hf_config, "rotary_base", 1000000.0)),

As per coding guidelines: "Use double quotes for strings (matching ruff formatter configuration)."

📝 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
rotary_base=getattr(hf_config, 'rope_theta', getattr(hf_config, 'rotary_base', 1000000.0)),
rotary_base=getattr(hf_config, "rope_theta", getattr(hf_config, "rotary_base", 1000000.0)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_bridge.py` at line 115, The code
uses single-quoted string literals in the getattr call for rotary_base; change
the single quotes to double quotes so the line reads using double-quoted strings
for the attribute names (update the getattr(hf_config, "rope_theta",
getattr(hf_config, "rotary_base", 1000000.0)) invocation), preserving the same
logic and default value and leaving variable names like rotary_base and
hf_config unchanged.

Comment on lines +150 to +157
if provider.rope_scaling:
hf_config["rope_scaling"] = {
"rope_type": "llama3",
"factor": provider.rope_scaling_factor,
"low_freq_factor": 1.0,
"high_freq_factor": 4.0,
"original_max_position_embeddings": 8192,
}

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.

⚠️ Potential issue | 🟠 Major

Hard-coded RoPE scaling constants cause lossy megatron_to_hf_config round-trips.

low_freq_factor, high_freq_factor, and original_max_position_embeddings are not stored on the provider, so they are unconditionally emitted as 1.0, 4.0, and 8192 regardless of the source model's actual values. Any checkpoint with different values will produce a mis-configured HF config on the return path. Consider persisting these on the provider, or at minimum reading them back from a stored dict.

♻️ Suggested approach: store the full rope_scaling dict on the provider

In provider_bridge:

+        # Store full rope_scaling dict for lossless round-trip
         if hf_rope_scaling is not None and hf_rope_scaling.get("rope_type") == "llama3":
             provider.rope_scaling = True
             provider.rope_scaling_factor = hf_rope_scaling.get("factor", 16.0)
+            provider.rope_scaling_original_max_position_embeddings = hf_rope_scaling.get(
+                "original_max_position_embeddings", 8192
+            )
+            provider.rope_scaling_low_freq_factor = hf_rope_scaling.get("low_freq_factor", 1.0)
+            provider.rope_scaling_high_freq_factor = hf_rope_scaling.get("high_freq_factor", 4.0)

In megatron_to_hf_config:

         if provider.rope_scaling:
             hf_config["rope_scaling"] = {
                 "rope_type": "llama3",
                 "factor": provider.rope_scaling_factor,
-                "low_freq_factor": 1.0,
-                "high_freq_factor": 4.0,
-                "original_max_position_embeddings": 8192,
+                "low_freq_factor": provider.rope_scaling_low_freq_factor,
+                "high_freq_factor": provider.rope_scaling_high_freq_factor,
+                "original_max_position_embeddings": provider.rope_scaling_original_max_position_embeddings,
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_bridge.py` around lines 150 - 157,
The code unconditionally emits hard-coded RoPE scaling keys in the hf_config
block (low_freq_factor, high_freq_factor, original_max_position_embeddings)
causing lossy megatron_to_hf_config round-trips; update the logic to persist and
reuse the full rope_scaling dict on the provider (e.g., add a
provider.rope_scaling_dict or store provider.rope_scaling as the complete dict)
and change the hf_config assignment in exaone4_bridge (where
hf_config["rope_scaling"] is set) to read and copy values from that persisted
dict instead of using fixed literals; also update the
provider_bridge/megatron_to_hf_config code path to populate
provider.rope_scaling from incoming model config so the round-trip restores
original low_freq_factor, high_freq_factor, and
original_max_position_embeddings.


import logging
from dataclasses import dataclass
from typing import Callable, Optional, Union

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.

⚠️ Potential issue | 🟡 Minor

Remove unused Optional import (confirmed by static analysis).

Optional is imported but never referenced in this file.

♻️ Proposed fix
-from typing import Callable, Optional, Union
+from typing import Callable, Union
📝 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
from typing import Callable, Optional, Union
from typing import Callable, Union
🧰 Tools
🪛 Flake8 (7.3.0)

[error] 35-35: 'typing.Optional' imported but unused

(F401)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` at line 35, Remove the
unused Optional import from the typing import in exaone4_provider.py: update the
import line that currently reads "from typing import Callable, Optional, Union"
to drop Optional so it becomes "from typing import Callable, Union" (or
equivalent import ordering used in the file) to satisfy static analysis and
avoid the unused-import warning; ensure no other occurrences of Optional are
referenced in functions or type hints such as any in ExaOne4Provider-related
methods before committing.

Comment on lines +91 to +94
def forward(self, x):
"""Forward with Post-LN applied to the linear output."""
output, bias = super().forward(x)
return self.post_layernorm(output), bias

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.

⚠️ Potential issue | 🟡 Minor

Post-LayerNorm applied before bias addition may produce incorrect results when bias is present.

TERowParallelLinear.forward returns (output, bias), where bias is a deferred addend applied by the downstream BDA (bias-dropout-add) function — the output tensor does not yet have bias added. The current implementation applies self.post_layernorm(output) before bias is added to it. For EXAONE (where add_bias_linear=False and bias is always None) this is harmless, but any other model or configuration that reuses TERowParallelLinearLayerNorm with add_bias_linear=True will normalize a biasless output and then add bias outside the norm, which is incorrect.

Add a guard or document the invariant explicitly.

🛡️ Proposed defensive fix
     def forward(self, x):
         """Forward with Post-LN applied to the linear output."""
         output, bias = super().forward(x)
+        if bias is not None:
+            raise ValueError(
+                "TERowParallelLinearLayerNorm does not support deferred bias "
+                "(add_bias_linear must be False for Post-LN architectures)."
+            )
         return self.post_layernorm(output), bias
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` around lines 91 - 94,
TERowParallelLinearLayerNorm.forward currently calls super().forward(x) ->
(output, bias) then applies self.post_layernorm(output) before bias is added;
add a guard to prevent silently incorrect behavior by checking the returned
bias: either assert bias is None with a clear message (e.g., raise
AssertionError("TERowParallelLinearLayerNorm expects no deferred bias; set
add_bias_linear=False or handle bias before layernorm")), or if you want to
support bias, add bias to output before calling self.post_layernorm (i.e.,
output = output + bias) and return (normalized_output, None); make the change
inside TERowParallelLinearLayerNorm.forward and reference super().forward,
self.post_layernorm, and the bias variable.

# =============================================================================


def exaone4_layer_spec(config: "GPTModelProvider") -> ModuleSpec:

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.

⚠️ Potential issue | 🟡 Minor

Unused function argument config in exaone4_layer_spec (confirmed by static analysis).

The function builds a fully static ModuleSpec without referencing config at any point. If it is a forward-looking placeholder for config-dependent specs (e.g., 32B hybrid attention), document that intent with a comment; otherwise remove the parameter.

♻️ Proposed fix (if config is intentionally unused for now)
-def exaone4_layer_spec(config: "GPTModelProvider") -> ModuleSpec:
+def exaone4_layer_spec(_config: "GPTModelProvider") -> ModuleSpec:
📝 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
def exaone4_layer_spec(config: "GPTModelProvider") -> ModuleSpec:
def exaone4_layer_spec(_config: "GPTModelProvider") -> ModuleSpec:
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 102-102: Unused function argument: config

(ARG001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` at line 102, The
function exaone4_layer_spec currently declares a parameter config but never uses
it; either remove the unused parameter or clearly mark it as an intentional
placeholder: if you intend future config-dependent specs, retain the parameter
and add a comment above exaone4_layer_spec explaining that config is
intentionally unused for now (e.g., "placeholder for future config-dependent
layer specs such as 32B hybrid attention"), otherwise change the signature to
def exaone4_layer_spec() -> ModuleSpec and update all call sites that pass a
config to call the new no-arg function.

params={"attn_mask_type": AttnMaskType.causal},
submodules=SelfAttentionSubmodules(
linear_qkv=TEColumnParallelLinear, # No Pre-LN (pure Post-LN arch)
core_attention=None, # Use default DotProductAttention

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file mentioned in the review
find . -name "exaone4_provider.py" -type f

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 126


🏁 Script executed:

# Search for SelfAttentionSubmodules definition
rg -n "class SelfAttentionSubmodules" --type py -A 10

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 53


🏁 Script executed:

# Search for core_attention parameter usage and handling
rg -n "core_attention" --type py -B 3 -A 5 | head -100

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 8878


🏁 Script executed:

# Check how SelfAttention uses the core_attention parameter
rg -n "class SelfAttention" --type py -A 20

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 53


🏁 Script executed:

# View the context around line 126 in exaone4_provider.py
sed -n '110,150p' ./src/megatron/bridge/models/exaone/exaone4_provider.py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 1588


🏁 Script executed:

# Check imports and structure of exaone4_provider.py
head -50 ./src/megatron/bridge/models/exaone/exaone4_provider.py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 1966


🏁 Script executed:

# Search for how SelfAttentionSubmodules is imported or used
rg -n "SelfAttentionSubmodules" --type py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 1550


🏁 Script executed:

# Check for None handling in core_attention across the codebase
rg -n "core_attention.*=.*None" --type py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 204


🏁 Script executed:

# Check gemma2_provider.py usage of SelfAttentionSubmodules
sed -n '310,330p' ./src/megatron/bridge/models/gemma/gemma2_provider.py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 982


🏁 Script executed:

# Check gemma3_provider.py usage of SelfAttentionSubmodules
sed -n '210,230p' ./src/megatron/bridge/models/gemma/gemma3_provider.py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 1032


🏁 Script executed:

# Search for imports in exaone4_provider to see what attention classes are available
grep -n "import\|from" ./src/megatron/bridge/models/exaone/exaone4_provider.py | head -60

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 741


🏁 Script executed:

# Check if there's a DotProductAttention or default import in exaone4_provider.py
grep -n "DotProductAttention\|CoreAttention\|MCoreDot" ./src/megatron/bridge/models/exaone/exaone4_provider.py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 150


🏁 Script executed:

# Search all files for core_attention assignments to understand the pattern
rg -n "core_attention=" --type py -B 2 -A 1

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 4302


🏁 Script executed:

# Check if there are any tests that verify core_attention=None behavior
rg -n "core_attention.*None" --type py -B 3 -A 3

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 878


🏁 Script executed:

# Look for DotProductAttention or related classes in the codebase
rg -n "DotProductAttention|TEDotProductAttention" --type py

Repository: NVIDIA-NeMo/Megatron-Bridge

Length of output: 4100


Import and explicitly assign a DotProductAttention class to core_attention instead of relying on None handling.

All other models in the codebase (Gemma2, Gemma3, Ministral3, GPT) explicitly assign an attention class to core_attention (e.g., Gemma2DotProductAttention, Gemma3TEDotProductAttention, MCoreDotProductAttention). Exaone4 is the only model using core_attention=None, which assumes megatron-core's SelfAttention handles None by falling back to a default—a behavior that should be verified or explicitly implemented. Import DotProductAttention from megatron.core.transformer.dot_product_attention (like gpt_provider.py does) and assign it to core_attention to match the established pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` at line 126, Import
DotProductAttention from megatron.core.transformer.dot_product_attention and
explicitly assign it to the core_attention argument instead of leaving
core_attention=None in ExaOne4 provider; update the import list in
src/megatron/bridge/models/exaone/exaone4_provider.py (matching the pattern used
in gpt_provider.py) and set core_attention=DotProductAttention where the ExaOne4
model is constructed so the provider uses the same explicit attention class as
Gemma2/Gemma3/MCore-based providers.

rotary_percent: float = 1.0

# Custom layer spec for Post-LN architecture
transformer_layer_spec: Union[ModuleSpec, Callable[["GPTModelProvider"], ModuleSpec]] = exaone4_layer_spec

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.

⚠️ Potential issue | 🟡 Minor

Use X | Y union syntax instead of Union[X, Y] (Python 3.10+ requirement).

♻️ Proposed fix
-    transformer_layer_spec: Union[ModuleSpec, Callable[["GPTModelProvider"], ModuleSpec]] = exaone4_layer_spec
+    transformer_layer_spec: ModuleSpec | Callable[["GPTModelProvider"], ModuleSpec] = exaone4_layer_spec

As per coding guidelines: "Use X | Y for union types instead of Union[X, Y]" and "Conform code to Python 3.10+."

📝 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
transformer_layer_spec: Union[ModuleSpec, Callable[["GPTModelProvider"], ModuleSpec]] = exaone4_layer_spec
transformer_layer_spec: ModuleSpec | Callable[["GPTModelProvider"], ModuleSpec] = exaone4_layer_spec
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/megatron/bridge/models/exaone/exaone4_provider.py` at line 175, Change
the type annotation for transformer_layer_spec to use PEP 604 union syntax:
replace Union[ModuleSpec, Callable[["GPTModelProvider"], ModuleSpec]] with
ModuleSpec | Callable[["GPTModelProvider"], ModuleSpec] (symbol:
transformer_layer_spec, types: ModuleSpec, Callable, GPTModelProvider,
exaone4_layer_spec); also remove any now-unused Union import from typing.

@Bias92

Bias92 commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

Hi @yaoyu-33 — I hope you're doing well! I wanted to send a gentle ping on this PR. It adds HuggingFace ↔ Megatron checkpoint conversion support for EXAONE 4.0 (LG AI Research), following the existing bridge pattern.

The CI checks are currently waiting for workflow approval from a vetter. Would you be able to take a look and trigger the CI when you get a chance? I'd greatly appreciate any feedback. Thank you!

@Bias92 Bias92 closed this Mar 7, 2026
@Bias92 Bias92 reopened this Mar 13, 2026


@dataclass
class Exaone4ModelProvider(GPTModelProvider):

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.

is there an existing model where these values are different from this default? If not we should write these as default values in the model building blocks. Ideally we can remove the model specific provider if not necessary

@weijiac0619

weijiac0619 commented May 11, 2026

Copy link
Copy Markdown
Contributor

Sorry for the delay.
Could you add a readme page and runs scripts following example here? https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/examples/models/minimax_m2

Can you also add a functional test and unit test following this example https://github.com/NVIDIA-NeMo/Megatron-Bridge/pull/2602/changes



class TERowParallelLinearLayerNorm(TERowParallelLinear):
"""Row-parallel linear with an additional Post-LayerNorm on the output.

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.

TERowParallelLinearLayerNorm is already provided in src/megatron/bridge/models/gemma/gemma2_provider.py. could you please take a look to see whether it's different from EXAONE 4.0? if they are similar, it could be pulled out to a common modules file and make both models import the class from there

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label May 12, 2026
@yaoyu-33 yaoyu-33 added area:model Model implementations and HF bridge logic feature New capabilities, enhancements, or enablement work labels May 12, 2026
@Bias92

Bias92 commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

Sorry for the delay. Could you add a readme page and runs scripts following example here? https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/examples/models/minimax_m2

Can you also add a functional test and unit test following this example https://github.com/NVIDIA-NeMo/Megatron-Bridge/pull/2602/changes

thank you for taking the time to review this PR , i'll add the README/scripts and tests. for TERowParallelLinearLayerNorm I'll check gemma2's version and move it to a common module if it's the same.
the provider does have some EXAONE-specific stuff (qk_layernorm, tied embeddings, post-LN layer spec),, but I'll clean up anything redundant.

@Bias92

Bias92 commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

I checked the existing Gemma implementations.
Gemma2 and Gemma3 both define TERowParallelLinearLayerNorm with the same implementation, and Gemma4 already reuses Gemma3’s definition rather than defining another copy.
given that, I agree a shared module is cleaner than having EXAONE import from Gemma directly. I’ll move the common class into a shared module and update Gemma2/Gemma3/Gemma4/EXAONE imports in this PR, unless you’d prefer that refactor to be split out.

@Bias92

Bias92 commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

(additional) i will add the README page, run scripts, and tests , working on it now.

Bias92 added 5 commits May 14, 2026 22:28
Move duplicated TERowParallelLinearLayerNorm class into
models/common/te_layers.py and update Gemma2, Gemma3, and EXAONE
imports. No behavior change on the normal no-bias path; adds a
defensive assertion for deferred bias.

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
Signed-off-by: Bias92 <pewpewplay315@gmail.com>
Signed-off-by: Bias92 <pewpewplay315@gmail.com>
Signed-off-by: Bias92 <pewpewplay315@gmail.com>
Drop two fields from Exaone4ModelProvider that duplicate parent defaults:
- share_embeddings_and_output_weights (parent: True)
- rotary_percent (parent: 1.0)

Per reviewer feedback on PR NVIDIA-NeMo#2532.

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
@weijiac0619

Copy link
Copy Markdown
Contributor

Thank you for the quick update. By the way, We have just released https://github.com/NVIDIA-NeMo/Megatron-Bridge/tree/main/skills/adding-model-support in the repo. Feel free to check out SKILL.md, llm-patterns.md, and tests-and-examples.md for the expected patterns and checklist using any AI tool. hope that also help model support work in the future.

@weijiac0619

Copy link
Copy Markdown
Contributor

Hi @Bias92 , let me know if it's ready for review. thank you for the input

@Bias92

Bias92 commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

oh sorry, i was busy for while. i will take a look , today.

@weijiac0619

Copy link
Copy Markdown
Contributor

no hurry. Just ping me when it's ready for review.

Bias92 added 2 commits June 11, 2026 22:04
…ling test

The llama3-style rope scaling fields (low_freq_factor, high_freq_factor,
original_max_position_embeddings) were dataclass fields on the removed
Exaone4ModelProvider but are not declared on GPTModelProvider, so passing
them to the constructor raises TypeError and fails Launch_Unit_Tests_Core.
Set them as plain attributes instead, mirroring provider_bridge.

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
…bias guard

- Remove the duplicated "Exaone4Bridge" entry from models/__init__.py
  __all__ (the alphabetically ordered entry is kept)
- Add the NVIDIA Apache 2.0 header to models/exaone/__init__.py
- Replace assert with ValueError in TERowParallelLinearLayerNorm.forward
  so the bias guard survives python -O

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
@Bias92

Bias92 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

oh sorry i'm just back. thanks for the progress

@ko3n1g

ko3n1g commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

/ok to test 8177979

Bias92 added 2 commits June 13, 2026 04:10
Signed-off-by: Bias92 <pewpewplay315@gmail.com>
Covers the deferred-bias ValueError branch in te_layers.py (the only uncovered line in the PR diff) and gives the shared module its own test file.

Signed-off-by: Bias92 <pewpewplay315@gmail.com>
@Bias92

Bias92 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

updated branch + added the missing test. codecov thing was a stale base issue. can you rerun ci?

@cuichenx

Copy link
Copy Markdown
Contributor

/ok to test 45afbe5

@cuichenx cuichenx added ready-to-merge PR is approved, current, and only waiting for CI to pass before merge and removed waiting-on-customer Waiting on the original author to respond labels Jun 12, 2026
@gautham-kollu

Copy link
Copy Markdown
Contributor

Rerunning codecov as it might running bundling with other PRs

@gautham-kollu

Copy link
Copy Markdown
Contributor

/ok to test 45afbe5

@yaoyu-33
yaoyu-33 merged commit da26d77 into NVIDIA-NeMo:main Jun 19, 2026
75 of 76 checks passed
@Bias92

Bias92 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

thx for the merge!

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

Labels

area:model Model implementations and HF bridge logic community-request feature New capabilities, enhancements, or enablement work ready-to-merge PR is approved, current, and only waiting for CI to pass before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants