|
30 | 30 | from executorch.exir.capture._config import ExecutorchBackendConfig |
31 | 31 | from executorch.exir.delegate import executorch_call_delegate |
32 | 32 | from executorch.exir.dialects._ops import ops as exir_ops |
| 33 | +from executorch.exir.pass_base import ExportedProgramPassBase, ExportedProgramPassResult |
33 | 34 | from executorch.exir.passes import MemoryPlanningPass |
34 | 35 | from executorch.exir.passes.propagate_device_pass import ( |
35 | 36 | _get_target_device_from_compile_specs, |
@@ -1478,6 +1479,134 @@ def forward(self, a, b): |
1478 | 1479 | "CPU placeholder", |
1479 | 1480 | ) |
1480 | 1481 |
|
| 1482 | + def test_constant_feeding_device_delegate_is_copied(self): |
| 1483 | + """A constant feeding a device delegate gets an _h2d_copy into planned device |
| 1484 | + memory, with or without skip_h2d_for_method_inputs: its data lives in the |
| 1485 | + program on the host, so it is not a method input the caller can place.""" |
| 1486 | + |
| 1487 | + class Model(torch.nn.Module): |
| 1488 | + def __init__(self): |
| 1489 | + super().__init__() |
| 1490 | + self.register_buffer("offset", torch.randn(2, 2)) |
| 1491 | + |
| 1492 | + def forward(self, a): |
| 1493 | + return torch.add(a, self.offset) |
| 1494 | + |
| 1495 | + model = Model() |
| 1496 | + inputs = (torch.randn(2, 2),) |
| 1497 | + for skip in (False, True): |
| 1498 | + et_config = ExecutorchBackendConfig( |
| 1499 | + emit_stacktrace=False, |
| 1500 | + propagate_device_config=PropagateDeviceConfig( |
| 1501 | + skip_h2d_for_method_inputs=skip |
| 1502 | + ), |
| 1503 | + enable_non_cpu_memory_planning=True, |
| 1504 | + ) |
| 1505 | + for pipeline, program, gm in self._get_executorch_program( |
| 1506 | + model, inputs, DeviceAwarePartitioner("cuda:0"), et_config |
| 1507 | + ): |
| 1508 | + with self.subTest(pipeline=pipeline, skip=skip): |
| 1509 | + nodes = _collect_device_copy_nodes(gm) |
| 1510 | + # Input a is copied only when the caller cannot place it. |
| 1511 | + self.assertEqual(len(nodes.h2d_nodes), 1 if skip else 2) |
| 1512 | + cuda_ph, _ = self._collect_placeholders_by_device(gm) |
| 1513 | + self.assertEqual(len(cuda_ph), int(skip)) |
| 1514 | + self.assertTrue(all(not n.meta["spec"].const for n in cuda_ph)) |
| 1515 | + copies = [n.meta["spec"] for n in nodes.h2d_nodes] |
| 1516 | + self.assertTrue(all(not spec.const for spec in copies)) |
| 1517 | + self._assert_nodes_buffer_device( |
| 1518 | + nodes.h2d_nodes, program, DeviceType.CUDA, pipeline, "H2D copy" |
| 1519 | + ) |
| 1520 | + |
| 1521 | + def test_constant_copy_keeps_stored_layout(self): |
| 1522 | + """The copy of a constant moves its bytes as they are, so it must use the |
| 1523 | + layout the constant is stored in, for a transposed parameter as well as |
| 1524 | + a channels-last one.""" |
| 1525 | + |
| 1526 | + class Model(torch.nn.Module): |
| 1527 | + def __init__(self, weight): |
| 1528 | + super().__init__() |
| 1529 | + self.weight = torch.nn.Parameter(weight) |
| 1530 | + |
| 1531 | + def forward(self, a): |
| 1532 | + return torch.add(a, self.weight) |
| 1533 | + |
| 1534 | + weights = { |
| 1535 | + "transposed": torch.randn(3, 2).t(), |
| 1536 | + "channels_last": torch.randn(1, 2, 3, 3).to( |
| 1537 | + memory_format=torch.channels_last |
| 1538 | + ), |
| 1539 | + } |
| 1540 | + for name, weight in weights.items(): |
| 1541 | + et_config = ExecutorchBackendConfig( |
| 1542 | + emit_stacktrace=False, |
| 1543 | + propagate_device_config=PropagateDeviceConfig( |
| 1544 | + skip_h2d_for_method_inputs=True |
| 1545 | + ), |
| 1546 | + enable_non_cpu_memory_planning=True, |
| 1547 | + ) |
| 1548 | + for pipeline, _program, gm in self._get_executorch_program( |
| 1549 | + Model(weight), |
| 1550 | + (torch.randn(weight.shape),), |
| 1551 | + DeviceAwarePartitioner("cuda:0"), |
| 1552 | + et_config, |
| 1553 | + ): |
| 1554 | + with self.subTest(pipeline=pipeline, weight=name): |
| 1555 | + nodes = _collect_device_copy_nodes(gm) |
| 1556 | + self.assertEqual(len(nodes.h2d_nodes), 1) |
| 1557 | + h2d = nodes.h2d_nodes[0] |
| 1558 | + # The emitter rewrites the constant's spec to the stored layout. |
| 1559 | + stored = h2d.args[0].meta["spec"] |
| 1560 | + self.assertEqual(h2d.meta["spec"].dim_order, stored.dim_order) |
| 1561 | + self.assertEqual( |
| 1562 | + tuple(h2d.meta["spec"].stride), tuple(stored.stride) |
| 1563 | + ) |
| 1564 | + |
| 1565 | + def test_constant_with_unstored_layout_is_refused(self): |
| 1566 | + """A constant that is neither contiguous nor channels-last is stored |
| 1567 | + contiguous, so its copy would not match the stored data. A pass that runs |
| 1568 | + after to_edge normalized the constants can create one, and lowering then |
| 1569 | + fails instead of producing wrong results.""" |
| 1570 | + |
| 1571 | + class StoreWeightTransposed(ExportedProgramPassBase): |
| 1572 | + def call(self, ep): |
| 1573 | + ep.state_dict["w"] = torch.nn.Parameter( |
| 1574 | + ep.state_dict["w"].t().contiguous().t() |
| 1575 | + ) |
| 1576 | + for node in ep.graph.nodes: |
| 1577 | + if node.name == "p_w": |
| 1578 | + node.meta["val"] = node.meta["val"].t().contiguous().t() |
| 1579 | + return ExportedProgramPassResult(ep, True) |
| 1580 | + |
| 1581 | + class Model(torch.nn.Module): |
| 1582 | + def __init__(self): |
| 1583 | + super().__init__() |
| 1584 | + self.w = torch.nn.Parameter(torch.randn(2, 3)) |
| 1585 | + |
| 1586 | + def forward(self, a): |
| 1587 | + return torch.add(a, self.w) |
| 1588 | + |
| 1589 | + for skip in (False, True): |
| 1590 | + edge = to_edge_transform_and_lower( |
| 1591 | + export(Model(), (torch.randn(2, 3),)), |
| 1592 | + transform_passes=[StoreWeightTransposed()], |
| 1593 | + partitioner=[DeviceAwarePartitioner("cuda:0")], |
| 1594 | + ) |
| 1595 | + et_config = ExecutorchBackendConfig( |
| 1596 | + emit_stacktrace=False, |
| 1597 | + propagate_device_config=PropagateDeviceConfig( |
| 1598 | + skip_h2d_for_method_inputs=skip |
| 1599 | + ), |
| 1600 | + enable_non_cpu_memory_planning=True, |
| 1601 | + ) |
| 1602 | + with self.subTest(skip=skip): |
| 1603 | + with self.assertRaisesRegex( |
| 1604 | + RuntimeError, |
| 1605 | + "Constant 'p_w' feeds a delegate on cuda:0, but it is neither " |
| 1606 | + "contiguous nor channels-last", |
| 1607 | + ): |
| 1608 | + edge.to_executorch(et_config) |
| 1609 | + |
1481 | 1610 | def test_tensorspec_repr_includes_device(self): |
1482 | 1611 | spec = TensorSpec(dtype=torch.float32, shape=torch.Size([2, 3])) |
1483 | 1612 | repr_str = repr(spec) |
|
0 commit comments