This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch script/canonical-parser-df
in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/script/canonical-parser-df by
this push:
new 7505c9a88d [REFACTOR][TVMSCRIPT] Let dialects resolve global metadata
7505c9a88d is described below
commit 7505c9a88d43b97931cfbb254b0b42cefa09ca44
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Sep 24 13:15:14 2026 +0000
[REFACTOR][TVMSCRIPT] Let dialects resolve global metadata
Global metadata selectors belong to the language that defines their meaning.
Dispatch registered global-info arguments through the active parser
protocol,
retain Relax selector behavior, and make unsupported TIRx lookup explicit.
Introduce a modular, customizable parser infrastructure for TVMScript
through transpilation. The key idea is to deterministically transpile language
syntax into a corresponding sequence of builder calls while preserving source
locations and providing span context.
Key design elements include:
- Acquire source and its definition scope, collect syntax facts once,
rewrite the owned AST, and execute the resulting builder program with the
original lexical bindings.
- Each language variant customizes behavior through a `parser_protocol.py`
file, which defines special handling for syntax such as `for` and `if`.
- Create reusable span entries during rewriting. Attach locations to
meaningful expressions and pass spans directly to builder operations that own
them.
- Reuse existing builder and IR APIs, so new types and functions can be
added without changing the parser, provided they have well-defined builder
behavior.
- Generate specialization handling only when specialization is requested.
For example, this kernel:
```python
@T.prim_func
def add_one(A: T.Buffer((128,), "float32"), B: T.Buffer((16, 8),
"float32")):
for i in range(128):
B[i // 8, i % 8] = A[i] + 1
```
is lowered to the following builder calls. This presentation wraps the
emitted program in `build_add_one` and gives its local scaffolding readable
aliases. `I` supplies shared builders, `T` is the dialect namespace, `_S` holds
source-span entries, and `_host0` and `_host1` are captured annotation helpers.
```python
def build_add_one():
with I.IRBuilder() as builder:
with T.function_(span=_S[6]) as fn:
T.func_name('add_one')
_parameter0 = T.arg('A', _host0(_S[1](T.Buffer(_S[0]((128,)),
'float32'))), span=_S[2])
_parameter1 = T.arg('B', _host1(_S[4](T.Buffer(_S[3]((16, 8)),
'float32'))), span=_S[5])
def build_body():
global T
A, B = fn.params
with T.for_(T.range_(128), span=_S[7], names='i') as i:
_value0 = _S[9](_S[8](A[i]) + 1)
T.setitem_(B, (_S[10](i // 8), _S[11](i % 8)), _value0,
span=_S[12])
build_body()
result = fn.function
result.__name__ = 'add_one'
T.check_well_formed_(result)
return fn.function
```
---
python/tvm/relax/script/builder/__init__.py | 7 +--
python/tvm/relax/script/builder/parser_protocol.py | 60 ++++++++++++++++++++++
python/tvm/script/ir_builder/ir/__init__.py | 2 +-
python/tvm/script/ir_builder/ir/parser_protocol.py | 53 ++++---------------
python/tvm/script/parser/transpile.py | 6 +--
python/tvm/tirx/script/builder/__init__.py | 1 +
python/tvm/tirx/script/builder/parser_protocol.py | 8 +++
tests/python/script/minilang.py | 2 +-
.../python/script/test_special_parser_protocol.py | 53 +++++++++++++++++--
9 files changed, 138 insertions(+), 54 deletions(-)
diff --git a/python/tvm/relax/script/builder/__init__.py
b/python/tvm/relax/script/builder/__init__.py
index 6d3f1eed09..3d8dbc1d41 100644
--- a/python/tvm/relax/script/builder/__init__.py
+++ b/python/tvm/relax/script/builder/__init__.py
@@ -30,7 +30,6 @@ from tvm.relax.distributed import device_mesh as device_mesh
from tvm.script.ir_builder import IRBuilder as _IRBuilder
from tvm.script.ir_builder.base import at as _at
from tvm.script.ir_builder.base import source_span as _source_span
-from tvm.script.ir_builder.ir.parser_protocol import resolve_global_info_ as
_lookup_global_info
from tvm.script.parser.protocol_registry import ARGS_POLICIES as _ARGS_POLICIES
from tvm.script.parser.protocol_registry import args_policy as _args_policy
from tvm.script.parser.protocol_registry import constexpr as constexpr
@@ -68,6 +67,7 @@ from .parser_protocol import (
not_,
or_,
range_,
+ resolve_global_info_,
resolve_type_var_,
return_,
set_mutable_cell_,
@@ -92,7 +92,7 @@ def Tensor(shape=None, dtype=None, vdevice=None, ndim=-1, *,
span=None):
# Python evaluates annotations before the module decorator opens its
# frame. The generated declaration resolves this module-owned
reference.
return _ir.Type.missing()
- vdevice = _lookup_global_info(vdevice)
+ vdevice = resolve_global_info_(vdevice)
return _relax.TensorType(shape, dtype, vdevice, ndim, _source_span(span))
@@ -106,7 +106,7 @@ def DTensor(shape=None, dtype=None, device_mesh=None,
placement="", *, ndim=-1,
if device_mesh is None:
device_mesh = _DeviceMesh([], _ir.Range(0, 1))
else:
- device_mesh = _lookup_global_info(device_mesh)
+ device_mesh = resolve_global_info_(device_mesh)
if isinstance(placement, _python.str):
placement = _Placement.from_text(placement)
return _DTensorType(Tensor(shape, dtype, ndim=ndim), device_mesh,
placement, _source_span(span))
@@ -232,6 +232,7 @@ __all__ = [
"if_",
"check_well_formed_",
"resolve_type_var_",
+ "resolve_global_info_",
"call_global_var_",
"decl_mutable_cell_",
"set_mutable_cell_",
diff --git a/python/tvm/relax/script/builder/parser_protocol.py
b/python/tvm/relax/script/builder/parser_protocol.py
index 2a88efc25e..9a27a03a77 100644
--- a/python/tvm/relax/script/builder/parser_protocol.py
+++ b/python/tvm/relax/script/builder/parser_protocol.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import builtins as _python
import numbers as _numbers
+import re as _re
from collections.abc import Sequence
from typing import Any, NoReturn
@@ -31,6 +32,7 @@ from tvm import tirx as _tir
from tvm.ir.prim import _ffi_api as _prim_ffi
from tvm.script.ir_builder import IRBuilder as _IRBuilder
from tvm.script.ir_builder import base as _base
+from tvm.script.ir_builder.ir.frame import IRModuleFrame as _IRModuleFrame
from .. import builder as _builder
from . import _ffi_api
@@ -40,6 +42,64 @@ from . import ir as _native
_Span = _base.SpanEntry | _ir.Span | tuple[_ir.SourceName, int, int, int, int]
| None
+def resolve_global_info_(content: Any) -> Any:
+ """Resolve a module-owned selector or retain a concrete object.
+
+ Parameters
+ ----------
+ content : str or Any
+ Original global-info selector or concrete value. "mesh[0]" indexes a
named list;
+ "cuda:1" selects the second CUDA vdevice; "vdevice:0" selects by
absolute index.
+ A trailing memory-scope suffix is accepted without changing device
selection.
+
+ Returns
+ -------
+ Any
+ The exact registered global-info object, or the unchanged non-string
input.
+
+ Notes
+ -----
+ String lookup requires the nearest active native module frame and creates
no metadata.
+ Missing context, malformed selectors or unmatched devices raise
ValueError; missing map
+ entries or out-of-range indices propagate KeyError/IndexError. Non-string
values require
+ no frame. No source span is attached to an existing metadata object.
+
+ .. code:: python
+
+ # Source
+ R.Tensor((n,), "float32", vdevice="cuda:0")
+ # Generated builder
+ R.Tensor((n,), "float32", vdevice=R.resolve_global_info_("cuda:0"))
+ """
+ if not isinstance(content, str):
+ return content
+ if not _IRBuilder.is_in_scope():
+ raise ValueError("Global-info lookup requires an enclosing module
frame")
+ for frame in reversed(_IRBuilder.current().frames):
+ if isinstance(frame, _IRModuleFrame):
+ break
+ else:
+ raise ValueError("Global-info lookup requires an enclosing module
frame")
+ match = _re.fullmatch(r"([^\[\]]+)\[(\d+)\]", content)
+ if match:
+ name, index = match.groups()
+ return frame.global_infos[name][int(index)]
+ selector = _re.fullmatch(r"([^:\[\]]+)(?::(\d+)(?::([^:]+))?)?", content)
+ if selector is None:
+ raise ValueError(f"Invalid global-info reference: {content!r}")
+ target, index, _scope = selector.groups()
+ ordinal = int(index) if index is not None else 0
+ devices = frame.global_infos.get("vdevice", ())
+ if target == "vdevice":
+ return devices[ordinal]
+ for device in devices:
+ if device.target.kind.name == target:
+ if ordinal == 0:
+ return device
+ ordinal -= 1
+ raise ValueError(f"Global-info device reference was not found:
{content!r}")
+
+
def bind_(
value: Any = _base.MISSING,
*,
diff --git a/python/tvm/script/ir_builder/ir/__init__.py
b/python/tvm/script/ir_builder/ir/__init__.py
index f38bc481e7..1fd0646e98 100644
--- a/python/tvm/script/ir_builder/ir/__init__.py
+++ b/python/tvm/script/ir_builder/ir/__init__.py
@@ -21,7 +21,7 @@ from tvm.ir import GenericConst, Range, StringImm, StringType
from tvm.script.parser.protocol_registry import constexpr
from ..base import IRBuilder, MISSING, annotation_value_, at_, with_at_group_,
require_defined
-from .parser_protocol import check_well_formed_, module_member_,
resolve_global_info_
+from .parser_protocol import check_well_formed_, module_member_
from .frame import IRModuleFrame
from .ir import (
decl_function,
diff --git a/python/tvm/script/ir_builder/ir/parser_protocol.py
b/python/tvm/script/ir_builder/ir/parser_protocol.py
index 39779390f7..f781895ff1 100644
--- a/python/tvm/script/ir_builder/ir/parser_protocol.py
+++ b/python/tvm/script/ir_builder/ir/parser_protocol.py
@@ -81,74 +81,43 @@ They specify a structural contract, without a base class or
runtime dispatcher.
from __future__ import annotations
-import re
from collections.abc import Sequence
from typing import Any
from tvm import ir as _ir
-from ..base import MISSING, IRBuilder, IRBuilderFrame, SpanEntry
-from .frame import IRModuleFrame
+from ..base import MISSING, IRBuilderFrame, SpanEntry
_Span = SpanEntry | _ir.Span | tuple[_ir.SourceName, int, int, int, int] | None
def resolve_global_info_(content: Any) -> Any:
- """Resolve a module-owned selector or retain a concrete object.
+ """Look up language-owned global metadata selected by a source argument.
Parameters
----------
content : str or Any
- Original global-info selector or concrete value. "mesh[0]" indexes a
named list;
- "cuda:1" selects the second CUDA vdevice; "vdevice:0" selects by
absolute index.
- A trailing memory-scope suffix is accepted without changing device
selection.
+ A selector or concrete value interpreted by the active language.
Returns
-------
Any
- The exact registered global-info object, or the unchanged non-string
input.
+ The language's metadata value. Selector syntax, lookup context and
+ unsupported-value errors belong to the dialect implementation.
Notes
-----
- String lookup requires the nearest active native module frame and creates
no metadata.
- Missing context, malformed selectors or unmatched devices raise
ValueError; missing map
- entries or out-of-range indices propagate KeyError/IndexError. Non-string
values require
- no frame. No source span is attached to an existing metadata object.
+ A string argument marked ``global_info`` is evaluated once and passed to
+ this hook without attaching a new span to the existing metadata object.
.. code:: python
- # Source
- R.Tensor((n,), "float32", vdevice="cuda:0")
+ # Source: an argument policy marks metadata as global_info.
+ X.CustomType(metadata="mesh[0]")
# Generated builder
- X.Tensor((n,), "float32", vdevice=I.resolve_global_info_("cuda:0"))
+ X.CustomType(metadata=X.resolve_global_info_("mesh[0]"))
"""
- if not isinstance(content, str):
- return content
- if not IRBuilder.is_in_scope():
- raise ValueError("Global-info lookup requires an enclosing module
frame")
- for frame in reversed(IRBuilder.current().frames):
- if isinstance(frame, IRModuleFrame):
- break
- else:
- raise ValueError("Global-info lookup requires an enclosing module
frame")
- match = re.fullmatch(r"([^\[\]]+)\[(\d+)\]", content)
- if match:
- name, index = match.groups()
- return frame.global_infos[name][int(index)]
- selector = re.fullmatch(r"([^:\[\]]+)(?::(\d+)(?::([^:]+))?)?", content)
- if selector is None:
- raise ValueError(f"Invalid global-info reference: {content!r}")
- target, index, _scope = selector.groups()
- ordinal = int(index) if index is not None else 0
- devices = frame.global_infos.get("vdevice", ())
- if target == "vdevice":
- return devices[ordinal]
- for device in devices:
- if device.target.kind.name == target:
- if ordinal == 0:
- return device
- ordinal -= 1
- raise ValueError(f"Global-info device reference was not found:
{content!r}")
+ raise NotImplementedError
def resolve_type_var_(
diff --git a/python/tvm/script/parser/transpile.py
b/python/tvm/script/parser/transpile.py
index a68f286714..b71a67e95c 100644
--- a/python/tvm/script/parser/transpile.py
+++ b/python/tvm/script/parser/transpile.py
@@ -616,11 +616,11 @@ class IRBuilderTranspiler(ast.NodeTransformer):
# X.Tensor(vdevice="cuda:1")
#
# Builder:
- # X.Tensor(vdevice=I.resolve_global_info_("cuda:1"))
+ # X.Tensor(vdevice=X.resolve_global_info_("cuda:1"))
# -------------------------------------------------
value = self.visit(node)
return self._call(
- self.module.infrastructure_name, "resolve_global_info_",
[value], node
+ self.function.dialect_prefix, "resolve_global_info_",
[value], node
)
return self.visit(node)
@@ -649,7 +649,7 @@ class IRBuilderTranspiler(ast.NodeTransformer):
# X.Tensor(("n",), vdevice="cuda:0")
#
# Builder:
- # X.Tensor((X.resolve_type_var_("n"),),
vdevice=I.resolve_global_info_("cuda:0"))
+ # X.Tensor((X.resolve_type_var_("n"),),
vdevice=X.resolve_global_info_("cuda:0"))
# -------------------------------------------------
binding_value = node is self.binding_expression
marker = self._read_constexpr_operand(node)
diff --git a/python/tvm/tirx/script/builder/__init__.py
b/python/tvm/tirx/script/builder/__init__.py
index df83450d41..cb510ad7f3 100644
--- a/python/tvm/tirx/script/builder/__init__.py
+++ b/python/tvm/tirx/script/builder/__init__.py
@@ -69,6 +69,7 @@ from .parser_protocol import (
not_,
or_,
range_,
+ resolve_global_info_,
resolve_type_var_,
return_,
set_mutable_cell_,
diff --git a/python/tvm/tirx/script/builder/parser_protocol.py
b/python/tvm/tirx/script/builder/parser_protocol.py
index e17e19b402..0d6fbc4d7b 100644
--- a/python/tvm/tirx/script/builder/parser_protocol.py
+++ b/python/tvm/tirx/script/builder/parser_protocol.py
@@ -37,6 +37,14 @@ from . import ir as _native
_Span = _base.SpanEntry | _ir.Span | tuple[_ir.SourceName, int, int, int, int]
| None
+def resolve_global_info_(content: Any) -> Any:
+ """Implements
:func:`tvm.script.ir_builder.ir.parser_protocol.resolve_global_info_`.
+
+ TIRx does not define global-info selectors.
+ """
+ raise NotImplementedError("TIRx does not support global-info lookup")
+
+
def _name(value: Any, name: str | None, span: _Span) -> Any:
if name is not None:
_IRBuilder.name(name, value)
diff --git a/tests/python/script/minilang.py b/tests/python/script/minilang.py
index 8ae88c245d..68cc30a5bf 100644
--- a/tests/python/script/minilang.py
+++ b/tests/python/script/minilang.py
@@ -157,7 +157,6 @@ class Language:
ir_module=lambda: Frame(self, "module"),
at_=self.at,
with_at_group_=self.with_at_group,
- resolve_global_info_=self.resolve_global_info,
module_member_=lambda name, value: value,
require_defined=self.require_defined,
annotation_value_=lambda name, value: value,
@@ -182,6 +181,7 @@ class Language:
break_=lambda **kwargs: self.statement("break"),
continue_=lambda **kwargs: self.statement("continue"),
resolve_type_var_=self.resolve_type_var,
+ resolve_global_info_=self.resolve_global_info,
bind_=self.bind,
check_well_formed_=lambda result: None,
emit_=self.emit,
diff --git a/tests/python/script/test_special_parser_protocol.py
b/tests/python/script/test_special_parser_protocol.py
index abb0d8ee1f..ed14fe7eb1 100644
--- a/tests/python/script/test_special_parser_protocol.py
+++ b/tests/python/script/test_special_parser_protocol.py
@@ -18,15 +18,20 @@
A failed constructor must not change the next function's interpretation of
literals.
"""
+import inspect
+import traceback
+
import pytest
+from tvm.script import tirx as T
from tvm.script.parser import protocol_registry as registry
def test_constructor_policy_survives_a_failed_definition(language):
- # A failing definition must not erase registered argument semantics for
the next one.
+ # A registered argument uses the dialect lookup, even after a constructor
failure.
M = language.M
calls = []
+ dialect_infos = {}
first, invalid, second = object(), object(), object()
failure = RuntimeError("constructor failed")
@@ -37,6 +42,10 @@ def
test_constructor_policy_survives_a_failed_definition(language):
raise failure
return device
+ def resolve(name):
+ return dialect_infos[name]
+
+ M.resolve_global_info_ = resolve
M.constructor = constructor
def build():
@@ -47,13 +56,13 @@ def
test_constructor_policy_survives_a_failed_definition(language):
return main
- language.global_infos["mesh"] = first
+ dialect_infos["mesh"] = first
assert build().body == [("emit", first), ("emit", "mesh")]
- language.global_infos["mesh"] = invalid
+ dialect_infos["mesh"] = invalid
with pytest.raises(RuntimeError, match="constructor failed") as caught:
build()
assert caught.value is failure
- language.global_infos["mesh"] = second
+ dialect_infos["mesh"] = second
assert build().body == [("emit", second), ("emit", "mesh")]
assert calls == [first, "mesh", invalid, second, "mesh"]
@@ -77,3 +86,39 @@ def
test_argument_policy_preserves_expression_dtype(language):
assert increment.op == "add"
assert increment.args[0] is n
assert increment.args[1] == 1
+
+
+def test_tirx_rejects_global_info_at_the_source_argument(monkeypatch):
+ # A custom global-info argument must report TIRx's unsupported policy at
its source.
+ @registry.args_policy("T.global_annotation", {"device": "global_info"})
+ def global_annotation(device):
+ return T.int32
+
+ monkeypatch.setattr(T, "global_annotation", global_annotation,
raising=False)
+ with pytest.raises(
+ NotImplementedError, match="TIRx does not support global-info lookup"
+ ) as caught:
+
+ @T.prim_func
+ def main(value: T.global_annotation(device="cuda:0")):
+ T.evaluate(value)
+
+ lines, first =
inspect.getsourcelines(test_tirx_rejects_global_info_at_the_source_argument)
+ index, line = next((i, line) for i, line in enumerate(lines) if "def
main(value:" in line)
+ location = first + index
+ frames = traceback.extract_tb(caught.value.__traceback__)
+ source_frames = [
+ frame for frame in frames if frame.filename == __file__ and
frame.lineno == location
+ ]
+ assert source_frames
+ if getattr(source_frames[-1], "colno", None) is not None:
+ column = line.index('"cuda:0"')
+ assert (
+ source_frames[-1].colno,
+ source_frames[-1].end_lineno,
+ source_frames[-1].end_colno,
+ ) == (
+ column,
+ location,
+ column + len('"cuda:0"'),
+ )