Skip to content

[v1] refactor registry plugin structure and params#10641

Open
jiaqiw09 wants to merge 2 commits into
hiyouga:mainfrom
jiaqiw09:fix_plugin
Open

[v1] refactor registry plugin structure and params#10641
jiaqiw09 wants to merge 2 commits into
hiyouga:mainfrom
jiaqiw09:fix_plugin

Conversation

@jiaqiw09

Copy link
Copy Markdown
Collaborator

What does this PR do?

The previous plugin registration mechanism managed both plugin implementations and their parameter types, which tightly coupled plugin discovery, configuration parsing, and invocation. Some plugin families also maintained separate registries or hubs.

This PR unifies the v1 plugin registration mechanism and moves parameter parsing to the actual plugin entry points.

Main Changes

1. Unified plugin registration

  • BasePlugin now maintains an independent registry for each plugin family, preventing registry data from being shared across PEFT, Kernel, Batching, and Distributed plugins.
  • Two implementation styles are supported:
    • A method can be registered directly for plugins that provide a single operation.
    • A class can be registered for plugins that provide a group of related operations. Static methods on the class expose the corresponding interface.
  • Kernel, batching, and distributed plugins now use this common mechanism, replacing their separate registry or hub implementations.

2. Parse parameters at plugin entry points

Plugin parameters are explicitly parsed through the corresponding plugin family’s parse_params() method where the plugin is actually invoked.

For example, LoRA parameters are parsed at the PEFT entry point:

peft_config = PeftPlugin.parse_params(peft_config, LoraParams)

This keeps the registry focused on plugin implementations, while each plugin family owns its parameter parsing and each entry point controls when parsing occurs. Quantization and distributed plugins follow the same pattern.

3. Kernel plugin updates

  • Kernel implementations are registered and resolved through KernelPlugin.
  • The standalone kernel registry has been removed.
  • The kernel base class now provides a unified dependency-checking and application flow.
  • Existing CUDA, NPU, Liger, and auto kernel behavior is preserved; only registration and invocation have been reorganized.
  • Kernel configuration can select either a single kernel or multiple kernels.

4. Batching plugin updates

  • Padding-free, dynamic batching, and dynamic padding-free implementations are now registered as batching plugins.
  • Batching implementations with multiple processing stages use class registration and group related operations as static methods.
  • This change focuses on registration and code organization without altering the existing batching behavior.

5. Distributed plugin updates

  • FSDP2 and DeepSpeed now use the unified distributed plugin registration mechanism.
  • Each backend is registered as a class that groups operations such as model sharding, state saving, and state loading.
  • The previous distributed hub has been removed, and plugin selection and invocation are now handled through DistributedInterface.
  • Backend-specific FSDP2 and DeepSpeed settings remain in dist_config.

6. Distributed parallelism parameters

Common parallel topology settings are no longer stored in backend-specific dist_config. They are managed directly by TrainingArguments and passed to the distributed interface:

  • dp_size
  • cp_size
  • cp_mode
  • mp_replicate_size
  • mp_shard_size
  • dist_timeout

This separates general parallelism topology from backend-specific configuration.

Sequence Parallel keeps its existing directory and module structure, with cp_size now passed explicitly at the call site.

7. Configuration and example updates

  • Updated v1 examples to remove obsolete plugin parameter registration fields.
  • Moved Sequence Parallel topology settings to their new locations.
  • Updated related test configurations and plugin invocation paths.

Before submitting

@jiaqiw09

jiaqiw09 commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

@frozenleaves @sunyi0505 @Kuangdd01 @codemayq @hiyouga It's a large pr to refactor plugin and params and influence many parts. Would you mind having a check? Please feel free to comment and I will try to fix issues.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the plugin architecture and configuration parsing in LlamaFactory (v1), introducing typed dataclasses for PEFT, quantization, and distributed training configurations to replace unstructured dictionaries. It simplifies the BasePlugin and BaseKernel interfaces, explicitly registers model kernels, and promotes distributed parameters (like context parallel size) directly to TrainingArguments. The review feedback highlights several robustness issues, including potential AttributeError and TypeError crashes if configurations are mixed between dictionaries and dataclasses, fragile device type checks, a potential crash when sorting mixed-type keys in plugin parameter parsing, and missing fallback handling for null compute_dtype values.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

"""Returns the unique identifier for the kernel."""
return cls._kernel_id
def check_deps(cls) -> None:
supported = cls._device if isinstance(cls._device, list) else [cls._device]

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.

medium

Using isinstance(cls._device, list) is fragile because subclasses might define _device as a tuple (e.g., (DeviceType.CUDA, DeviceType.NPU)). If a tuple is used, supported will become a single-element list containing the tuple, which will cause dependency checks to fail. Using isinstance(cls._device, (list, tuple)) is more robust.

Suggested change
supported = cls._device if isinstance(cls._device, list) else [cls._device]
supported = cls._device if isinstance(cls._device, (list, tuple)) else [cls._device]

