Jlm
Loading...
Searching...
No Matches
decouple-mem-state.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2025 David Metz <david.c.metz@ntnu.no>
3 * See COPYING for terms of redistribution.
4 */
5
11#include <jlm/hls/ir/hls.hpp>
12#include <jlm/hls/util/view.hpp>
19#include <jlm/rvsdg/node.hpp>
21#include <jlm/rvsdg/theta.hpp>
23#include <jlm/rvsdg/view.hpp>
24
25#include <algorithm>
26#include <deque>
27
28namespace jlm::hls
29{
30
31static rvsdg::Output *
33 rvsdg::Input * state_edge,
34 std::vector<rvsdg::SimpleNode *> & mem_ops,
35 bool modify);
36
37static rvsdg::Output *
42 rvsdg::Output * end)
43{
45 while (true)
46 {
47 // make sure we make progress
50 // terminate once desired point is reached
51 if (state_edge->origin() == end)
52 {
53 return end;
54 }
56 JLM_ASSERT(new_edge_user->region() == state_edge->region());
57 if (auto rr = dynamic_cast<jlm::rvsdg::RegionResult *>(state_edge))
58 {
59 return rr->output();
60 }
62 {
63 auto si = util::assertedCast<rvsdg::StructuralInput>(state_edge);
64 auto arg = si->arguments.begin().ptr();
65 std::vector<rvsdg::SimpleNode *> mem_ops;
67 if (std::count(mem_ops.begin(), mem_ops.end(), target_call))
68 {
69 // only route new edge through if target is contained
70 auto new_out = ln->AddLoopVar(new_edge);
71 new_edge_user->divert_to(new_out);
72 auto new_in = util::assertedCast<rvsdg::StructuralInput>(get_mem_state_user(new_edge));
74 out
75 == trace_edge(
77 new_in->arguments.begin().ptr(),
79 end));
82 }
84 JLM_ASSERT(out->region() == state_edge->region());
86 continue;
87 }
88
89 auto si = state_edge;
91 auto new_si = new_edge_user;
98 ->loop) // this is an example of why preserving structural nodes would be nice
99 {
100 // start of gamma
101 auto nbr = BranchOperation::create(*sn->input(0)->origin(), *new_edge);
102 auto nmux = MuxOperation::create(*sn->input(0)->origin(), nbr, false)[0];
103 new_edge_user->divert_to(nmux);
104 rvsdg::Output * out = nullptr;
105 for (size_t i = 0; i < sn->noutputs(); ++i)
106 {
107 out = trace_edge(get_mem_state_user(sn->output(i)), nbr[i], target_call, end);
108 }
112 new_edge = nmux;
113 }
114 else if (branchOperation)
115 {
116 // end of loop
119 return util::assertedCast<rvsdg::RegionResult>(get_mem_state_user(sn->output(0)))->output();
120 }
121 else if (muxOperation && !muxOperation->loop)
122 {
123 // end of gamma
125 return sn->output(0);
126 }
127 else if (muxOperation)
128 {
129 // start of theta
132 state_edge = get_mem_state_user(sn->output(0));
133 new_edge = new_sn->output(0);
134 }
136 {
137 // start of loop
139 state_edge = get_mem_state_user(sn->output(0));
140 new_edge = new_sn->output(0);
141 }
143 {
144 rvsdg::Output * after_merge = nullptr;
145 for (size_t i = 0; i < sn->noutputs(); ++i)
146 {
147 // pick right edge by searching for target
148 std::vector<rvsdg::SimpleNode *> mem_ops;
152 if (std::count(mem_ops.begin(), mem_ops.end(), target_call))
153 {
154 auto out = trace_edge(get_mem_state_user(sn->output(i)), new_edge, target_call, end);
156 }
157 else
158 {
159 // nothing relevant below the split - can just ignore it
160 }
161 }
163 new_edge = new_edge_user->origin();
164 }
165 else if (
168 {
169 // we did not split the new state
170 return sn->output(0);
171 }
173 {
174 state_edge = get_mem_state_user(sn->output(si->index()));
175 }
177 {
178 state_edge = get_mem_state_user(sn->output(1));
179 }
181 {
182 auto state_origin = state_edge->origin();
183 if (sn == target_call)
184 {
185 // move decouple call to new edge
186 state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
187 state_edge->divert_to(state_origin);
188 si->divert_to(new_edge);
189 new_edge_user->divert_to(sn->output(sn->noutputs() - 1));
190 new_edge = new_edge_user->origin();
191 }
192 else
193 {
194 state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
195 }
196 }
197 else
198 {
199 JLM_UNREACHABLE("whoops");
200 }
201 }
202}
203
204static void
206 std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> & outstanding_dec_reqs,
207 std::vector<rvsdg::SimpleNode *> & mem_ops,
210{
211 JLM_ASSERT(state_edge_before->region() == state_edge_after->region());
212 // the reqs we encountered
213 for (auto op : mem_ops)
214 {
215 if (is_dec_req(op))
216 {
217 outstanding_dec_reqs.push_back(std::make_tuple(op, state_edge_before));
218 }
219 }
220 for (auto op : mem_ops)
221 {
223 {
224 // can't handle things if there is a store on the edge in the same loop/gamma
225 return;
226 }
227 }
228 for (auto resp : mem_ops)
229 {
230 if (!is_dec_res(resp))
231 continue;
232 auto res_constant = trace_constant(resp->input(1)->origin());
235 {
236 auto req_constant = trace_constant(req->input(1)->origin());
237 if (*req_constant == *res_constant)
238 {
239 // we found a match and can split the req off the state edge between input and output
240 // in this area both the req and resp can run separate from the main edge
242 state_edge_req->divert_to(split_outputs[0]);
243 JLM_ASSERT(state_edge_after->region() == split_outputs[1]->region());
245 std::vector<rvsdg::Output *> operands(
248 after_user->divert_to(merge_out);
251 }
252 }
253 }
254}
255
256static void
258 std::vector<rvsdg::SimpleNode *> & mem_ops,
261{
262 // the idea here is that if there is only one memory operation, and no other memory
263 // operations/staet gates on a state edge in a loop we can remove the backedge part and treat the
264 // edge like a loop constant, albeit with an output this will especially be important for stores
265 // that have a response.
266 // TODO: should this also be enabled for just memory state gates?
267 if (mem_ops.size() == 1
270 {
271 // before and after belong to same loop node
277 }
278}
279
280static rvsdg::Output *
283 std::vector<rvsdg::SimpleNode *> & mem_ops,
284 bool modify)
285{
286 // we use input so we can handle the scenario of a store having an extra user to deq from addrq
287 // things we can encounter:
288 // * region result:
289 // * return associated output
290 // * loop-node:
291 // * follow subregion
292 // * check which subset of dec_req, dec_resp and store is contained using handle_structural
293 // * converted gamma:
294 // * need to follow all sub-paths
295 // * begins with branch with !br->loop
296 // * follow_state_edge
297 // * ends with mux
298 // * return mux output
299 // * check which subset of dec_req, dec_resp and store is contained using handle_structural
300 // * mem state split/merge:
301 // * follow_state_edge/return merge output
302 // * load/state-gate
303 // * continue on state output
304 // * store/call
305 // * add to mem_ops
306 // * continue on state output
307 // * special case for store - can have multiple users because of addr_deq
308 // this tracks decouple requests that have not been handled yet
309 std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> outstanding_dec_reqs;
310 while (true)
311 {
312 if (auto rr = dynamic_cast<jlm::rvsdg::RegionResult *>(state_edge))
313 {
314 return rr->output();
315 }
317 {
318 std::vector<rvsdg::SimpleNode *> loop_mem_ops;
319 auto si = jlm::util::assertedCast<rvsdg::StructuralInput>(state_edge);
320 auto arg = si->arguments.begin().ptr();
322 // get this here before the graph is modified by handle_structural
324 if (modify)
325 {
328 }
330 mem_ops.insert(mem_ops.cend(), loop_mem_ops.begin(), loop_mem_ops.end());
331 continue;
332 }
333 auto si = state_edge;
340 ->loop) // this is an example of why preserving structural nodes would be nice
341 {
342 std::vector<rvsdg::SimpleNode *> gamma_mem_ops;
343 // start of gamma
344 rvsdg::Output * out = nullptr;
345 for (size_t i = 0; i < sn->noutputs(); ++i)
346 {
348 }
351 // get this here before the graph is modified by handle_structural
353 if (modify)
356 mem_ops.insert(mem_ops.cend(), gamma_mem_ops.begin(), gamma_mem_ops.end());
357 }
358 else if (branchOperation)
359 {
360 // end of loop
362 return util::assertedCast<rvsdg::RegionResult>(get_mem_state_user(sn->output(0)))->output();
363 }
364 else if (muxOperation && !muxOperation->loop)
365 {
366 // end of gamma
367 return sn->output(0);
368 }
369 else if (muxOperation)
370 {
371 // start of theta
373 state_edge = get_mem_state_user(sn->output(0));
374 }
376 {
377 // start of theta
378 state_edge = get_mem_state_user(sn->output(0));
379 }
380 else if (
383 {
384 for (size_t i = 0; i < sn->noutputs(); ++i)
385 {
389
393 }
394 }
395 else if (
396
399 {
400 return sn->output(0);
401 }
403 {
404 mem_ops.push_back(sn);
405 state_edge = get_mem_state_user(sn->output(si->index()));
406 }
408 {
409 mem_ops.push_back(sn);
410 state_edge = get_mem_state_user(sn->output(1));
411 }
413 {
414 mem_ops.push_back(sn);
415 state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
416 }
418 {
419 mem_ops.push_back(sn);
420 JLM_ASSERT(sn->output(0)->nusers() == 1);
421 state_edge = get_mem_state_user(sn->output(0));
422 // handle case of store that has one edge going off to deq
424 {
425 // output 0 is the normal edge
426 state_edge =
428 }
429 }
430 else
431 {
432 JLM_UNREACHABLE("whoops");
433 }
434 }
435}
436
437void
439{
442 auto si = util::assertedCast<rvsdg::StructuralInput>(loop_state_input);
443 auto arg = si->arguments.begin().ptr();
444 auto user = get_mem_state_user(arg);
449 *mux_node->input(0)->origin(),
450 *mux_node->input(1)->origin())[0];
451 mux_node->output(0)->divert_users(lcb_out);
452 JLM_ASSERT(mux_node->IsDead());
454}
455
456static void
458{
459 const auto & graph = rvsdgModule.Rvsdg();
460 const auto rootRegion = &graph.GetRootRegion();
461 if (rootRegion->numNodes() != 1)
462 {
463 throw std::logic_error("Root should have only one node now");
464 }
465
466 const auto lambda = dynamic_cast<const rvsdg::LambdaNode *>(rootRegion->Nodes().begin().ptr());
467 if (!lambda)
468 {
469 throw std::logic_error("Node needs to be a lambda");
470 }
471
473 if (!state_arg)
474 {
475 // No memstate, i.e., no memory used
476 return;
477 }
478 // * pass after mem-queue
479 // * common edge is edge 0 in splits
480 // * keep response on common edge
481 // * seperate out request
482 // * [ ] scenario 1 - accross outer loops
483 // * split off before loop with req, merge after loop with resp
484 // * [ ] scenario 2.1 - within outer loop - no store on edge in loop
485 // * split before, merge after loop
486 // * [ ] scenario 2.2 - within outer loop - store on edge in loop
487 // * split at highest loop that contains no store on edge
488 // * store not being at higher level doesn't work
489 // * apply recursively - i.e. the same way for inner loops as for outer
490 const auto entryNode = llvm::tryGetMemoryStateEntrySplit(*lambda);
491 const auto exitNode = llvm::tryGetMemoryStateExitMerge(*lambda);
492 JLM_ASSERT(entryNode->noutputs() == exitNode->ninputs());
493 // process different pointer arg edges separately
494 for (size_t i = 0; i < entryNode->noutputs(); ++i)
495 {
496 std::vector<rvsdg::SimpleNode *> mem_ops;
497 std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> dummy;
499 // we need this one final time across the whole lambda - at least for this edge
501 dummy,
502 mem_ops,
504 exitNode->input(i)->origin());
505 }
506
509 dne.Run(*lambda->subregion(), statisticsCollector);
510}
511
513
517
518void
523
524}
static jlm::util::StatisticsCollector statisticsCollector
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, jlm::rvsdg::Output &value, bool loop=false)
Definition hls.hpp:68
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, jlm::rvsdg::Output &value)
Definition hls.hpp:378
void Run(rvsdg::RvsdgModule &rvsdgModule, util::StatisticsCollector &statisticsCollector) override
Perform RVSDG transformation.
~MemoryStateDecoupling() noexcept override
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, const std::vector< jlm::rvsdg::Output * > &alternatives, bool discarding, bool loop=false)
Definition hls.hpp:235
static rvsdg::Output * Create(const std::vector< rvsdg::Output * > &operands)
static std::vector< rvsdg::Output * > Create(rvsdg::Output &operand, const size_t numResults)
Output * origin() const noexcept
Definition node.hpp:58
Represents the result of a region.
Definition region.hpp:120
Represents an RVSDG transformation.
#define JLM_ASSERT(x)
Definition common.hpp:16
#define JLM_UNREACHABLE(msg)
Definition common.hpp:43
static rvsdg::Output * follow_state_edge(rvsdg::Input *state_edge, std::vector< rvsdg::SimpleNode * > &mem_ops, bool modify)
static void handle_structural(std::vector< std::tuple< rvsdg::SimpleNode *, rvsdg::Input * > > &outstanding_dec_reqs, std::vector< rvsdg::SimpleNode * > &mem_ops, rvsdg::Input *state_edge_before, rvsdg::Output *state_edge_after)
bool is_dec_res(rvsdg::SimpleNode *node)
const llvm::IntegerConstantOperation * trace_constant(const rvsdg::Output *dst)
static void optimize_single_mem_op_loop(std::vector< rvsdg::SimpleNode * > &mem_ops, rvsdg::Input *state_edge_before, rvsdg::Output *state_edge_after)
static void decouple_mem_state(rvsdg::RvsdgModule &rvsdgModule)
rvsdg::Input * get_mem_state_user(rvsdg::Output *state_edge)
static rvsdg::Output * trace_edge(rvsdg::Input *state_edge, rvsdg::Output *new_edge, rvsdg::SimpleNode *target_call, rvsdg::Output *end)
bool is_dec_req(rvsdg::SimpleNode *node)
void convert_loop_state_to_lcb(rvsdg::Input *loop_state_input)
rvsdg::SimpleNode * tryGetMemoryStateEntrySplit(const rvsdg::LambdaNode &lambdaNode) noexcept
rvsdg::SimpleNode * tryGetMemoryStateExitMerge(const rvsdg::LambdaNode &lambdaNode) noexcept
rvsdg::Output & GetMemoryStateRegionArgument(const rvsdg::LambdaNode &lambdaNode) noexcept
static void remove(Node *node)
Definition region.hpp:1035
static std::vector< jlm::rvsdg::Output * > operands(const Node *node)
Definition node.hpp:1049
@ State
Designate a state type.
NodeType * TryGetOwnerNode(const rvsdg::Input &input) noexcept
Checks if this is an input to a node of specified type.
Definition node.hpp:872