-
Notifications
You must be signed in to change notification settings - Fork 536
feat(megatron-bridge): adopt TEGroupedMLP per-expert quantizers (#1550) for W4A16 NVFP4 four_over_six #2072
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
87bab30
b6659bc
d2b46a4
e2742fc
2883da0
5b8f9d7
4708d85
8902e04
8bd4685
39074f2
761d1c1
57fe963
790a871
2b0d5ea
72d3cb5
972a57a
c53ee30
5f2f3de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,9 @@ | |||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import argparse | ||||||||||||||||||||||||||||
| import yaml | ||||||||||||||||||||||||||||
| import pathlib | ||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import torch | ||||||||||||||||||||||||||||
| from megatron.bridge.models.hf_pretrained.utils import is_safe_repo | ||||||||||||||||||||||||||||
|
|
@@ -44,6 +47,10 @@ | |||||||||||||||||||||||||||
| import modelopt.torch.utils.distributed as dist | ||||||||||||||||||||||||||||
| from modelopt.torch.export import export_mcore_gpt_to_hf | ||||||||||||||||||||||||||||
| from modelopt.torch.utils import print_args, print_rank_0 | ||||||||||||||||||||||||||||
| from modelopt.torch.quantization.nn.modules.tensor_quantizer import ( | ||||||||||||||||||||||||||||
| StaticBlockScaleQuantizer, | ||||||||||||||||||||||||||||
| TensorQuantizer, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| from modelopt.torch.utils.plugins.mbridge import ( | ||||||||||||||||||||||||||||
| load_mbridge_model_from_hf, | ||||||||||||||||||||||||||||
| load_modelopt_megatron_checkpoint, | ||||||||||||||||||||||||||||
|
|
@@ -52,6 +59,13 @@ | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def get_args() -> argparse.Namespace: | ||||||||||||||||||||||||||||
| parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) | ||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||
| "--grouped_experts", | ||||||||||||||||||||||||||||
| action="store_true", | ||||||||||||||||||||||||||||
| help="Build MoE experts grouped (GroupedMLP). Default is non-grouped, which per-block " | ||||||||||||||||||||||||||||
| "NVFP4 checkpoints require. Set this only when the checkpoint was saved with grouped " | ||||||||||||||||||||||||||||
| "experts; the layout must match or the weights will not load.", | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||
| "--hf_model_name_or_path", | ||||||||||||||||||||||||||||
| type=str, | ||||||||||||||||||||||||||||
|
|
@@ -99,7 +113,39 @@ def get_args() -> argparse.Namespace: | |||||||||||||||||||||||||||
| return args | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _provider_overrides_from_checkpoint(megatron_path: str) -> dict: | ||||||||||||||||||||||||||||
| """Read ``mtp_num_layers`` from the checkpoint so the exporter matches how it was saved. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Only ``mtp_num_layers`` is taken from here. ``moe_grouped_gemm`` is deliberately NOT derived: | ||||||||||||||||||||||||||||
| for a ``MambaModelProvider`` the expert layout is set by ``mamba_stack_spec``, so a checkpoint | ||||||||||||||||||||||||||||
| saved with non-grouped experts still records ``moe_grouped_gemm: true`` and trusting it would | ||||||||||||||||||||||||||||
| build a mismatched model. | ||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||
| defaults = {"mtp_num_layers": 0} | ||||||||||||||||||||||||||||
| run_config = next(iter(sorted(pathlib.Path(megatron_path).glob("*/run_config.yaml"))), None) | ||||||||||||||||||||||||||||
| if run_config is None: | ||||||||||||||||||||||||||||
| run_config = pathlib.Path(megatron_path) / "run_config.yaml" | ||||||||||||||||||||||||||||
|
Comment on lines
+125
to
+127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Inspect the iter_* resolution used by the Megatron checkpoint loader.
set -euo pipefail
fd -t f 'mbridge.py' modelopt | xargs -r rg -n -C 5 '_get_modelopt_checkpoint_path|iter_'
rg -n -C 5 'def _get_modelopt_checkpoint_path' --glob '*.py'Repository: NVIDIA/Model-Optimizer Length of output: 2401 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exporter ---'
cat -n examples/megatron_bridge/export_quantized_megatron_to_hf.py | sed -n '95,145p'
printf '%s\n' '--- loader call sites and definitions ---'
rg -n -C 12 'load_modelopt_megatron_checkpoint|_get_modelopt_checkpoint_path|def _get_modelopt_checkpoint_path' \
modelopt examples --glob '*.py'
printf '%s\n' '--- iteration directory fixtures/usages ---'
rg -n -C 4 'iter_[0-9]+|run_config\.yaml' modelopt examples --glob '*.py' --glob '*.yaml'Repository: NVIDIA/Model-Optimizer Length of output: 29061 🌐 Web query:
💡 Result: The function Citations:
🌐 Web query:
💡 Result: The term get_modelopt_checkpoint_path does not appear to be a standard, publicly documented public API function within the NVIDIA Model Optimizer (ModelOpt) or the NVIDIA NeMo/Megatron-Bridge libraries. Based on an analysis of the NVIDIA NeMo/Megatron-Bridge repository [1][2][3], interactions with ModelOpt checkpoints typically involve: 1. Manual Directory Specification: Scripts generally expect users to provide a path to the checkpoint directory directly via arguments (e.g., --megatron-save-path or --pretrained-checkpoint) [3][4][5]. 2. Internal Logic: The codebase contains logic within internal utility modules (such as src/megatron/bridge/training/post_training/checkpointing.py) that handles the identification and selection of ModelOpt checkpoints [6]. This logic often involves checking for specific configuration files like run_config.yaml or iterating through subdirectories labeled iter [1][2]. 3. Model Configuration: Quantization configurations are typically handled through functions like get_modelopt_torch_quantization_config [4] or by defining model specifications using tools like get_gpt_modelopt_spec [7]. If you encountered this name in a specific codebase, it is likely an internal helper function or a custom utility specific to that project rather than a public function provided by NVIDIA ModelOpt. You may wish to search for the definition of this function within your local project files to understand its specific implementation and usage. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
url='https://raw.githubusercontent.com/NVIDIA-NeMo/Megatron-Bridge/main/src/megatron/bridge/training/post_training/checkpointing.py'
curl -fsSL "$url" | rg -n -C 20 '_get_modelopt_checkpoint_path|iter_|latest_train_state|run_config'Repository: NVIDIA/Model-Optimizer Length of output: 5010 🏁 Script executed: #!/bin/bash
set -euo pipefail
url='https://raw.githubusercontent.com/NVIDIA-NeMo/Megatron-Bridge/main/src/megatron/bridge/training/checkpointing.py'
curl -fsSL "$url" | rg -n -C 25 '_resolve_checkpoint_iteration|_DIRECT_ITERATION_DIR_SENTINEL|latest_train_state|ckpt_step'
printf '%s\n' '--- exporter path construction ---'
cat -n examples/megatron_bridge/export_quantized_megatron_to_hf.py | sed -n '145,180p'
rg -n -C 8 'megatron_path|checkpoint_export_paths' examples/megatron_bridge/export_quantized_megatron_to_hf.pyRepository: NVIDIA/Model-Optimizer Length of output: 28745 Resolve 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| if not run_config.exists(): | ||||||||||||||||||||||||||||
| print_rank_0(f"No run_config.yaml under {megatron_path}; using defaults {defaults}.") | ||||||||||||||||||||||||||||
| return defaults | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| cfg = yaml.safe_load(run_config.read_text()) or {} | ||||||||||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||||||||||
| print_rank_0(f"Could not parse {run_config} ({exc}); using defaults {defaults}.") | ||||||||||||||||||||||||||||
| return defaults | ||||||||||||||||||||||||||||
| model_cfg = cfg.get("model") if isinstance(cfg.get("model"), dict) else cfg | ||||||||||||||||||||||||||||
| resolved = { | ||||||||||||||||||||||||||||
| key: model_cfg.get(key, default) | ||||||||||||||||||||||||||||
| for key, default in defaults.items() | ||||||||||||||||||||||||||||
| if isinstance(model_cfg, dict) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| resolved = {**defaults, **resolved} | ||||||||||||||||||||||||||||
|
Comment on lines
+136
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win An explicit If The ♻️ Proposed fix model_cfg = cfg.get("model") if isinstance(cfg.get("model"), dict) else cfg
- resolved = {
- key: model_cfg.get(key, default)
- for key, default in defaults.items()
- if isinstance(model_cfg, dict)
- }
- resolved = {**defaults, **resolved}
+ resolved = dict(defaults)
+ if isinstance(model_cfg, dict):
+ for key, default in defaults.items():
+ value = model_cfg.get(key)
+ resolved[key] = default if value is None else value📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| print_rank_0(f"Model shape from {run_config.name}: {resolved}") | ||||||||||||||||||||||||||||
| return resolved | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def main(args: argparse.Namespace): | ||||||||||||||||||||||||||||
| _ckpt_shape = _provider_overrides_from_checkpoint(args.megatron_path) | ||||||||||||||||||||||||||||
| trust_remote_code = is_safe_repo( | ||||||||||||||||||||||||||||
| trust_remote_code=args.trust_remote_code, hf_path=args.hf_model_name_or_path | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
@@ -116,8 +162,11 @@ def main(args: argparse.Namespace): | |||||||||||||||||||||||||||
| "num_layers_in_first_pipeline_stage": args.num_layers_in_first_pipeline_stage, | ||||||||||||||||||||||||||||
| "num_layers_in_last_pipeline_stage": args.num_layers_in_last_pipeline_stage, | ||||||||||||||||||||||||||||
| "pipeline_dtype": torch.bfloat16, | ||||||||||||||||||||||||||||
| "mtp_num_layers": _ckpt_shape["mtp_num_layers"], | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| init_model_parallel=True, | ||||||||||||||||||||||||||||
| # Default non-grouped, matching quantize.py; the layout must match the checkpoint. | ||||||||||||||||||||||||||||
| moe_grouped_gemm=args.grouped_experts, | ||||||||||||||||||||||||||||
| load_weights=False, # The weights come from the Megatron checkpoint, so HF weights are not loaded | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -127,6 +176,59 @@ def main(args: argparse.Namespace): | |||||||||||||||||||||||||||
| load_modelopt_megatron_checkpoint(model, args.megatron_path) | ||||||||||||||||||||||||||||
| unwrapped_model = unwrap_model(model[0]) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Static-NVFP4 export guard. | ||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||
| # An *enabled* NVFP4 weight quantizer that reaches the exporter without its calibrated scales | ||||||||||||||||||||||||||||
| # means the values stored in the checkpoint were not restored. Exporting such a weight silently | ||||||||||||||||||||||||||||
| # falls back to BF16: the result is larger than the recipe specifies and no longer matches it, | ||||||||||||||||||||||||||||
| # with nothing in the logs to say so. Fail loudly instead. | ||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||
| # Static-block NVFP4 needs BOTH ``_amax`` (per block) and ``_global_amax`` (per tensor). A | ||||||||||||||||||||||||||||
| # missing ``_global_amax`` slips past an ``_amax``-only check and then fails much later inside | ||||||||||||||||||||||||||||
| # ``NVFP4QTensor.quantize``, where ``scale * scale_2`` broadcasts [N, 1] against [N] into an | ||||||||||||||||||||||||||||
| # N x N allocation. Naming the attribute here turns that into an actionable message. | ||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||
| # Set MODELOPT_ALLOW_UNCALIBRATED_NVFP4=1 to keep the previous behavior (disable the quantizer | ||||||||||||||||||||||||||||
| # and emit BF16), which is then reported rather than silent. | ||||||||||||||||||||||||||||
| uncalibrated: list[tuple[str, str]] = [] | ||||||||||||||||||||||||||||
| for name, module in unwrapped_model.named_modules(): | ||||||||||||||||||||||||||||
| # StaticBlockScaleQuantizer must be INCLUDED: `_global_amax` is defined on it, so excluding | ||||||||||||||||||||||||||||
| # it would skip exactly the case this guard exists to catch. Only report enabled quantizers, | ||||||||||||||||||||||||||||
| # matching the message. | ||||||||||||||||||||||||||||
| if not isinstance(module, TensorQuantizer) or not getattr(module, "is_enabled", False): | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| block_sizes = getattr(module, "_block_sizes", None) | ||||||||||||||||||||||||||||
| is_nvfp4 = getattr(module, "_num_bits", None) == (2, 1) and ( | ||||||||||||||||||||||||||||
| isinstance(block_sizes, dict) and block_sizes.get("scale_bits") == (4, 3) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| if not is_nvfp4: | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| if getattr(module, "_amax", None) is None: | ||||||||||||||||||||||||||||
| uncalibrated.append((name, "_amax")) | ||||||||||||||||||||||||||||
| elif ( | ||||||||||||||||||||||||||||
| isinstance(module, StaticBlockScaleQuantizer) | ||||||||||||||||||||||||||||
| or block_sizes.get("type") == "static" | ||||||||||||||||||||||||||||
| ) and getattr(module, "_global_amax", None) is None: | ||||||||||||||||||||||||||||
| uncalibrated.append((name, "_global_amax")) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if uncalibrated: | ||||||||||||||||||||||||||||
| detail = ", ".join(f"{name}.{attr}" for name, attr in uncalibrated[:8]) | ||||||||||||||||||||||||||||
| if len(uncalibrated) > 8: | ||||||||||||||||||||||||||||
| detail += ", ..." | ||||||||||||||||||||||||||||
| message = ( | ||||||||||||||||||||||||||||
| f"{len(uncalibrated)} enabled NVFP4 weight quantizer(s) are missing calibrated scales " | ||||||||||||||||||||||||||||
| f"after loading {args.megatron_path}: {detail}. These weights would be exported as " | ||||||||||||||||||||||||||||
| "BF16 instead of NVFP4. Re-run PTQ with a ModelOpt that saves and restores this " | ||||||||||||||||||||||||||||
| "quantizer state, or set MODELOPT_ALLOW_UNCALIBRATED_NVFP4=1 to export them as BF16." | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| if os.environ.get("MODELOPT_ALLOW_UNCALIBRATED_NVFP4") != "1": | ||||||||||||||||||||||||||||
| raise RuntimeError(message) | ||||||||||||||||||||||||||||
| print_rank_0(f"WARNING (MODELOPT_ALLOW_UNCALIBRATED_NVFP4=1): {message}") | ||||||||||||||||||||||||||||
| for name, _ in uncalibrated: | ||||||||||||||||||||||||||||
| unwrapped_model.get_submodule(name).disable() | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| print_rank_0("All enabled NVFP4 weight quantizers have calibrated scales.") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Extra modules (Medusa / EAGLE / MTP) only exist on the last pipeline stage. Use an all-reduce | ||||||||||||||||||||||||||||
| # MAX over all ranks (rather than a broadcast from a hard-coded source rank) so the decision is | ||||||||||||||||||||||||||||
| # correct regardless of pipeline placement / global rank ordering. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -608,6 +608,52 @@ def _set_input_tensor(self, input_tensors: list[Tensor]): | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # HACK: Concatenate output tensors when PP>1 so they can be passed between ranks. | ||||||||||||||||||||||||||||||
| def _forward(self, *args, **kwargs): | ||||||||||||||||||||||||||||||
| # Static-block NVFP4: promote the student's weight quantizers once, after the | ||||||||||||||||||||||||||||||
| # checkpoint amax/scales have been loaded, so the training forward takes the | ||||||||||||||||||||||||||||||
| # StaticBlockScaleQuantizer path rather than the generic FP8 (E4M3) path. Promotion | ||||||||||||||||||||||||||||||
| # cannot happen at build time because the scales only exist after the load. | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # NOTE: in practice this converts exactly ONE module -- ``output_layer``. A measured run | ||||||||||||||||||||||||||||||
| # reports ``already promoted 460, converted 1, skipped 0``: every other quantizer is | ||||||||||||||||||||||||||||||
| # already a StaticBlockScaleQuantizer by the time training starts. So this is a workaround | ||||||||||||||||||||||||||||||
| # for output_layer being the one module the restore path does not promote (the same | ||||||||||||||||||||||||||||||
| # asymmetry behind its weight-quantizer scales not being restored). The better fix is to | ||||||||||||||||||||||||||||||
| # promote it on the normal path; until then, without this block the output projection | ||||||||||||||||||||||||||||||
| # would train through the generic FP8 path instead of static-block NVFP4. | ||||||||||||||||||||||||||||||
| if not getattr(self, "_modelopt_nvfp4_promoted", False): | ||||||||||||||||||||||||||||||
| from modelopt.torch.quantization.nn.modules.tensor_quantizer import ( | ||||||||||||||||||||||||||||||
| StaticBlockScaleQuantizer, | ||||||||||||||||||||||||||||||
| TensorQuantizer, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| n_promoted = n_skipped = 0 | ||||||||||||||||||||||||||||||
| for name, module in self.named_modules(): | ||||||||||||||||||||||||||||||
| if not isinstance(module, TensorQuantizer) or isinstance( | ||||||||||||||||||||||||||||||
| module, StaticBlockScaleQuantizer | ||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
|
Comment on lines
+630
to
+634
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win The scan traverses the teacher, not only the student.
A BF16 teacher carries no NVFP4 quantizers, so nothing changes today. A quantized teacher would be mutated silently. Scope the traversal to the student. 🛠️ Proposed fix n_promoted = n_skipped = 0
- for name, module in self.named_modules():
+ teacher_modules = {id(m) for m in self._teacher_model.modules()}
+ for name, module in self.named_modules():
+ if id(module) in teacher_modules:
+ continue
if not isinstance(module, TensorQuantizer) or isinstance(
module, StaticBlockScaleQuantizer
):
continue📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| block_sizes = getattr(module, "_block_sizes", None) | ||||||||||||||||||||||||||||||
| is_nvfp4 = getattr(module, "_num_bits", None) == (2, 1) and ( | ||||||||||||||||||||||||||||||
| isinstance(block_sizes, dict) and block_sizes.get("scale_bits") == (4, 3) | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| if not is_nvfp4: | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| amax = getattr(module, "_amax", None) | ||||||||||||||||||||||||||||||
| if amax is None: | ||||||||||||||||||||||||||||||
| # Uncalibrated: leave it alone rather than silently changing precision. | ||||||||||||||||||||||||||||||
| logger.warning(f"NVFP4 weight quantizer {name} has no _amax; not promoted.") | ||||||||||||||||||||||||||||||
| n_skipped += 1 | ||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||
| StaticBlockScaleQuantizer.from_tensor_quantizer( | ||||||||||||||||||||||||||||||
| module, global_amax=amax.detach().float().abs().max() | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| n_promoted += 1 | ||||||||||||||||||||||||||||||
| if n_promoted or n_skipped: | ||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||
| f"Promoted {n_promoted} NVFP4 weight quantizer(s) to " | ||||||||||||||||||||||||||||||
| f"StaticBlockScaleQuantizer ({n_skipped} skipped)." | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| self._modelopt_nvfp4_promoted = True | ||||||||||||||||||||||||||||||
| with torch.no_grad(): | ||||||||||||||||||||||||||||||
| self._teacher_model.eval() | ||||||||||||||||||||||||||||||
| teacher_output = self._teacher_model(*args, **kwargs) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a copy from my PR #1550, you can remove this as you will rebase on my later