if unknown:
raise ValueError(
f"Unknown params for {cls.__name__}.{params_cls.__name__}: {sorted(unknown)}. "
f"Expected: {sorted(known)}"

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.

medium

If unknown contains mixed types (e.g., strings and integers/booleans from YAML keys), calling sorted(unknown) will raise a TypeError in Python 3. Converting all keys to strings before sorting prevents this crash.

Suggested change
f"Expected: {sorted(known)}"
f"Unknown params for {cls.__name__}.{params_cls.__name__}: {sorted(str(k) for k in unknown)}. "

if dist_config is None or getattr(dist_config, "name", None) != "deepspeed":
return False

config_file = dist_config.get("config_file")

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.

medium

If dist_config is passed as a dataclass or custom object (like DeepSpeedParams) instead of a dict/PluginConfig, calling .get() will raise an AttributeError. Using getattr or checking for hasattr(dist_config, 'get') makes it robust to both dict-like and object-like configurations.

Suggested change
config_file = dist_config.get("config_file")
config_file = dist_config.get("config_file") if hasattr(dist_config, "get") else getattr(dist_config, "config_file", None)

def setup_deepspeed_zero3_model_loading():
"""Enable ZeRO-3-aware model loading for the registered backend config."""
dist_config = _registered_dist_config
config_file = dist_config.get("config_file") if dist_config is not None else 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.

medium

If dist_config is passed as a dataclass or custom object (like DeepSpeedParams) instead of a dict/PluginConfig, calling .get() will raise an AttributeError. Using getattr or checking for hasattr(dist_config, 'get') makes it robust to both dict-like and object-like configurations.

Suggested change
config_file = dist_config.get("config_file") if dist_config is not None else None
config_file = (dist_config.get("config_file") if hasattr(dist_config, "get") else getattr(dist_config, "config_file", None)) if dist_config is not None else None

Comment on lines +49 to +55
if isinstance(self.compute_dtype, str):
dtype = getattr(torch, self.compute_dtype, None)
if not isinstance(dtype, torch.dtype):
raise ValueError(f"compute_dtype={self.compute_dtype!r} is not a torch dtype name.")
self.compute_dtype = dtype
elif not isinstance(self.compute_dtype, torch.dtype):
raise TypeError(f"compute_dtype must be str or torch.dtype, got {type(self.compute_dtype).__name__}.")

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.

medium

If self.compute_dtype is explicitly set to None (e.g., compute_dtype: null in YAML), __post_init__ will raise a TypeError. Handling None by defaulting to torch.float16 improves usability and backward compatibility.

Suggested change
if isinstance(self.compute_dtype, str):
dtype = getattr(torch, self.compute_dtype, None)
if not isinstance(dtype, torch.dtype):
raise ValueError(f"compute_dtype={self.compute_dtype!r} is not a torch dtype name.")
self.compute_dtype = dtype
elif not isinstance(self.compute_dtype, torch.dtype):
raise TypeError(f"compute_dtype must be str or torch.dtype, got {type(self.compute_dtype).__name__}.")
if self.compute_dtype is None:
self.compute_dtype = torch.float16
if isinstance(self.compute_dtype, str):
dtype = getattr(torch, self.compute_dtype, None)
if not isinstance(dtype, torch.dtype):
raise ValueError(f"compute_dtype={self.compute_dtype!r} is not a torch dtype name.")
self.compute_dtype = dtype
elif not isinstance(self.compute_dtype, torch.dtype):
raise TypeError(f"compute_dtype must be str or torch.dtype, got {type(self.compute_dtype).__name__}.")

if export_config.get("name") == "lora":
adapters = export_config.get("adapter_name_or_path")
else:
if raw_config.name != "lora":

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.

medium

If raw_config is a standard dictionary (e.g., when called programmatically or in custom scripts), raw_config.name will raise an AttributeError. Using getattr(raw_config, 'name', None) is safer and more robust.

Suggested change
if raw_config.name != "lora":
if getattr(raw_config, "name", None) != "lora":

@@ -93,9 +93,12 @@ def __init__(
dist_name = self.args.dist_config.name if self.args.dist_config is not None else 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.

medium

If self.args.dist_config is a standard dictionary, self.args.dist_config.name will raise an AttributeError. Using getattr(self.args.dist_config, 'name', None) is safer and more robust.

Suggested change
dist_name = self.args.dist_config.name if self.args.dist_config is not None else None
dist_name = getattr(self.args.dist_config, "name", None)

from ..plugins.trainer_plugins.distributed.interface import DistributedPlugin

self.model = DistributedPlugin(self.args.dist_config.name)(
self.model = DistributedPlugin(self.args.dist_config.name).shard_model(

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.

medium

If self.args.dist_config is a standard dictionary, self.args.dist_config.name will raise an AttributeError. Using getattr(self.args.dist_config, 'name', None) is safer and more robust.

Suggested change
self.model = DistributedPlugin(self.args.dist_config.name).shard_model(
self.model = DistributedPlugin(getattr(self.args.dist_config, "name", None)).shard_model(

Comment on lines 347 to 350
if self.args.dist_config is not None and self.args.dist_config.name in ("deepspeed", "fsdp2"):
from ..plugins.trainer_plugins.distributed.hub import DistributedPlugin
from ..plugins.trainer_plugins.distributed.interface import DistributedPlugin

DistributedPlugin(self.args.dist_config.name).save_model(

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.

medium

If self.args.dist_config is a standard dictionary, self.args.dist_config.name will raise an AttributeError. Using getattr(self.args.dist_config, 'name', None) is safer and more robust.

Suggested change
if self.args.dist_config is not None and self.args.dist_config.name in ("deepspeed", "fsdp2"):
from ..plugins.trainer_plugins.distributed.hub import DistributedPlugin
from ..plugins.trainer_plugins.distributed.interface import DistributedPlugin
DistributedPlugin(self.args.dist_config.name).save_model(
if self.args.dist_config is not None and getattr(self.args.dist_config, "name", None) in ("deepspeed", "fsdp2"):
from ..plugins.trainer_plugins.distributed.interface import DistributedPlugin
DistributedPlugin(getattr(self.args.dist_config, "name", None)).save_model(

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant