Description
Symptom / Motivation
tileops/ops/elementwise.py is a 2681-line monolith hosting:
- 3 umbrella classes (
UnaryOp, BinaryOp, FusedGatedOp at lines 668 / 769 / 866)
- ~60 per-op
*FwdOp classes (24 unary_math + 16 unary_activation + 24 binary + multi-input)
The reduction and normalization families already live as directories (tileops/ops/reduction/, tileops/ops/norm/) with one module per op. Elementwise is the only family where the W2 alignment landed without splitting the host file.
Drift cost: any per-op edit forces every reviewer of elementwise.py to scroll past hundreds of unrelated classes. New scaffolds keep landing in the monolith because that's where their siblings are.
Downstream importers reference per-op classes by their flat path:
tileops/kernels/__init__.py:39 imports BinaryKernel, FusedGatedKernel, UnaryKernel from tileops/kernels/elementwise.py (mirror split needed)
tileops/ops/moe/experts/nopad.py:10 imports SiluAndMulFwdOp from tileops.ops.elementwise
tests/ops/test_elementwise_fp8.py imports several *FwdOp classes from tileops.ops.elementwise
These imports MUST keep working: re-export from the new package's __init__.py so the public path tileops.ops.elementwise.<OpName> is preserved.
Root Cause Analysis
N/A — historical: ops were originally siblings under one umbrella; split deferred until W2 stabilized class shapes.
Related Files
tileops/ops/elementwise.py (split source)
tileops/ops/__init__.py (re-exports)
tileops/kernels/elementwise.py + tileops/kernels/__init__.py (mirror, if W2 hasn't already split kernel side)
tileops/ops/moe/experts/nopad.py, tests/ops/test_elementwise_fp8.py — verify imports still resolve
.claude/rules/code-style.md — __init__.py MUST have explicit __all__ and from .module import Symbol re-exports
Goal
tileops/ops/elementwise.py is replaced by a tileops/ops/elementwise/ package with one module per op (or per tight cluster — e.g. all clamp variants in one file is acceptable). The package __init__.py re-exports every previously-public symbol; downstream imports of tileops.ops.elementwise.<OpName> continue to work without code changes elsewhere.
Plan
- Create
tileops/ops/elementwise/ directory with __init__.py.
- Move each
*FwdOp class to its own module (file names per .claude/rules/code-style.md: lowercase + underscores, multi-letter abbreviations stay lowercase). Group tightly-coupled variants (e.g. clamp.py for the four Clamp ops) when they share helpers.
- Move the umbrella classes
UnaryOp, BinaryOp, FusedGatedOp to _base.py inside the package.
__init__.py re-exports every public name with explicit __all__ and from .module import Symbol form (no star imports).
- Delete the old
tileops/ops/elementwise.py.
- Run
grep -rn "from tileops.ops.elementwise import\|from .elementwise import" across tileops/ tests/ benchmarks/ and confirm every match still resolves.
- Run
pytest tests/ops/test_elementwise* and the elementwise bench smoke gate.
Constraints
- MUST NOT change op runtime behavior. Pure file-layout move.
- MUST NOT change any class signature, docstring (beyond moving), or kernel binding.
- MUST preserve every public import path:
from tileops.ops.elementwise import <Symbol> must keep working for every symbol that was importable before.
- MUST keep umbrella classes (
UnaryOp, BinaryOp, FusedGatedOp) accessible at the same path.
- MUST follow
.claude/rules/code-style.md: explicit __all__, no file-level lint suppressions, intra-package relative imports, cross-package absolute.
- Out of scope: renaming any op class, renaming the manifest entry, splitting kernels (already done — verify, don't redo).
Acceptance Criteria
Description
Symptom / Motivation
tileops/ops/elementwise.pyis a 2681-line monolith hosting:UnaryOp,BinaryOp,FusedGatedOpat lines 668 / 769 / 866)*FwdOpclasses (24 unary_math + 16 unary_activation + 24 binary + multi-input)The reduction and normalization families already live as directories (
tileops/ops/reduction/,tileops/ops/norm/) with one module per op. Elementwise is the only family where the W2 alignment landed without splitting the host file.Drift cost: any per-op edit forces every reviewer of
elementwise.pyto scroll past hundreds of unrelated classes. New scaffolds keep landing in the monolith because that's where their siblings are.Downstream importers reference per-op classes by their flat path:
tileops/kernels/__init__.py:39importsBinaryKernel, FusedGatedKernel, UnaryKernelfromtileops/kernels/elementwise.py(mirror split needed)tileops/ops/moe/experts/nopad.py:10importsSiluAndMulFwdOpfromtileops.ops.elementwisetests/ops/test_elementwise_fp8.pyimports several*FwdOpclasses fromtileops.ops.elementwiseThese imports MUST keep working: re-export from the new package's
__init__.pyso the public pathtileops.ops.elementwise.<OpName>is preserved.Root Cause Analysis
N/A — historical: ops were originally siblings under one umbrella; split deferred until W2 stabilized class shapes.
Related Files
tileops/ops/elementwise.py(split source)tileops/ops/__init__.py(re-exports)tileops/kernels/elementwise.py+tileops/kernels/__init__.py(mirror, if W2 hasn't already split kernel side)tileops/ops/moe/experts/nopad.py,tests/ops/test_elementwise_fp8.py— verify imports still resolve.claude/rules/code-style.md—__init__.pyMUST have explicit__all__andfrom .module import Symbolre-exportsGoal
tileops/ops/elementwise.pyis replaced by atileops/ops/elementwise/package with one module per op (or per tight cluster — e.g. all clamp variants in one file is acceptable). The package__init__.pyre-exports every previously-public symbol; downstream imports oftileops.ops.elementwise.<OpName>continue to work without code changes elsewhere.Plan
tileops/ops/elementwise/directory with__init__.py.*FwdOpclass to its own module (file names per.claude/rules/code-style.md: lowercase + underscores, multi-letter abbreviations stay lowercase). Group tightly-coupled variants (e.g.clamp.pyfor the four Clamp ops) when they share helpers.UnaryOp,BinaryOp,FusedGatedOpto_base.pyinside the package.__init__.pyre-exports every public name with explicit__all__andfrom .module import Symbolform (no star imports).tileops/ops/elementwise.py.grep -rn "from tileops.ops.elementwise import\|from .elementwise import"acrosstileops/ tests/ benchmarks/and confirm every match still resolves.pytest tests/ops/test_elementwise*and the elementwise bench smoke gate.Constraints
from tileops.ops.elementwise import <Symbol>must keep working for every symbol that was importable before.UnaryOp,BinaryOp,FusedGatedOp) accessible at the same path..claude/rules/code-style.md: explicit__all__, no file-level lint suppressions, intra-package relative imports, cross-package absolute.Acceptance Criteria
pytest tests/ops/).tileops/ops/elementwise.pyno longer exists;tileops/ops/elementwise/is a package with__init__.py.python -c "from tileops.ops.elementwise import UnaryOp, BinaryOp, FusedGatedOp"succeeds.python -c "from tileops.ops.elementwise import SiluAndMulFwdOp, ReluFwdOp, AddFwdOp, ExpFwdOp, DivFwdOp"succeeds (the imports referenced in current downstream code).tests/ops/test_elementwise_fp8.pypasses without modification to itsfrom tileops.ops.elementwise import …lines.grep -nE "^# ruff:|^# flake8:" tileops/ops/elementwise/returns nothing (no file-level suppressions introduced).