Hardware-agnostic model definition via HF transformer backend (1/N) (#49458)
Signed-off-by: Thomas Ortner <boh@zurich.ibm.com>
Thomas Ortner committed
Aug 13, 2026 at 12:21 UTC
b8baa31a28650b6f3189bc7fcb051e1360c9270c
10 files changed
+691
-5
tests/models/transformers/test_layer_registry.py
new
+177
@@ -0,0 +1,177 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Unit tests for the Transformers backend's hw-agnostic layer resolution.
4
+
5
+`layers._resolve` imports a layer symbol from
6
+`vllm.model_executor.hw_agnostic.layers.<module>` when `VLLM_USE_HW_AGNOSTIC`
7
+is set and the symbol exists, and otherwise falls back to
8
+`vllm.model_executor.layers.<module>`. These tests pin that contract and the
9
+logging that reports which source was used.
10
+"""
11
+
12
+import logging
13
+import sys
14
+import types
15
+
16
+import pytest
17
+import torch
18
+
19
+from vllm.model_executor.models.transformers import layers
20
+
21
+HW_MODULE = "vllm.model_executor.hw_agnostic.layers.layernorm"
22
+
23
+
24
+@pytest.fixture
25
+def fake_hw_layernorm(monkeypatch):
26
+ """Inject a hw-agnostic `layernorm` module exposing a sentinel `RMSNorm`.
27
+
28
+ A `SimpleNamespace` stands in for the module: `importlib.import_module`
29
+ returns it from `sys.modules` and `getattr` resolves `RMSNorm`, while its
30
+ attributes are set at construction (no `ModuleType` attribute-set that mypy
31
+ rejects, no constant `setattr` that ruff rejects)."""
32
+ module = types.SimpleNamespace(RMSNorm=type("HwRMSNorm", (), {}))
33
+ monkeypatch.setitem(sys.modules, HW_MODULE, module)
34
+ return module
35
+
36
+
37
+def test_falls_back_to_vllm_when_disabled(monkeypatch, fake_hw_layernorm):
38
+ """Disabled: the vLLM class is used even if a hw-agnostic one exists."""
39
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "0")
40
+ from vllm.model_executor.layers.layernorm import RMSNorm as VllmRMSNorm
41
+
42
+ assert layers._resolve("layernorm", "RMSNorm") is VllmRMSNorm
43
+
44
+
45
+def test_uses_hw_agnostic_when_enabled(monkeypatch, fake_hw_layernorm, caplog):
46
+ """Enabled and available: the hw-agnostic class is used and logged."""
47
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "1")
48
+ with caplog.at_level(logging.INFO):
49
+ resolved = layers._resolve("layernorm", "RMSNorm")
50
+ assert resolved is fake_hw_layernorm.RMSNorm
51
+ assert "Using hw-agnostic layer: RMSNorm" in caplog.text
52
+
53
+
54
+def test_falls_back_when_symbol_missing(monkeypatch, caplog):
55
+ """Enabled but the symbol is not ported: fall back to vLLM and warn."""
56
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "1")
57
+ # A hw-agnostic module without the requested attribute triggers fallback.
58
+ empty = types.ModuleType(HW_MODULE)
59
+ monkeypatch.setitem(sys.modules, HW_MODULE, empty)
60
+ from vllm.model_executor.layers.layernorm import RMSNorm as VllmRMSNorm
61
+
62
+ with caplog.at_level(logging.WARNING):
63
+ resolved = layers._resolve("layernorm", "RMSNorm")
64
+ assert resolved is VllmRMSNorm
65
+ assert "falling back to default" in caplog.text
66
+
67
+
68
+def test_act_and_mul_falls_back_for_unknown_activation(
69
+ monkeypatch, default_vllm_config
70
+):
71
+ """An activation with no hw-agnostic equivalent falls back to vLLM's.
72
+
73
+ `default_vllm_config` supplies the config context the CustomOp needs.
74
+ """
75
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "1")
76
+ from vllm.model_executor.layers.activation import GeluAndMul
77
+
78
+ assert isinstance(layers.get_act_and_mul_fn("gelu"), GeluAndMul)
79
+
80
+
81
+@pytest.fixture(scope="module")
82
+def tiny_llama_path(tmp_path_factory):
83
+ """A randomly-initialized microscopic Llama saved to disk (with an ungated
84
+ tokenizer) so vLLM can load it like any local checkpoint."""
85
+ from transformers import AutoTokenizer, LlamaConfig, LlamaForCausalLM
86
+
87
+ tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/llama-tokenizer")
88
+ config = LlamaConfig(
89
+ vocab_size=tokenizer.vocab_size,
90
+ hidden_size=128,
91
+ intermediate_size=256,
92
+ num_hidden_layers=2,
93
+ num_attention_heads=4,
94
+ rms_norm_eps=1e-6,
95
+ hidden_act="silu",
96
+ )
97
+ torch.manual_seed(0)
98
+ model = LlamaForCausalLM(config)
99
+
100
+ path = tmp_path_factory.mktemp("tiny_llama")
101
+ model.save_pretrained(path)
102
+ tokenizer.save_pretrained(path)
103
+ return str(path)
104
+
105
+
106
+# Registered names of the layers the backend can
107
+# currently route to hw-agnostic implementations.
108
+_COVERED_LAYERS = ("rms_norm", "silu_and_mul")
109
+
110
+
111
+def _layer_providers(model) -> dict[str, str]:
112
+ """Map each covered layer type present in the model to the provider its
113
+ implementation came from (``hw_agnostic`` or ``vllm``).
114
+ """
115
+
116
+ def provider_of(module) -> str | None:
117
+ for cls in type(module).__mro__:
118
+ if "hw_agnostic.layers" in cls.__module__:
119
+ return "hw_agnostic"
120
+ if ".model_executor.layers." in cls.__module__:
121
+ return "vllm"
122
+ return None
123
+
124
+ providers: dict[str, str] = {}
125
+ for module in model.modules():
126
+ name = getattr(module, "name", None)
127
+ if (
128
+ name in _COVERED_LAYERS
129
+ and name not in providers
130
+ and (prov := provider_of(module)) is not None
131
+ ):
132
+ providers[name] = prov
133
+ return providers
134
+
135
+
136
+def _serve(vllm_runner, model_path, prompts):
137
+ """Serve the model through the backend; return (layer_providers, logprobs)."""
138
+ with vllm_runner(
139
+ model_path,
140
+ model_impl="transformers",
141
+ max_model_len=64,
142
+ enforce_eager=True,
143
+ gpu_memory_utilization=0.3,
144
+ ) as runner:
145
+ assert runner.llm.llm_engine.model_config.using_transformers_backend()
146
+ providers = runner.apply_model(_layer_providers)[0]
147
+ outputs = runner.generate_greedy_logprobs(
148
+ prompts, max_tokens=32, num_logprobs=5
149
+ )
150
+ return providers, outputs
151
+
152
+
153
+def test_hw_agnostic_matches_vllm_end_to_end(monkeypatch, vllm_runner, tiny_llama_path):
154
+ """Serving the tiny model with hw-agnostic layers matches the vLLM baseline."""
155
+ # spawn: worker re-imports layers with the env set (see docstring).
156
+ monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
157
+ # apply_model pickles the introspection function.
158
+ monkeypatch.setenv("VLLM_ALLOW_INSECURE_SERIALIZATION", "1")
159
+ from ..utils import check_logprobs_close
160
+
161
+ prompts = ["The capital of France is", "vLLM is"]
162
+
163
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "0")
164
+ vllm_providers, vllm_outputs = _serve(vllm_runner, tiny_llama_path, prompts)
165
+ # Both replaceable layers present in a Llama block must be vLLM's here.
166
+ assert vllm_providers == {"rms_norm": "vllm", "silu_and_mul": "vllm"}
167
+
168
+ monkeypatch.setenv("VLLM_USE_HW_AGNOSTIC", "1")
169
+ hw_providers, hw_outputs = _serve(vllm_runner, tiny_llama_path, prompts)
170
+ assert hw_providers == {"rms_norm": "hw_agnostic", "silu_and_mul": "hw_agnostic"}
171
+
172
+ check_logprobs_close(
173
+ outputs_0_lst=vllm_outputs,
174
+ outputs_1_lst=hw_outputs,
175
+ name_0="vllm",
176
+ name_1="hw_agnostic",
177
+ )
vllm/envs.py
+5
@@ -125,6 +125,7 @@ if TYPE_CHECKING:
125
VLLM_ALLOW_RUNTIME_LORA_UPDATING: bool = False
126
VLLM_SKIP_P2P_CHECK: bool = False
127
VLLM_DISABLED_KERNELS: list[str] = []
128
+ VLLM_USE_HW_AGNOSTIC: bool = False
129
VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE: bool = True
130
VLLM_DISABLE_PYNCCL: bool = False
131
VLLM_USE_OINK_OPS: bool = False
@@ -1200,6 +1201,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
1201
if "VLLM_DISABLED_KERNELS" not in os.environ
1202
else os.environ["VLLM_DISABLED_KERNELS"].split(",")
1203
),
1204
+ # Selects hw-agnostic layers for HF transformer backend
1205
+ "VLLM_USE_HW_AGNOSTIC": lambda: (
1206
+ os.getenv("VLLM_USE_HW_AGNOSTIC", "False").lower() in ("true", "1")
1207
+ ),
1208
"VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE": lambda: bool(
1209
int(os.getenv("VLLM_ENABLE_FLA_PACKED_RECURRENT_DECODE", "1"))
1210
),
vllm/model_executor/hw_agnostic/__init__.py
new
+2
@@ -0,0 +1,2 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/model_executor/hw_agnostic/custom_op.py
new
+318
@@ -0,0 +1,318 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import functools
4
+import inspect
5
+
6
+import torch
7
+import torch.nn as nn
8
+
9
+from vllm.config import get_cached_compilation_config
10
+from vllm.logger import init_logger
11
+from vllm.model_executor.utils import maybe_disable_graph_partition
12
+from vllm.platforms import current_platform
13
+
14
+logger = init_logger(__name__)
15
+
16
+# Dictionary of all custom ops (classes, indexed by registered name).
17
+# To check if an op with a name is enabled, call .enabled() on the class.
18
+# Examples:
19
+# - MyOp.enabled()
20
+# - op_registry["my_op"].enabled()
21
+op_registry: dict[str, type["CustomOp"] | type["PluggableLayer"]] = {}
22
+op_registry_oot: dict[str, type["CustomOp"] | type["PluggableLayer"]] = {}
23
+
24
+
25
+def maybe_get_oot_by_class(class_type: type) -> type:
26
+ class_name = class_type.__name__
27
+ if class_name in op_registry_oot:
28
+ return op_registry_oot[class_name]
29
+ return class_type
30
+
31
+
32
+class PluggableLayer(nn.Module):
33
+ """
34
+ Base class for pluggable layers.
35
+
36
+ A PluggableLayer is a *module-composing* abstraction: it may instantiate other
37
+ ``torch.nn.Module`` objects as sub-layers, and its functionality depends on
38
+ these sub-layers following a generalized invocation sequence. Also, it is stateful
39
+ and may hold parameters or buffers.
40
+
41
+ Unlike :class:`CustomOp`, PluggableLayer does NOT provide per-platform
42
+ ``forward_*`` dispatch. Instead, it supports out-of-tree (OOT) replacement
43
+ of the entire layer class at instantiation time, allowing customized
44
+ initialization and submodule composition.
45
+ """
46
+
47
+ def __new__(cls, *args, **kwargs):
48
+ try:
49
+ layer_class_name = cls.__name__
50
+ except AttributeError:
51
+ raise TypeError(
52
+ f"Cannot instantiate '{cls.__name__}': its 'name' attribute "
53
+ f"was not set, possibly because it was not decorated with "
54
+ f"@PluggableLayer.register, or it's the PluggableLayer itself."
55
+ ) from None
56
+
57
+ if layer_class_name not in op_registry_oot:
58
+ layer_cls_to_instantiate = cls
59
+ else:
60
+ layer_cls_to_instantiate = op_registry_oot[layer_class_name]
61
+ logger.debug(
62
+ "Instantiating pluggable layer: %s using %s",
63
+ layer_class_name,
64
+ str(layer_cls_to_instantiate),
65
+ )
66
+ return super().__new__(layer_cls_to_instantiate)
67
+
68
+ # Decorator to register pluggable layers.
69
+ @classmethod
70
+ def register(cls, name: str):
71
+ def decorator(op_cls):
72
+ assert name not in op_registry, f"Duplicate op name: {name}"
73
+ op_cls.name = name
74
+ op_registry[name] = op_cls
75
+ return op_cls
76
+
77
+ return decorator
78
+
79
+ # Decorator to register out-of-tree(oot) pluggable layers.
80
+ # For OOT pluggable layers:
81
+ # if in-tree layer class is registered with an oot_custom_layer,
82
+ # the oot_custom_layer will be used instead.
83
+ @classmethod
84
+ def register_oot(cls, _decorated_layer_cls=None, name: str | None = None):
85
+ def decorator(layer_cls):
86
+ reg_name = name if name is not None else cls.__name__
87
+ assert reg_name not in op_registry_oot, f"Duplicate layer name: {reg_name}"
88
+ layer_cls.name = reg_name
89
+ op_registry_oot[reg_name] = layer_cls
90
+ return layer_cls
91
+
92
+ if _decorated_layer_cls is None:
93
+ # Called with parentheses: @PluggableLayer.register_oot()
94
+ # or @PluggableLayer.register_oot(name="...")
95
+ return decorator
96
+ elif isinstance(_decorated_layer_cls, type): # Check if it's a class
97
+ # Called without parentheses: @PluggableLayer.register_oot
98
+ return decorator(_decorated_layer_cls)
99
+ else:
100
+ raise TypeError("Decorator can only be applied to classes.")
101
+
102
+
103
+class CustomOp(nn.Module):
104
+ """
105
+ Base class for custom ops.
106
+ Dispatches the forward method to the appropriate backend.
107
+ """
108
+
109
+ def __new__(cls, *args, **kwargs):
110
+ try:
111
+ op_name = cls.__name__
112
+ except AttributeError:
113
+ raise TypeError(
114
+ f"Cannot instantiate '{cls.__name__}': its 'name' attribute "
115
+ f"was not set, possibly because it was not decorated with "
116
+ f"@CustomOp.register, or it's the CustomOp base class itself."
117
+ ) from None
118
+
119
+ if op_name not in op_registry_oot:
120
+ op_cls_to_instantiate = cls
121
+ else:
122
+ op_cls_to_instantiate = op_registry_oot[op_name]
123
+ logger.debug(
124
+ "Instantiating custom op: %s using %s",
125
+ op_name,
126
+ str(op_cls_to_instantiate),
127
+ )
128
+ return super().__new__(op_cls_to_instantiate)
129
+
130
+ def __init__(self, *, enforce_enable: bool = False, compile_native: bool = False):
131
+ super().__init__()
132
+ self._enforce_enable = enforce_enable
133
+ self._forward_method = self.dispatch_forward(compile_native=compile_native)
134
+
135
+ def forward(self, *args, **kwargs):
136
+ return self._forward_method(*args, **kwargs)
137
+
138
+ def forward_native(self, *args, **kwargs):
139
+ """PyTorch-native implementation; OOT plugins override via
140
+ :meth:`forward_oot`."""
141
+ raise NotImplementedError
142
+
143
+ def forward_oot(self, *args, **kwargs):
144
+ # By default, OOT ops fall back to the PyTorch-native implementation.
145
+ return self.forward_native(*args, **kwargs)
146
+
147
+ def dispatch_forward(self, compile_native: bool):
148
+ # The hw-agnostic CustomOp dispatches only between forward_native
149
+ # (in-tree, used on every platform) and forward_oot (overridden by
150
+ # an OOT plugin via register_oot).
151
+ compilation_config = get_cached_compilation_config()
152
+
153
+ # NOTE(shen-shanshan): CustomOp object can be enforce enabled, e.g.,
154
+ # enable device-specific kernels in ViT models when enabling graph
155
+ # mode. By default, it will follow the compilation_config to determine
156
+ # whether enable itself.
157
+ # This enforce_enable mechanism will be removed after we adding a
158
+ # separate compilation_config for multi-modal part.
159
+ enabled = self._enforce_enable or self.enabled()
160
+ if enabled:
161
+ compilation_config.enabled_custom_ops.update([self.__class__.name])
162
+ else:
163
+ compilation_config.disabled_custom_ops.update([self.__class__.name])
164
+
165
+ if not enabled:
166
+ # Compile forward_native to avoid eager torch ops if inside
167
+ # opaque torch custom op (e.g. fused_moe, unified_attention, etc.)
168
+ return self.maybe_compile(self.forward_native, enable=compile_native)
169
+
170
+ if current_platform.is_out_of_tree():
171
+ return self.forward_oot
172
+ return self.forward_native
173
+
174
+ def maybe_compile(self, fn, *, enable: bool = True):
175
+ """
176
+ Compile fn if compilation enabled.
177
+ Useful for CustomOp instances called from within a torch custom op,
178
+ meaning the forward call is hidden from the model-level torch.compile.
179
+
180
+ NOTE: this does not enable fusion across ops, so opaque custom ops
181
+ should still be unwrapped wherever possible.
182
+ """
183
+ from vllm.config.compilation import CompilationMode
184
+
185
+ # Do not compile if compilation disabled
186
+ if not enable:
187
+ return fn
188
+
189
+ # Do not compile if global compilation disabled
190
+ compilation_config = get_cached_compilation_config()
191
+ if compilation_config.mode == CompilationMode.NONE:
192
+ return fn
193
+
194
+ # If eager backend is used, do not compile either
195
+ if compilation_config.backend == "eager":
196
+ return fn
197
+
198
+ compile_options = maybe_disable_graph_partition(
199
+ current_platform.simple_compile_backend
200
+ )
201
+ backend = current_platform.simple_compile_backend
202
+
203
+ dynamic_arg_dims = getattr(self.__class__, "_dynamic_arg_dims", None)
204
+ if dynamic_arg_dims is not None:
205
+ compiled_fn = torch.compile(
206
+ fn,
207
+ dynamic=False,
208
+ backend=backend,
209
+ options=compile_options,
210
+ )
211
+ sig = inspect.signature(fn)
212
+
213
+ @functools.wraps(fn)
214
+ def wrapper(*args, **kwargs):
215
+ bound = sig.bind(*args, **kwargs)
216
+ bound.apply_defaults()
217
+ for name, dims in dynamic_arg_dims.items():
218
+ arg = bound.arguments.get(name)
219
+ if arg is not None and isinstance(arg, torch.Tensor):
220
+ dims_list = [dims] if isinstance(dims, int) else dims
221
+ for d in dims_list:
222
+ real_d = arg.ndim + d if d < 0 else d
223
+ torch._dynamo.mark_dynamic(arg, real_d)
224
+ return compiled_fn(*args, **kwargs)
225
+
226
+ return wrapper
227
+
228
+ # dynamic=True to avoid recompilations
229
+ return torch.compile(
230
+ fn,
231
+ dynamic=True,
232
+ backend=backend,
233
+ options=compile_options,
234
+ )
235
+
236
+ @classmethod
237
+ def enabled(cls) -> bool:
238
+ # if no name, then it was not registered
239
+ compilation_config = get_cached_compilation_config()
240
+ custom_ops = compilation_config.custom_ops
241
+ if not hasattr(cls, "name"):
242
+ logger.warning_once(
243
+ "Custom op %s was not registered, which means it won't appear "
244
+ "in the op registry. It will be enabled/disabled based on the "
245
+ "global settings.",
246
+ cls.__name__,
247
+ )
248
+ return CustomOp.default_on()
249
+
250
+ enabled = f"+{cls.name}" in custom_ops
251
+ disabled = f"-{cls.name}" in custom_ops
252
+ assert not (enabled and disabled), f"Cannot enable and disable {cls.name}"
253
+
254
+ return (CustomOp.default_on() or enabled) and not disabled
255
+
256
+ @staticmethod
257
+ def default_on() -> bool:
258
+ """
259
+ Behavior controlled by `CompilationConfig.custom_ops`: On by default if
260
+ 'all', off by default if 'none'.
261
+ When PyTorch Inductor is used, 'none' is the default value,
262
+ otherwise 'all'.
263
+ """
264
+ compilation_config = get_cached_compilation_config()
265
+ count_none = compilation_config.custom_ops.count("none")
266
+ count_all = compilation_config.custom_ops.count("all")
267
+ assert count_none + count_all == 1
268
+
269
+ return not count_none > 0 or count_all > 0
270
+
271
+ # Decorator to register custom ops.
272
+ @classmethod
273
+ def register(
274
+ cls,
275
+ name: str,
276
+ dynamic_arg_dims: dict[str, int | list[int]] | None = None,
277
+ ):
278
+ def decorator(op_cls):
279
+ assert name not in op_registry, f"Duplicate op name: {name}"
280
+ op_cls.name = name
281
+ op_cls._dynamic_arg_dims = dynamic_arg_dims
282
+ op_registry[name] = op_cls
283
+ return op_cls
284
+
285
+ return decorator
286
+
287
+ # Decorator to register out-of-tree(oot) custom ops.
288
+ # For OOT custom ops:
289
+ # if in-tree layer class is registered with an oot_custom_op layer,
290
+ # the oot_custom_op layer will be used instead.
291
+ # Example:
292
+ # - @UnquantizedFusedMoEMethod.register_oot
293
+ # class HPUUnquantizedFusedMoEMethod(UnquantizedFusedMoEMethod)
294
+ # or
295
+ # - @CustomOP.register_oot(name="UnquantizedFusedMoEMethod")
296
+ @classmethod
297
+ def register_oot(cls, _decorated_op_cls=None, name: str | None = None):
298
+ def decorator(op_cls):
299
+ reg_name = name if name is not None else cls.__name__
300
+ assert reg_name not in op_registry_oot, f"Duplicate op name: {reg_name}"
301
+ op_cls.name = reg_name
302
+ op_registry_oot[reg_name] = op_cls
303
+ return op_cls
304
+
305
+ if _decorated_op_cls is None:
306
+ # Called with parentheses: @CustomOP.register_oot()
307
+ # or @CustomOP.register_oot(name="...")
308
+ # So, _decorated_op_cls is None.
309
+ # We return the actual decorator function.
310
+ return decorator
311
+ elif isinstance(_decorated_op_cls, type): # Check if it's a class
312
+ # Called without parentheses: @CustomOP.register_oot
313
+ # The first argument is the class itself.
314
+ # We call the 'decorator' function immediately with the class.
315
+ return decorator(_decorated_op_cls)
316
+ else:
317
+ # Handle other unexpected cases if necessary
318
+ raise TypeError("Decorator can only be applied to classes.")
vllm/model_executor/hw_agnostic/layers/__init__.py
new
+2
@@ -0,0 +1,2 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
vllm/model_executor/hw_agnostic/layers/activation.py
new
+27
@@ -0,0 +1,27 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import torch
4
+import torch.nn.functional as F
5
+
6
+from vllm.model_executor.hw_agnostic.custom_op import CustomOp
7
+
8
+
9
+@CustomOp.register("silu_and_mul")
10
+class SiluAndMul(CustomOp):
11
+ """SwiGLU: ``x -> silu(x[:d]) * x[d:]`` where ``d = x.shape[-1] // 2``."""
12
+
13
+ def forward_native(self, x: torch.Tensor) -> torch.Tensor:
14
+ d = x.shape[-1] // 2
15
+ return F.silu(x[..., :d]) * x[..., d:]
16
+
17
+
18
+# Activation-and-mul ops keyed by HF activation name.
19
+_ACTIVATION_AND_MUL_REGISTRY = {
20
+ "silu": SiluAndMul,
21
+ "swish": SiluAndMul,
22
+}
23
+
24
+
25
+def get_act_and_mul_fn(act_fn_name: str) -> CustomOp:
26
+ """Build the hw-agnostic activation-and-mul op named `act_fn_name`."""
27
+ return _ACTIVATION_AND_MUL_REGISTRY[act_fn_name.lower()]()
vllm/model_executor/hw_agnostic/layers/layernorm.py
new
+90
@@ -0,0 +1,90 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import torch
4
+import torch.nn as nn
5
+from torch import Tensor
6
+
7
+from vllm.model_executor.hw_agnostic.custom_op import CustomOp
8
+
9
+
10
+@CustomOp.register("rms_norm")
11
+class RMSNorm(CustomOp):
12
+ """``x -> w * x / sqrt(E[x^2] + eps)``. With ``residual``, fuses
13
+ ``residual += x`` then RMSNorm and returns ``(normalized, residual)``."""
14
+
15
+ def __init__(
16
+ self,
17
+ hidden_size: int,
18
+ eps: float = 1e-6,
19
+ var_hidden_size: int | None = None,
20
+ has_weight: bool = True,
21
+ dtype: torch.dtype | None = None,
22
+ ) -> None:
23
+ super().__init__()
24
+ self.hidden_size = hidden_size
25
+ self.variance_epsilon = eps
26
+ self.variance_size_override = (
27
+ None if var_hidden_size == hidden_size else var_hidden_size
28
+ )
29
+ weight_dtype = dtype or torch.get_default_dtype()
30
+ self.has_weight = has_weight
31
+ weight = torch.ones(hidden_size, dtype=weight_dtype)
32
+ if has_weight:
33
+ self.weight = nn.Parameter(weight)
34
+ else:
35
+ self.register_buffer("weight", weight, persistent=False)
36
+
37
+ def _rms_norm(
38
+ self,
39
+ x: Tensor,
40
+ weight: Tensor | None,
41
+ epsilon: float,
42
+ variance_size: int | None = None,
43
+ ) -> Tensor:
44
+ """Weighted root-mean-square layer normalization"""
45
+ orig_dtype = x.dtype
46
+ x = x.to(torch.float32)
47
+ x_var = x if variance_size is None else x[..., :variance_size]
48
+ variance = x_var.pow(2).mean(dim=-1, keepdim=True)
49
+ x = x * torch.rsqrt(variance + epsilon)
50
+ if weight is not None:
51
+ x = x.to(weight.dtype) * weight
52
+ return x.to(orig_dtype)
53
+
54
+ def _fused_add_rms_norm(
55
+ self,
56
+ x: Tensor,
57
+ x_residual: Tensor,
58
+ weight: Tensor | None,
59
+ epsilon: float,
60
+ variance_size: int | None = None,
61
+ ) -> tuple[Tensor, Tensor]:
62
+ """Fused add and weighted root-mean-square layer normalization"""
63
+ orig_dtype = x.dtype
64
+ x = x.to(torch.float32)
65
+ x = x + x_residual.to(torch.float32)
66
+ x_residual = x.to(orig_dtype)
67
+
68
+ x_var = x if variance_size is None else x[..., :variance_size]
69
+ variance = x_var.pow(2).mean(dim=-1, keepdim=True)
70
+ x = x * torch.rsqrt(variance + epsilon)
71
+ if weight is not None:
72
+ x = x.to(weight.dtype) * weight
73
+ return x.to(orig_dtype), x_residual
74
+
75
+ def forward_native(
76
+ self,
77
+ x: torch.Tensor,
78
+ residual: torch.Tensor | None = None,
79
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
80
+ weight = self.weight if self.has_weight else None
81
+ epsilon = self.variance_epsilon
82
+ variance_size = self.variance_size_override
83
+
84
+ if residual is None:
85
+ return self._rms_norm(x, weight, epsilon, variance_size)
86
+ else:
87
+ return self._fused_add_rms_norm(x, residual, weight, epsilon, variance_size)
88
+
89
+ def extra_repr(self) -> str:
90
+ return f"hidden_size={self.hidden_size}, eps={self.variance_epsilon}"
vllm/model_executor/models/transformers/fusers/glu.py
+2
-4
@@ -11,10 +11,7 @@ from torch import fx, nn
11
from transformers.activations import ACT2CLS
12
13
from vllm.logger import init_logger
14
-from vllm.model_executor.layers.activation import (
15
- _ACTIVATION_AND_MUL_REGISTRY,
16
- get_act_and_mul_fn,
17
-)
14
+from vllm.model_executor.layers.activation import _ACTIVATION_AND_MUL_REGISTRY
15
from vllm.model_executor.layers.linear import MergedColumnParallelLinear
16
from vllm.model_executor.models.transformers.fusers.base import StackedFuser
17
from vllm.model_executor.models.transformers.fx_utils import (
@@ -26,6 +23,7 @@ from vllm.model_executor.models.transformers.fx_utils import (
23
replace_expr,
24
single_self_call,
25
)
26
+from vllm.model_executor.models.transformers.layers import get_act_and_mul_fn
27
from vllm.model_executor.models.transformers.utils import (
28
log_replacement,
29
replace_linear_class,
vllm/model_executor/models/transformers/fusers/rms_norm.py
+4
-1
@@ -15,7 +15,6 @@ from vllm.distributed import (
15
)
16
from vllm.distributed.parallel_state import model_parallel_is_initialized
17
from vllm.distributed.utils import split_tensor_along_last_dim
18
-from vllm.model_executor.layers.layernorm import GemmaRMSNorm, RMSNorm
18
from vllm.model_executor.models.transformers.fusers.base import BaseFuser
19
from vllm.model_executor.models.transformers.fx_utils import (
20
find_node,
@@ -25,6 +24,10 @@ from vllm.model_executor.models.transformers.fx_utils import (
24
peel,
25
trace,
26
)
27
+from vllm.model_executor.models.transformers.layers import (
28
+ GemmaRMSNorm,
29
+ RMSNorm,
30
+)
31
32
if TYPE_CHECKING:
33
from vllm.config import VllmConfig
vllm/model_executor/models/transformers/layers.py
new
+64
@@ -0,0 +1,64 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Layer provider resolution for the Transformers modeling backend.
4
+
5
+When ``VLLM_USE_HW_AGNOSTIC`` is set, layer symbols are imported from
6
+``vllm.model_executor.hw_agnostic.layers.<module>``, falling back to
7
+``vllm.model_executor.layers.<module>`` for anything not yet ported. The
8
+resolved source of every symbol is logged so it is clear which layers run
9
+hw-agnostic and which fell back to vLLM.
10
+"""
11
+
12
+import importlib
13
+
14
+import vllm.envs as envs
15
+from vllm.logger import init_logger
16
+
17
+logger = init_logger(__name__)
18
+
19
+_HW_PKG = "vllm.model_executor.hw_agnostic.layers"
20
+_VLLM_PKG = "vllm.model_executor.layers"
21
+
22
+
23
+def _resolve(module: str, name: str):
24
+ """Return `name` from the hw-agnostic `module` when enabled and available,
25
+ else from vLLM. Logs which source was used."""
26
+ if envs.VLLM_USE_HW_AGNOSTIC:
27
+ try:
28
+ obj = getattr(importlib.import_module(f"{_HW_PKG}.{module}"), name)
29
+ logger.info("Using hw-agnostic layer: %s", name)
30
+ return obj
31
+ except (ImportError, AttributeError):
32
+ logger.warning(
33
+ "hw-agnostic layer %s is not available; falling back to default", name
34
+ )
35
+ return getattr(importlib.import_module(f"{_VLLM_PKG}.{module}"), name)
36
+
37
+
38
+RMSNorm = _resolve("layernorm", "RMSNorm")
39
+GemmaRMSNorm = _resolve("layernorm", "GemmaRMSNorm")
40
+
41
+
42
+def get_act_and_mul_fn(act_fn_name: str):
43
+ """Fused activation-and-mul op for `act_fn_name`, preferring hw-agnostic.
44
+
45
+ Resolved per call because the op is name-parameterized: an activation with
46
+ no hw-agnostic equivalent falls back to vLLM individually.
47
+ """
48
+ if envs.VLLM_USE_HW_AGNOSTIC:
49
+ try:
50
+ from vllm.model_executor.hw_agnostic.layers.activation import (
51
+ get_act_and_mul_fn as hw_fn,
52
+ )
53
+
54
+ fn = hw_fn(act_fn_name)
55
+ logger.info_once("Using hw-agnostic activation: %s", act_fn_name)
56
+ return fn
57
+ except (ImportError, KeyError):
58
+ logger.warning_once(
59
+ "hw-agnostic activation %s is not available; falling back to vLLM",
60
+ act_fn_name,
61
+ )
62
+ from vllm.model_executor.layers.activation import get_act_and_mul_fn as vllm_fn
63
+
64
+ return vllm_fn(act_fn_name)