diff --git a/CHANGELOG.md b/CHANGELOG.md index d15ac5bafd..75858107e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ This file contains the changelog for the Deeploy project. The changelog is divid - Aligned CLI commands across the project - Added @runwangdl as a code owner - Skip emitting duplicate `testInputVector` data for inputs placed in L3 (loaded at runtime from the readfs hex instead), reducing test binary size +- Tiler (`TilerExtension`, `MemoryScheduler`) and `NetworkContext.dealiasBuffer` use directed `VariableBuffer.alias_of` instead of the legacy `_alias` attribute (#201) ### Fixed - Fix Neureka's output-channels subtile size (in ConvTemplate) and Dense/DW/PW tile constraints @@ -85,6 +86,7 @@ This file contains the changelog for the Deeploy project. The changelog is divid ### Removed - removed experimental `enable3x3` flag, from Neureka Engine. Now, 3x3 mode is enabled by default. - `testDMA.py` was an old test; we now have `test_dmas.py` instead. +- Legacy `_alias` workaround in Generic/PULPOpen `ReshapeTemplate` (tiling now uses `alias_of`) ## Release v0.2.1 (2026-02-05) [#158](https://github.com/pulp-platform/Deeploy/pull/158) diff --git a/Deeploy/DeeployTypes.py b/Deeploy/DeeployTypes.py index d054ffea8c..ef000c805a 100644 --- a/Deeploy/DeeployTypes.py +++ b/Deeploy/DeeployTypes.py @@ -259,6 +259,9 @@ def __init__(self, name: str = '', shape = [1], aliases: Optional[List[str]] = N self.is_output: bool = False self.aliases: Set[str] = set(aliases) if aliases is not None else set() + # Directed "I am an alias of these storage ancestors" (tiling / dealiasBuffer). + # Distinct from symmetric self.aliases used by has_live_aliases. + self.alias_of: Set[str] = set() def _bufferRepresentation(self) -> Dict: return {"type": self._instance, "name": self.name, "size": int(np.prod(self.shape))} @@ -563,9 +566,14 @@ def dealiasBuffer(self, name: str) -> str: """ seenAliases: Set[str] = set() alias = self.lookup(name) - while hasattr(alias, "_alias"): + assert isinstance(alias, VariableBuffer) + while alias.alias_of: seenAliases.add(alias.name) - alias = self.lookup(alias._alias) + # Reshape and other current aliasers have a single storage parent. + # Pick a deterministic parent if multiple are ever present. + parentName = sorted(alias.alias_of)[0] + alias = self.lookup(parentName) + assert isinstance(alias, VariableBuffer) assert alias.name not in seenAliases, "Circular aliasing detected!" return alias.name diff --git a/Deeploy/Targets/Generic/Templates/ReshapeTemplate.py b/Deeploy/Targets/Generic/Templates/ReshapeTemplate.py index e4cb01381c..dae49b1fb0 100644 --- a/Deeploy/Targets/Generic/Templates/ReshapeTemplate.py +++ b/Deeploy/Targets/Generic/Templates/ReshapeTemplate.py @@ -33,12 +33,8 @@ def alignToContext(self, ctxt: NetworkContext, # Link aliases to each buffer bufferIn.aliases.add(bufferOut.name) bufferOut.aliases.add(bufferIn.name) - - # Tiling still reads the legacy single-valued `_alias` attribute - # (TilerExtension / MemoryScheduler). Set it here so platforms that - # rely on Reshape pointer-passthrough during tiling don't each need - # to carry the same workaround in a subclass. - bufferOut._alias = bufferIn.name + # Directed storage parent for tiling / dealiasBuffer + bufferOut.alias_of.add(bufferIn.name) return ctxt, operatorRepresentation, [] diff --git a/Deeploy/Targets/PULPOpen/Templates/ReshapeTemplate.py b/Deeploy/Targets/PULPOpen/Templates/ReshapeTemplate.py index c37fad2ee7..fbed2e56d1 100644 --- a/Deeploy/Targets/PULPOpen/Templates/ReshapeTemplate.py +++ b/Deeploy/Targets/PULPOpen/Templates/ReshapeTemplate.py @@ -2,9 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 -from typing import Dict, List, Tuple - -from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation, VariableBuffer from Deeploy.Targets.Generic.Templates.ReshapeTemplate import _ReshapeTemplate as _GenericReshapeTemplate @@ -13,24 +10,6 @@ class _ReshapeTemplate(_GenericReshapeTemplate): def __init__(self, templateStr): super().__init__(templateStr) - def alignToContext(self, ctxt: NetworkContext, - operatorRepresentation: OperatorRepresentation) -> Tuple[NetworkContext, Dict, List[str]]: - - ctxt, operatorRepresentation, _ = super().alignToContext(ctxt, operatorRepresentation) - - # Get buffers - bufferIn = ctxt.lookup(operatorRepresentation['data_in']) - assert isinstance(bufferIn, VariableBuffer) - - bufferOut = ctxt.lookup(operatorRepresentation['data_out']) - assert isinstance(bufferOut, VariableBuffer) - - # HACK: Tiling wasn't updated in the Fix aliasing PR so we have to still - # set the _alias argument - bufferOut._alias = bufferIn.name - - return ctxt, operatorRepresentation, [] - referenceTemplate = _ReshapeTemplate(""" // Reshape (Name: ${nodeName}, Op: ${nodeOp}) diff --git a/Deeploy/TilingExtension/MemoryScheduler.py b/Deeploy/TilingExtension/MemoryScheduler.py index e46f50e6f7..dfa5c9e95e 100644 --- a/Deeploy/TilingExtension/MemoryScheduler.py +++ b/Deeploy/TilingExtension/MemoryScheduler.py @@ -301,11 +301,11 @@ def filterTensorMemoryConstraint(ctxt: NetworkContext, tensorMemoryConstraint: T buffer = ctxt.lookup(tensorName) # JUNGVI: Buffer targeted by alias have to say alive as long as their "aliasers" - if hasattr(buffer, "_alias"): - alias = buffer._alias - if alias in tensorLifetimeMap.keys(): - prevLifetime = tensorLifetimeMap[alias] - tensorLifetimeMap[alias] = tuple((prevLifetime[0], stepIdx)) + if buffer.alias_of: + for alias in buffer.alias_of: + if alias in tensorLifetimeMap.keys(): + prevLifetime = tensorLifetimeMap[alias] + tensorLifetimeMap[alias] = tuple((prevLifetime[0], stepIdx)) if tensorName in tensorLifetimeMap.keys(): prevLifetime = tensorLifetimeMap[tensorName] @@ -369,7 +369,7 @@ def _buildCostVector(self, ctxt, graph, tensorMap, memoryLevel): cost = wordCost * c.multiBufferCoefficient # SCHEREMO: In-place operator outputs are "costless" whenever their input is in the same pattern - if hasattr(ctxt.lookup(node), "_alias") and ctxt.lookup(node)._alias in neighbors: + if ctxt.lookup(node).alias_of and any(a in neighbors for a in ctxt.lookup(node).alias_of): cost = 0 costVector.append(cost) @@ -655,9 +655,10 @@ def permMatrix2permList(permMatrix: np.ndarray) -> List[int]: continue # SCHEREMO: Don't fully unroll aliases here - this is pattern-sensitive! - if hasattr(_buffer, "_alias") and _buffer._alias in blockNames: - _alias = ctxt.lookup(memoryBlock.name)._alias - aliasedBlocks.append((memoryBlock, _alias)) + inPatternParents = [a for a in _buffer.alias_of if a in blockNames] + if inPatternParents: + # Prefer a deterministic immediate parent when multiple exist + aliasedBlocks.append((memoryBlock, sorted(inPatternParents)[0])) continue upperIdx = blockIdx diff --git a/Deeploy/TilingExtension/TilerExtension.py b/Deeploy/TilingExtension/TilerExtension.py index 2186d4d4c4..f41a093a1c 100644 --- a/Deeploy/TilingExtension/TilerExtension.py +++ b/Deeploy/TilingExtension/TilerExtension.py @@ -296,7 +296,8 @@ def _convertCtxtToStaticSchedule(self, ctxt: NetworkContext, _buffer = ctxt.lookup(node.name) # SCHEREMO: If alias buffers have zero cost, they don't contribute to the currentMax and their addrSpace is None - if hasattr(_buffer, "_alias") and (ctxt.is_global(_buffer._alias) or _buffer._alias in blockNames): + if _buffer.alias_of and (any(ctxt.is_global(a) for a in _buffer.alias_of) + or any(a in blockNames for a in _buffer.alias_of)): continue currentMax = max(currentMax, node._addrSpace[1]) @@ -333,10 +334,10 @@ def _convertCtxtToStaticSchedule(self, ctxt: NetworkContext, if _buffer._memoryLevel != memoryLevel: continue - if hasattr(_buffer, "_alias") and ctxt.is_global(_buffer._alias): + if _buffer.alias_of and any(ctxt.is_global(a) for a in _buffer.alias_of): continue - if hasattr(_buffer, "_alias") and _buffer._alias in blockNames: + if _buffer.alias_of and any(a in blockNames for a in _buffer.alias_of): alias = ctxt.dealiasBuffer(tensorName) aliasNodes = [node for node in nodeList if node.name == alias] diff --git a/DeeployTest/testSchedulingExtension.py b/DeeployTest/testSchedulingExtension.py index be77ecce53..0fe77c1608 100644 --- a/DeeployTest/testSchedulingExtension.py +++ b/DeeployTest/testSchedulingExtension.py @@ -195,9 +195,7 @@ def validateDynamicMemoryLayoutSolution(ctxt: NetworkContext, tilingSchedule: Ti _buffer = ctxt.lookup(block.name) for other in otherBlocks: _otherBuffer = ctxt.lookup(other.name) - if (hasattr(_buffer, "_alias") - and _buffer._alias == other.name) or (hasattr(_otherBuffer, "_alias") - and _otherBuffer._alias == block.name): + if (other.name in _buffer.alias_of) or (block.name in _otherBuffer.alias_of): collisions.append(False) continue diff --git a/DeeployTest/testTypes.py b/DeeployTest/testTypes.py index 9858015229..9c8018172e 100644 --- a/DeeployTest/testTypes.py +++ b/DeeployTest/testTypes.py @@ -224,6 +224,34 @@ def testPointerTypeEquivalence(): return True +def testDealiasBufferUsesAliasOf(): + """Regression for #201: dealiasBuffer walks directed alias_of, not legacy _alias.""" + ctxt = NetworkContext(VariableBuffer, ConstantBuffer, StructBuffer, TransientBuffer) + + bufferIn = VariableBuffer("reshape_in", shape = [4, 4]) + bufferOut = VariableBuffer("reshape_out", shape = [16]) + ctxt.add(bufferIn, "local") + ctxt.add(bufferOut, "local") + + bufferIn.aliases.add(bufferOut.name) + bufferOut.aliases.add(bufferIn.name) + bufferOut.alias_of.add(bufferIn.name) + + assert not hasattr(bufferOut, "_alias"), "legacy _alias must not be required for dealiasing" + assert ctxt.dealiasBuffer(bufferOut.name) == bufferIn.name + assert ctxt.dealiasBuffer(bufferIn.name) == bufferIn.name + + bufferOut2 = VariableBuffer("reshape_out2", shape = [2, 8]) + ctxt.add(bufferOut2, "local") + bufferOut.aliases.add(bufferOut2.name) + bufferOut2.aliases.add(bufferOut.name) + bufferOut2.alias_of.add(bufferOut.name) + + assert ctxt.dealiasBuffer(bufferOut2.name) == bufferIn.name + + return True + + if __name__ == "__main__": testImmediateSerialization() testImmediatePromotion() @@ -239,3 +267,4 @@ def testPointerTypeEquivalence(): testPointerSerialization() testPointerPromotion() testPointerTypeEquivalence() + testDealiasBufferUsesAliasOf()