Skip to content

Commit d756d97

Browse files
authored
Dedup constants in emitter (pytorch#15139)
### Summary Fix bug where if we have two constants with different names but the same data, the second one is not saved and results in missing tensor. (This happened in quant flows). ### Test plan ``` python -m unittest executorch.exir.emit.test.test_emit.TestEmit.test_constant_tagged_tensor_dedup ```
1 parent 7d4db45 commit d756d97

File tree

2 files changed

+108
-5
lines changed

2 files changed

+108
-5
lines changed

exir/emit/_emitter.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,21 @@ def _get_allocation_info(self, spec: TensorSpec) -> AllocationDetails:
372372
)
373373
return allocation_info
374374

375+
def _save_to_external_constant_map(
376+
self,
377+
fqn: str,
378+
buffer_idx: int,
379+
constant_tag: str,
380+
) -> None:
381+
"""
382+
Saves external constant to the map.
383+
"""
384+
# buffer data should be in the external_constant_buffer already.
385+
assert buffer_idx < len(self.program_state.external_constant_buffer)
386+
if constant_tag not in self.program_state.external_constant_map:
387+
self.program_state.external_constant_map[constant_tag] = {}
388+
self.program_state.external_constant_map[constant_tag][fqn] = buffer_idx
389+
375390
def _save_new_const_tensor(
376391
self,
377392
spec: TensorSpec,
@@ -403,11 +418,9 @@ def _save_new_const_tensor(
403418
buffer_idx = len(self.program_state.external_constant_buffer)
404419
self.program_state.external_constant_hash[hashed] = buffer_idx
405420
self.program_state.external_constant_buffer.append(buffer_data)
406-
if constant_tag not in self.program_state.external_constant_map:
407-
self.program_state.external_constant_map[constant_tag] = {}
408-
self.program_state.external_constant_map[constant_tag][
409-
spec.extra_tensor_info.fully_qualified_name # pyre-ignore Undefined attribute [16]: `Optional` has no attribute `fully_qualified_name`.
410-
] = buffer_idx
421+
self._save_to_external_constant_map(
422+
spec.extra_tensor_info.fully_qualified_name, buffer_idx, constant_tag
423+
)
411424
# Tensor is mutable with initial state. Place into mutable segment
412425
elif allocation_info:
413426
buffer_idx = len(self.program_state.mutable_buffer)
@@ -466,6 +479,19 @@ def _tensor_spec_to_evalue(
466479
and spec.extra_tensor_info.location == TensorDataLocation.EXTERNAL
467480
):
468481
buffer_idx = self.program_state.external_constant_hash.get(hashed, -1)
482+
if buffer_idx != -1:
483+
# This constant already exists in the external_constant_buffer,
484+
# And doesn't need to be duplicated. However, the fqn is unique
485+
# and should be added. ie, we have the case: fqn0->data, fqn1->data.
486+
# When buffer_idx == 1, the data is new and added with
487+
# `_save_new_const_tensor` below.
488+
assert spec.extra_tensor_info.fully_qualified_name is not None
489+
assert constant_tag is not None
490+
self._save_to_external_constant_map(
491+
spec.extra_tensor_info.fully_qualified_name,
492+
buffer_idx,
493+
constant_tag,
494+
)
469495
else:
470496
buffer_idx = self.program_state.cached_spec_hash_values.get(hashed, -1)
471497

exir/emit/test/test_emit.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,83 @@ def forward(self, x):
17191719
self.assertEqual(external_map["linear.weight"], 0)
17201720
self.assertEqual(external_map["linear.bias"], 1)
17211721

1722+
def test_constant_tagged_tensor_dedup(self) -> None:
1723+
class ConstantModule(nn.Module):
1724+
def __init__(self):
1725+
super().__init__()
1726+
constant = torch.tensor([1.0, 2.0, 3.0])
1727+
1728+
# Register the same value with two different names as persistent buffers
1729+
self.register_buffer("c0", constant.clone(), persistent=True)
1730+
self.register_buffer("c1", constant.clone(), persistent=True)
1731+
1732+
def forward(self, x):
1733+
return x + self.c0 + self.c1
1734+
1735+
model = to_edge(
1736+
export(ConstantModule(), (torch.ones(1, 3),), strict=True)
1737+
).to_executorch(
1738+
config=ExecutorchBackendConfig(
1739+
external_constants=True,
1740+
)
1741+
)
1742+
emitter_output = model._emitter_output
1743+
# constant_buffer is empty besides the non-constant placeholder 0.
1744+
self.assertEqual(len(emitter_output.program.constant_buffer), 1)
1745+
# only one item in the external constant buffer.
1746+
self.assertEqual(len(emitter_output.external_constant_buffer), 1)
1747+
# Setting external_constants=True, saves all constants to the key
1748+
# '_default_external_constant'.
1749+
external_map = emitter_output.external_constant_map[
1750+
"_default_external_constant"
1751+
]
1752+
self.assertEqual(len(external_map), 2)
1753+
self.assertEqual(external_map["c0"], 0)
1754+
self.assertEqual(external_map["c1"], 0)
1755+
1756+
def test_constant_tagged_tensor_dedup_2(self) -> None:
1757+
class ConstantModule(nn.Module):
1758+
def __init__(self):
1759+
super().__init__()
1760+
constant0_4 = torch.tensor([1.0, 2.0, 3.0])
1761+
constant4_5 = torch.tensor([2.0, 3.0, 4.0])
1762+
1763+
# Register the same value with two different names as persistent buffers
1764+
self.register_buffer("c0", constant0_4.clone(), persistent=True)
1765+
self.register_buffer("c1", constant0_4.clone(), persistent=True)
1766+
self.register_buffer("c2", constant0_4.clone(), persistent=True)
1767+
self.register_buffer("c3", constant0_4.clone(), persistent=True)
1768+
self.register_buffer("c4", constant4_5.clone(), persistent=True)
1769+
self.register_buffer("c5", constant4_5.clone(), persistent=True)
1770+
1771+
def forward(self, x):
1772+
return x + self.c0 + self.c1 + self.c2 + self.c3 + self.c4 + self.c5
1773+
1774+
model = to_edge(
1775+
export(ConstantModule(), (torch.ones(1, 3),), strict=True)
1776+
).to_executorch(
1777+
config=ExecutorchBackendConfig(
1778+
external_constants=True,
1779+
)
1780+
)
1781+
emitter_output = model._emitter_output
1782+
# constant_buffer is empty besides the non-constant placeholder 0.
1783+
self.assertEqual(len(emitter_output.program.constant_buffer), 1)
1784+
# Two items in the external constant buffer.
1785+
self.assertEqual(len(emitter_output.external_constant_buffer), 2)
1786+
# Setting external_constants=True, saves all constants to the key
1787+
# '_default_external_constant'.
1788+
external_map = emitter_output.external_constant_map[
1789+
"_default_external_constant"
1790+
]
1791+
self.assertEqual(len(external_map), 6)
1792+
self.assertEqual(external_map["c0"], 0)
1793+
self.assertEqual(external_map["c1"], 0)
1794+
self.assertEqual(external_map["c2"], 0)
1795+
self.assertEqual(external_map["c3"], 0)
1796+
self.assertEqual(external_map["c4"], 1)
1797+
self.assertEqual(external_map["c5"], 1)
1798+
17221799
def test_delegate_deduplicate(self) -> None:
17231800
class SharedModule(torch.nn.Module):
17241801
def __init__(self):

0 commit comments

Comments
 (0)