| 96 | # Let's define our ``ProfilingInterpreter`` class: |
| 97 | |
| 98 | class ProfilingInterpreter(Interpreter): |
| 99 | def __init__(self, mod : torch.nn.Module): |
| 100 | # Rather than have the user symbolically trace their model, |
| 101 | # we're going to do it in the constructor. As a result, the |
| 102 | # user can pass in any ``Module`` without having to worry about |
| 103 | # symbolic tracing APIs |
| 104 | gm = torch.fx.symbolic_trace(mod) |
| 105 | super().__init__(gm) |
| 106 | |
| 107 | # We are going to store away two things here: |
| 108 | # |
| 109 | # 1. A list of total runtimes for ``mod``. In other words, we are |
| 110 | # storing away the time ``mod(...)`` took each time this |
| 111 | # interpreter is called. |
| 112 | self.total_runtime_sec : List[float] = [] |
| 113 | # 2. A map from ``Node`` to a list of times (in seconds) that |
| 114 | # node took to run. This can be seen as similar to (1) but |
| 115 | # for specific sub-parts of the model. |
| 116 | self.runtimes_sec : Dict[torch.fx.Node, List[float]] = {} |
| 117 | |
| 118 | ###################################################################### |
| 119 | # Next, let's override our first method: ``run()``. ``Interpreter``'s ``run`` |
| 120 | # method is the top-level entry point for execution of the model. We will |
| 121 | # want to intercept this so that we can record the total runtime of the |
| 122 | # model. |
| 123 | |
| 124 | def run(self, *args) -> Any: |
| 125 | # Record the time we started running the model |
| 126 | t_start = time.time() |
| 127 | # Run the model by delegating back into Interpreter.run() |
| 128 | return_val = super().run(*args) |
| 129 | # Record the time we finished running the model |
| 130 | t_end = time.time() |
| 131 | # Store the total elapsed time this model execution took in the |
| 132 | # ``ProfilingInterpreter`` |
| 133 | self.total_runtime_sec.append(t_end - t_start) |
| 134 | return return_val |
| 135 | |
| 136 | ###################################################################### |
| 137 | # Now, let's override ``run_node``. ``Interpreter`` calls ``run_node`` each |
| 138 | # time it executes a single node. We will intercept this so that we |
| 139 | # can measure and record the time taken for each individual call in |
| 140 | # the model. |
| 141 | |
| 142 | def run_node(self, n : torch.fx.Node) -> Any: |
| 143 | # Record the time we started running the op |
| 144 | t_start = time.time() |
| 145 | # Run the op by delegating back into Interpreter.run_node() |
| 146 | return_val = super().run_node(n) |
| 147 | # Record the time we finished running the op |
| 148 | t_end = time.time() |
| 149 | # If we don't have an entry for this node in our runtimes_sec |
| 150 | # data structure, add one with an empty list value. |
| 151 | self.runtimes_sec.setdefault(n, []) |
| 152 | # Record the total elapsed time for this single invocation |
| 153 | # in the runtimes_sec data structure |
| 154 | self.runtimes_sec[n].append(t_end - t_start) |
| 155 | return return_val |
no outgoing calls
no test coverage detected