Jlm
Loading...
Searching...
No Matches
add-buffers.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2021 David Metz <david.c.metz@ntnu.no>
3 * See COPYING for terms of redistribution.
4 */
5
9#include <jlm/hls/ir/hls.hpp>
10#include <jlm/hls/util/view.hpp>
14
15namespace jlm::hls
16{
17
18static rvsdg::Input *
20{
21 auto user = &out->SingleUser();
22
24 user->GetOwner(),
25 [&](rvsdg::Region * region)
26 {
27 if (!region->node())
28 {
29 return user;
30 }
32 *region->node(),
33 [&](LoopNode & loopNode)
34 {
35 return rvsdg::MatchVariant(
36 loopNode.mapResult(*user),
37 [&](const LoopNode::BackEdgeVar & backedge)
38 {
39 return FindUserNode(backedge.pre);
40 },
41 [&](const LoopNode::ExitVar & exit)
42 {
43 return FindUserNode(exit.output);
44 });
45 },
46 [&]()
47 {
48 return user;
49 });
50 },
51 [&](rvsdg::Node * node)
52 {
53 return rvsdg::MatchTypeOrFail(
54 *node,
55 [&](LoopNode & loopNode)
56 {
57 return FindUserNode(loopNode.mapInput(*user).inner);
58 },
59 [&](rvsdg::SimpleNode &)
60 {
61 return user;
62 });
63 });
64}
65
66static void
67PlaceBuffer(rvsdg::Output * out, size_t capacity, bool passThrough)
68{
69 // places or re-places a buffer on an output
70 auto user = FindUserNode(out);
71 // don't place buffers after constants
72 if (is_constant(rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*out)))
73 {
74 return;
75 }
76 auto [forkNode, forkOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<ForkOperation>(*out);
77 if (forkOperation && forkOperation->IsConstant())
78 {
79 return;
80 }
81
82 // TODO: handle out being a buf?
83 auto [bufferNode, bufferOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<BufferOperation>(*user);
84 if (bufferOperation
85 && (bufferOperation->IsPassThrough() != passThrough
86 || bufferOperation->Capacity() != capacity))
87 {
88 // replace buffer and keep larger size
89 auto node = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*user);
90 passThrough = passThrough && bufferOperation->IsPassThrough();
91 capacity = std::max(capacity, bufferOperation->Capacity());
92 auto bufOut = BufferOperation::create(*node->input(0)->origin(), capacity, passThrough)[0];
93 node->output(0)->divert_users(bufOut);
94 JLM_ASSERT(node->IsDead());
95 remove(node);
96 }
97 else
98 {
99 // create new buffer
100 auto & directUser = *out->Users().begin();
101 auto newOut = BufferOperation::create(*out, capacity, passThrough)[0];
102 directUser.divert_to(newOut);
103 }
104}
105
106static void
108{
109 auto addrq = dynamic_cast<const AddressQueueOperation *>(&node->GetOperation());
110 JLM_ASSERT(addrq);
111 // place buffer on addr output
112 PlaceBuffer(node->output(0), addrq->capacity, true);
113}
114
115static void
117{
118 auto buf = dynamic_cast<const BufferOperation *>(&node->GetOperation());
119 JLM_ASSERT(buf);
120 auto user = FindUserNode(node->output(0));
121 auto [bufferNode, bufferOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<BufferOperation>(*user);
122 if (bufferOperation)
123 {
124 auto node2 = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*user);
125 // merge buffers and keep larger size
126 bool passThrough = buf->IsPassThrough() && bufferOperation->IsPassThrough();
127 auto capacity = std::max(buf->Capacity(), bufferOperation->Capacity());
128 auto newOut = BufferOperation::create(*node->input(0)->origin(), capacity, passThrough)[0];
129 JLM_ASSERT(node2->region() == newOut->region());
130 node2->output(0)->divert_users(newOut);
131 JLM_ASSERT(node2->IsDead());
132 remove(node2);
133 JLM_ASSERT(node->IsDead());
134 remove(node);
135 }
136}
137
138static void
140{
141 // TODO: should this be changed?
142 bool outerLoop = !rvsdg::is<LoopOperation>(loopNode->region()->node());
143 if (outerLoop)
144 {
145 // push buffers above branches, so they also act as output buffers
146 for (size_t i = 0; i < loopNode->noutputs(); ++i)
147 {
148 auto out = loopNode->output(i);
149 auto res = out->results.begin().ptr();
150 auto [branchNode, branchOperation] =
151 rvsdg::TryGetSimpleNodeAndOptionalOp<BranchOperation>(*res->origin());
152 if (!branchOperation)
153 {
154 // this is a memory operation or stream
155 continue;
156 }
157 JLM_ASSERT(branchOperation->loop);
158 auto oldBufInput = &branchNode->output(1)->SingleUser();
159 auto [oldBufferNode, oldBufferOperation] =
160 rvsdg::TryGetSimpleNodeAndOptionalOp<BufferOperation>(*oldBufInput);
161 if (rvsdg::IsOwnerNodeOperation<SinkOperation>(*oldBufInput))
162 {
163 // no backedge
164 continue;
165 }
166 JLM_ASSERT(oldBufferOperation);
167 auto oldBufNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*oldBufInput);
168 // place new buffers
170 branchNode->input(1)->origin(),
171 oldBufferOperation->Capacity(),
172 oldBufferOperation->IsPassThrough());
173 // this buffer should just make the fork buf non-passthrough - needed to avoid combinatorial
174 // cycle
176 branchNode->input(0)->origin(),
177 oldBufferOperation->Capacity(),
178 oldBufferOperation->IsPassThrough());
179 // remove old buffer
180 oldBufNode->output(0)->divert_users(oldBufInput->origin());
181 JLM_ASSERT(oldBufNode->IsDead());
182 remove(oldBufNode);
183 }
184 }
185 else
186 {
187 // add input buffers
188 for (size_t i = 0; i < loopNode->ninputs(); ++i)
189 {
190 auto in = loopNode->input(i);
191 auto arg = in->arguments.begin().ptr();
192 auto user = &arg->SingleUser();
193 // only do this for proper loop variables
194 if (auto [node, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*user);
195 muxOperation)
196 {
197 if (!muxOperation->loop)
198 {
199 // stream
200 continue;
201 }
202 }
203 else if (rvsdg::IsOwnerNodeOperation<LoopConstantBufferOperation>(*user))
204 {
205 }
206 else
207 {
208 continue;
209 }
210 PlaceBuffer(in->origin(), 2, false);
211 }
212 }
213}
214
215static void
217{
218 for (auto & node : rvsdg::TopDownTraverser(region))
219 {
220 if (auto structnode = dynamic_cast<rvsdg::StructuralNode *>(node))
221 {
222 auto loop = dynamic_cast<LoopNode *>(node);
223 JLM_ASSERT(loop);
224 OptimizeLoop(loop);
225 for (size_t n = 0; n < structnode->nsubregions(); n++)
226 {
227 AddBuffers(structnode->subregion(n));
228 }
229 }
230 else if (auto simple = dynamic_cast<jlm::rvsdg::SimpleNode *>(node))
231 {
233 {
234 OptimizeBuffer(simple);
235 }
236 else if (jlm::rvsdg::is<ForkOperation>(node))
237 {
238 // OptimizeFork(simple);
239 }
240 else if (jlm::rvsdg::is<BranchOperation>(node))
241 {
242 // OptimizeBranch(simple);
243 }
245 {
246 // OptimizeStateGate(simple);
247 }
249 {
250 OptimizeAddrQ(simple);
251 }
252 }
253 }
254}
255
256static size_t MemoryLatency = 10;
257
258static constexpr uint32_t
259round_up_pow2(uint32_t x)
260{
261 if (x == 0)
262 return 1;
263 --x;
264 x |= x >> 1;
265 x |= x >> 2;
266 x |= x >> 4;
267 x |= x >> 8;
268 x |= x >> 16;
269 return x + 1;
270}
271
272static void
274{
275 // const size_t capacity = 256;
276 std::vector<jlm::rvsdg::SimpleNode *> nodes;
277 for (auto & node : rvsdg::TopDownTraverser(region))
278 {
279 if (auto structnode = dynamic_cast<rvsdg::StructuralNode *>(node))
280 {
281 auto loop = dynamic_cast<LoopNode *>(node);
282 JLM_ASSERT(loop);
283 for (size_t n = 0; n < structnode->nsubregions(); n++)
284 {
285 MaximizeBuffers(structnode->subregion(n));
286 }
287 }
288 else if (auto sn = dynamic_cast<jlm::rvsdg::SimpleNode *>(node))
289 {
290 if (rvsdg::is<BufferOperation>(node))
291 {
292 nodes.push_back(sn);
293 }
294 else if (rvsdg::is<DecoupledLoadOperation>(node))
295 {
296 nodes.push_back(sn);
297 }
298 }
299 }
300 for (auto node : nodes)
301 {
302 if (auto dl = dynamic_cast<const DecoupledLoadOperation *>(&node->GetOperation()))
303 {
304 auto capacity = round_up_pow2(MemoryLatency);
305 if (dl->capacity < capacity)
306 {
307 divert_users(
308 node,
309 DecoupledLoadOperation::create(
310 *node->input(0)->origin(),
311 *node->input(1)->origin(),
312 capacity));
313 remove(node);
314 }
315 }
316 }
317}
318
319static std::vector<size_t>
320NodeCycles(rvsdg::SimpleNode * node, std::vector<size_t> & input_cycles)
321{
322 auto max_cycles = *std::max_element(input_cycles.begin(), input_cycles.end());
323 if (auto op = dynamic_cast<const llvm::FBinaryOperation *>(&node->GetOperation()))
324 {
325 if (op->fpop() == llvm::fpop::add)
326 {
327 return { max_cycles + 1 };
328 }
329 }
330 else if (auto op = dynamic_cast<const BufferOperation *>(&node->GetOperation()))
331 {
332 if (op->IsPassThrough())
333 {
334 return { max_cycles + 0 };
335 }
336 return { max_cycles + 1 };
337 }
338 else if (dynamic_cast<const AddressQueueOperation *>(&node->GetOperation()))
339 {
340 return { input_cycles[0] };
341 }
342 else if (rvsdg::is<DecoupledLoadOperation>(node))
343 {
344 return { max_cycles + MemoryLatency, 0 };
345 }
346 else if (rvsdg::is<StateGateOperation>(node))
347 {
348 // handle special state gate that sits on dec_load response
349 auto sg0_user = &node->output(0)->SingleUser();
350 if (rvsdg::IsOwnerNodeOperation<DecoupledLoadOperation>(*sg0_user) && sg0_user->index() == 1)
351 {
352 JLM_ASSERT(max_cycles == 0);
353 return { 0, MemoryLatency };
354 }
355 }
356 else if (rvsdg::is<StoreOperation>(node))
357 {
358 JLM_ASSERT(node->noutputs() == 3);
359 return { max_cycles + MemoryLatency, 0, 0 };
360 }
361 return std::vector<size_t>(node->noutputs(), max_cycles);
362}
363
364const size_t UnlimitedBufferCapacity = std::numeric_limits<uint32_t>::max();
365
366static std::vector<size_t>
367NodeCapacity(rvsdg::SimpleNode * node, std::vector<size_t> & input_capacities)
368{
369 auto min_capacity = *std::min_element(input_capacities.begin(), input_capacities.end());
370 if (auto op = dynamic_cast<const llvm::FBinaryOperation *>(&node->GetOperation()))
371 {
372 if (op->fpop() == llvm::fpop::add)
373 {
374 return { min_capacity + 1 };
375 }
376 }
377 else if (auto op = dynamic_cast<const BufferOperation *>(&node->GetOperation()))
378 {
379 return { min_capacity + op->Capacity() };
380 }
381 else if (dynamic_cast<const AddressQueueOperation *>(&node->GetOperation()))
382 {
383 return { input_capacities[0] };
384 }
385 else if (auto op = dynamic_cast<const DecoupledLoadOperation *>(&node->GetOperation()))
386 {
387 return { min_capacity + op->capacity, 0 };
388 }
389 else if (rvsdg::is<StateGateOperation>(node))
390 {
391 // handle special state gate that sits on dec_load response
392 auto sg0_user = &node->output(0)->SingleUser();
393 if (rvsdg::IsOwnerNodeOperation<DecoupledLoadOperation>(*sg0_user) && sg0_user->index() == 1)
394 {
395 JLM_ASSERT(min_capacity == UnlimitedBufferCapacity);
396 return { 0, MemoryLatency };
397 }
398 }
399 else if (rvsdg::is<StoreOperation>(node))
400 {
401 return { min_capacity + MemoryLatency, 0, 0 };
402 }
403 return std::vector<size_t>(node->noutputs(), min_capacity);
404}
405
406static void
408 LoopNode * loop,
409 std::unordered_map<rvsdg::Output *, size_t> & output_cycles,
410 std::unordered_set<rvsdg::Input *> & frontier,
411 std::unordered_set<rvsdg::Input *> & stream_backedges,
412 std::unordered_set<rvsdg::SimpleNode *> & top_muxes)
413{
414 for (size_t i = 0; i < loop->ninputs(); ++i)
415 {
416 auto in = loop->input(i);
417 auto arg = in->arguments.begin().ptr();
418
419 auto user = &arg->SingleUser();
420 auto userNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*user);
421 auto [muxNode, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*user);
422 if (rvsdg::IsOwnerNodeOperation<LoopConstantBufferOperation>(*user)
423 || (muxOperation && muxOperation->loop))
424 {
425 top_muxes.insert(userNode);
426 // we start from these
427 auto out = userNode->output(0);
428 output_cycles[out] = output_cycles[in->origin()];
429 frontier.insert(&out->SingleUser());
430 }
431 // these are needed so we can finish with an empty frontier
432 output_cycles[arg] = output_cycles[in->origin()];
433 frontier.insert(&arg->SingleUser());
434 }
435 for (auto & tn : loop->subregion()->TopNodes())
436 {
437 JLM_ASSERT(is_constant(&tn));
438 auto out = tn.output(0);
439 output_cycles[out] = 0;
440 frontier.insert(&out->SingleUser());
441 }
442 for (auto arg : loop->subregion()->Arguments())
443 {
444 auto var = loop->mapArgument(*arg);
445 auto backedge = std::get_if<LoopNode::BackEdgeVar>(&var);
446 if (!backedge)
447 {
448 continue;
449 }
450 auto user = &arg->SingleUser();
451 auto [muxNode, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*user);
452 if ((muxOperation && muxOperation->loop))
453 {
454 continue;
455 }
456 if (rvsdg::IsOwnerNodeOperation<BufferOperation>(*user))
457 {
458 auto bufNode = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*user);
459 if (rvsdg::IsOwnerNodeOperation<PredicateBufferOperation>(bufNode->output(0)->SingleUser()))
460 {
461 // skip predicate buffer
462 continue;
463 }
464 }
465 // this comes from somewhere inside the loop
466 output_cycles[arg] = 0;
467 frontier.insert(&arg->SingleUser());
468 stream_backedges.insert(backedge->post);
469 }
470}
471
472static void
473CalculateLoopCycleDepth(
474 LoopNode * loop,
475 std::unordered_map<rvsdg::Output *, size_t> & output_cycles,
476 bool analyze_inner_loop = false);
477
478static void
480 std::unordered_map<rvsdg::Output *, size_t> & output_cycles,
481 std::unordered_set<rvsdg::Input *> & frontier,
482 std::unordered_set<rvsdg::Input *> & stream_backedges,
483 std::unordered_set<rvsdg::SimpleNode *> & top_muxes)
484{
485 bool changed = false;
486 do
487 {
488 changed = false;
489 for (auto in : frontier)
490 {
491 bool inner_break = false;
492 rvsdg::MatchVariant(
493 in->GetOwner(),
494 [&](rvsdg::Node * node)
495 {
496 rvsdg::MatchTypeOrFail(
497 *node,
498 [&](rvsdg::SimpleNode & simpleNode)
499 {
500 bool all_contained = true;
501 for (auto & input : simpleNode.Inputs())
502 {
503 auto f = frontier.find(&input);
504 if (f == frontier.end())
505 {
506 all_contained = false;
507 }
508 }
509 if (all_contained)
510 {
511 // all inputs of node are in frontier - move them forward
512 std::vector<size_t> input_cycles;
513 for (auto & input : simpleNode.Inputs())
514 {
515 input_cycles.push_back(output_cycles[input.origin()]);
516 frontier.erase(&input);
517 }
518 std::vector<size_t> out_cycles = NodeCycles(&simpleNode, input_cycles);
519
520 if (top_muxes.find(&simpleNode) != top_muxes.end())
521 {
522 if (dynamic_cast<const MuxOperation *>(&simpleNode.GetOperation()))
523 {
524 // TODO: do this in NodeCycles instead?
525 // this works for most cases, but is not ideal if the backedge has an II >
526 // 1, and the predicate hasn't
527 auto pred_latency = output_cycles[simpleNode.input(0)->origin()];
528 auto input_latency = output_cycles[simpleNode.input(1)->origin()];
529 auto backedge_latency = output_cycles[simpleNode.input(2)->origin()];
530 auto out_latency = backedge_latency - pred_latency + input_latency;
531 std::cout << "top_mux " << &simpleNode << " pred latency: " << pred_latency
532 << " input latency: " << input_latency
533 << " backedge latency: " << backedge_latency
534 << " out latency: " << out_latency << std::endl;
535 output_cycles[simpleNode.output(0)] = out_latency;
536 }
537 else
538 {
539 JLM_ASSERT(
540 rvsdg::is<LoopConstantBufferOperation>(simpleNode.GetOperation()));
541 // don't update output cycles
542 }
543 }
544 else
545 {
546 for (size_t i = 0; i < simpleNode.noutputs(); ++i)
547 {
548 auto out = simpleNode.output(i);
549 output_cycles[out] = out_cycles[i];
550 frontier.insert(&out->SingleUser());
551 }
552 }
553 changed = true;
554 inner_break = true;
555 }
556 },
557 [&](LoopNode & inner_loop)
558 {
559 bool all_contained = true;
560 for (auto & input : inner_loop.Inputs())
561 {
562 auto f = frontier.find(&input);
563 if (f == frontier.end())
564 {
565 all_contained = false;
566 }
567 }
568 if (all_contained)
569 {
570 for (auto & input : inner_loop.Inputs())
571 {
572 frontier.erase(&input);
573 }
574 // TODO: do we just want the latency of a single iteration here?
575 CalculateLoopCycleDepth(&inner_loop, output_cycles, true);
576 for (size_t i = 0; i < inner_loop.noutputs(); ++i)
577 {
578 std::cout << "output latency " << i << " "
579 << output_cycles[inner_loop.output(i)] << std::endl;
580 frontier.insert(&inner_loop.output(i)->SingleUser());
581 }
582 changed = true;
583 inner_break = true;
584 }
585 });
586 },
587 [&](rvsdg::Region * region)
588 {
589 rvsdg::MatchTypeOrFail(
590 *region->node(),
591 [&](LoopNode & loop)
592 {
593 rvsdg::MatchVariant(
594 loop.mapResult(*in),
595 [&](const LoopNode::BackEdgeVar & backedge)
596 {
597 frontier.erase(in);
598 auto out = backedge.pre;
599 if (stream_backedges.find(in) == stream_backedges.end())
600 {
601 // skip stream backedges
602 output_cycles[out] = output_cycles[in->origin()];
603 frontier.insert(&out->SingleUser());
604 }
605 changed = true;
606 inner_break = true;
607 },
608 [&](const LoopNode::ExitVar & exit)
609 {
610 frontier.erase(in);
611 auto out = exit.output;
612 JLM_ASSERT(out);
613 output_cycles[out] = output_cycles[in->origin()];
614 // don't continue frontier out of loop
615 changed = true;
616 inner_break = true;
617 });
618 },
620 {
621 // FIXME: at some point need to be very specific about
622 // the kind of structural node.
623 auto rr = dynamic_cast<rvsdg::RegionResult *>(in);
624 frontier.erase(in);
625 auto out = rr->output();
626 JLM_ASSERT(out);
627 output_cycles[out] = output_cycles[in->origin()];
628 // don't continue frontier out of loop
629 changed = true;
630 inner_break = true;
631 });
632 });
633 if (inner_break)
634 break;
635 }
636 } while (changed);
637 // TODO: is "changed" even necessary or can we just wait for frontier to be empty?
638 if (!frontier.empty())
639 {
640 std::unordered_map<rvsdg::Output *, std::string> o_color;
641 std::unordered_map<rvsdg::Input *, std::string> i_color;
642 for (auto i : frontier)
643 {
644 i_color.insert({ i, "red" });
645 }
646 }
647 JLM_ASSERT(frontier.empty());
648}
649
650void
652 LoopNode * loop,
653 std::unordered_map<rvsdg::Output *, size_t> & output_cycles,
654 bool analyze_inner_loop)
655{
656 if (!analyze_inner_loop)
657 {
658 for (size_t i = 0; i < loop->ninputs(); ++i)
659 {
660 auto in = loop->input(i);
661 output_cycles[in->origin()] = 0;
662 }
663 }
664 std::unordered_set<rvsdg::Input *> frontier;
665 std::unordered_set<rvsdg::Input *> stream_backedges;
666 std::unordered_set<rvsdg::SimpleNode *> top_muxes;
667 CreateLoopFrontier(loop, output_cycles, frontier, stream_backedges, top_muxes);
668 std::unordered_set<rvsdg::Input *> frontier2(frontier);
669 std::cout << "CalculateLoopCycleDepth(" << loop << ", " << analyze_inner_loop << ")" << std::endl;
670 /* the reason for having two iterations here is a loop value being updated at the end of the loop,
671 * for example the nextRow in SPMV. In theory more iterations could be necessary, until things
672 * only increase by the iterative intensity.
673 */
674 // TODO: should there be more iterations of this? We could iterate until there is no more change
675 // in the difference. This would also give us the II
676 PushCycleFrontier(output_cycles, frontier, stream_backedges, top_muxes);
677
678 std::unordered_map<rvsdg::Output *, std::string> o_color;
679 std::unordered_map<rvsdg::Input *, std::string> i_color;
680 std::unordered_map<rvsdg::Output *, std::string> tail_label;
681 if (!analyze_inner_loop)
682 {
683 for (auto i : frontier2)
684 {
685 i_color.insert({ i, "red" });
686 }
687 for (auto [o, l] : output_cycles)
688 {
689 tail_label[o] = std::to_string(l);
690 }
691 }
692 std::cout << "second iteration" << std::endl;
693 PushCycleFrontier(output_cycles, frontier2, stream_backedges, top_muxes);
694 if (!analyze_inner_loop)
695 {
696 for (auto [o, l] : output_cycles)
697 {
698 tail_label[o] = std::to_string(l);
699 }
700 }
701}
702
703void
704setMemoryLatency(size_t memoryLatency)
705{
706 MemoryLatency = memoryLatency;
707}
708
709const size_t MaximumBufferSize = 512;
710
711static size_t
712PlaceBufferLoop(rvsdg::Output * out, size_t min_capacity, bool passThrough)
713{
714 // places or re-places a buffer on an output
715 // don't place buffers after constants
716 JLM_ASSERT(!is_constant(rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*out)));
717 auto [forkNode, forkOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<ForkOperation>(*out);
718 JLM_ASSERT(!(forkOperation && forkOperation->IsConstant()));
719
720 if (rvsdg::is<rvsdg::LambdaOperation>(out->region()->node()))
721 {
722 // don't place buffers outside loops
723 return min_capacity;
724 }
725
726 auto arg = dynamic_cast<rvsdg::RegionArgument *>(out);
727 if (arg && arg->input())
728 {
729 return PlaceBufferLoop(arg->input()->origin(), min_capacity, passThrough);
730 }
731
732 // push buf above loop_const_buf
733 if (auto [loopConstantNode, op] =
734 rvsdg::TryGetSimpleNodeAndOptionalOp<LoopConstantBufferOperation>(*out);
735 op)
736 {
737 return std::min(
738 PlaceBufferLoop(loopConstantNode->input(0)->origin(), min_capacity, passThrough),
739 PlaceBufferLoop(loopConstantNode->input(1)->origin(), min_capacity, passThrough));
740 }
741
742 if (auto [node, bufferOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<BufferOperation>(*out);
743 bufferOperation)
744 {
745 // replace buffer and keep larger size
746 passThrough = passThrough && bufferOperation->IsPassThrough();
747 size_t capacity = round_up_pow2(bufferOperation->Capacity() + min_capacity);
748 // if the maximum buffer size is exceeded place a smaller buffer, but pretend a large one was
749 // placed, to prevent additional buffers further down
750 auto actual_capacity = std::min(capacity, MaximumBufferSize);
751 auto bufOut =
752 BufferOperation::create(*node->input(0)->origin(), actual_capacity, passThrough)[0];
753 node->output(0)->divert_users(bufOut);
754 JLM_ASSERT(node->IsDead());
755 remove(node);
756 return capacity;
757 }
758 else
759 {
760 // create new buffer
761 auto & directUser = *out->Users().begin();
762 size_t capacity = round_up_pow2(min_capacity);
763 // if the maximum buffer size is exceeded place a smaller buffer, but pretend a large one was
764 // placed, to prevent additional buffers further down
765 auto actual_capacity = std::min(capacity, MaximumBufferSize);
766 auto newOut = BufferOperation::create(*out, actual_capacity, passThrough)[0];
767 directUser.divert_to(newOut);
768 return capacity;
769 }
770}
771
772static void
774 LoopNode * loop,
775 std::unordered_map<rvsdg::Output *, size_t> & output_cycles,
776 std::unordered_map<rvsdg::Output *, size_t> & buffer_capacity,
777 bool analyze_inner_loop = false)
778{
779 if (!analyze_inner_loop)
780 {
781 for (size_t i = 0; i < loop->ninputs(); ++i)
782 {
783 auto in = loop->input(i);
784 buffer_capacity[in->origin()] = 0;
785 }
786 }
787 std::unordered_set<rvsdg::Input *> frontier;
788 std::unordered_set<rvsdg::Input *> stream_backedges;
789 std::unordered_set<rvsdg::SimpleNode *> top_muxes;
790 CreateLoopFrontier(loop, buffer_capacity, frontier, stream_backedges, top_muxes);
791 // set buffer capacity for constant nodes to max
792 for (auto & tn : loop->subregion()->TopNodes())
793 {
794 auto out = tn.output(0);
795 // don't use size_t max, since that is used to signal down below
796 buffer_capacity[out] = UnlimitedBufferCapacity;
797 }
798 // same for inputs - we only care what happens within the loop
799 for (size_t i = 0; i < loop->ninputs(); ++i)
800 {
801 auto in = loop->input(i);
802 buffer_capacity[in->origin()] = UnlimitedBufferCapacity;
803 buffer_capacity[in->arguments.begin().ptr()] = UnlimitedBufferCapacity;
804 }
805 // TODO: unlimited buffers for stream backedges, but loose that property if a fork is reached?
806 // this might also make the current special case handling of addrqs unnecessary
807
808 std::unordered_map<rvsdg::Output *, std::string> o_color;
809 std::unordered_map<rvsdg::Input *, std::string> i_color;
810 std::unordered_map<rvsdg::Output *, std::string> tail_label;
811 if (!analyze_inner_loop)
812 {
813 for (auto i : frontier)
814 {
815 i_color.insert({ i, "red" });
816 }
817 for (auto [o, l] : buffer_capacity)
818 {
819 tail_label[o] = std::to_string(l);
820 }
821 }
822
823 bool changed = false;
824 do
825 {
826 changed = false;
827 for (auto in : frontier)
828 {
829 bool inner_break = false;
830
831 rvsdg::MatchVariant(
832 in->GetOwner(),
833 [&](rvsdg::Node * node)
834 {
835 rvsdg::MatchTypeOrFail(
836 *node,
837 [&](rvsdg::SimpleNode & simpleNode)
838 {
839 bool all_contained = true;
840 for (auto & input : simpleNode.Inputs())
841 {
842 auto f = frontier.find(&input);
843 if (f == frontier.end())
844 {
845 all_contained = false;
846 }
847 }
848 if (all_contained)
849 {
850 // all inputs of node are in frontier - move them forward
851 size_t max_cycles = 0;
852 for (auto & input : simpleNode.Inputs())
853 {
854 max_cycles = std::max(max_cycles, output_cycles[input.origin()]);
855 frontier.erase(&input);
856 }
857
858 std::vector<size_t> input_capacities;
859 // adjust capacities
860 for (auto & input : simpleNode.Inputs())
861 {
862 auto capacity = buffer_capacity[input.origin()];
863 if (!analyze_inner_loop && (!rvsdg::is<AddressQueueOperation>(&simpleNode))
864 && capacity < max_cycles)
865 {
866 size_t capacity_diff = max_cycles - capacity;
867 capacity += PlaceBufferLoop(input.origin(), capacity_diff, true);
868 buffer_capacity[input.origin()] = capacity;
869 }
870 input_capacities.push_back(capacity);
871 }
872
873 if (top_muxes.find(&simpleNode) != top_muxes.end())
874 {
875 // we reached our starting point again
876 auto mux = dynamic_cast<const MuxOperation *>(&simpleNode.GetOperation());
877 if (mux)
878 {
879 std::cout << "top_mux " << &simpleNode << " pred capacity: "
880 << buffer_capacity[simpleNode.input(0)->origin()]
881 << " backedge capacity: "
882 << buffer_capacity[simpleNode.input(2)->origin()] << std::endl;
883 }
884 }
885 else
886 {
887 std::vector<size_t> out_capacities =
888 NodeCapacity(&simpleNode, input_capacities);
889 for (size_t i = 0; i < simpleNode.noutputs(); ++i)
890 {
891 auto out = simpleNode.output(i);
892 buffer_capacity[out] = out_capacities[i];
893 JLM_ASSERT(
894 analyze_inner_loop || buffer_capacity[out] >= output_cycles[out]);
895 frontier.insert(&out->SingleUser());
896 }
897 }
898 changed = true;
899 inner_break = true;
900 }
901 },
902 [&](LoopNode & inner_loop)
903 {
904 bool all_contained = true;
905 for (auto & input : inner_loop.Inputs())
906 {
907 auto f = frontier.find(&input);
908 if (f == frontier.end())
909 {
910 all_contained = false;
911 }
912 }
913 if (all_contained)
914 {
915 // all inputs of node are in frontier - move them forward
916 size_t max_cycles = 0;
917 for (auto & input : inner_loop.Inputs())
918 {
919 max_cycles = std::max(max_cycles, output_cycles[input.origin()]);
920 frontier.erase(&input);
921 }
922 // adjust capacities
923 for (size_t i = 0; i < inner_loop.ninputs(); ++i)
924 {
925 auto capacity = buffer_capacity[inner_loop.input(i)->origin()];
926 if (!analyze_inner_loop && capacity < max_cycles)
927 {
928 auto user = &inner_loop.input(i)->arguments.begin().ptr()->SingleUser();
929 auto [muxNode, muxOperation] =
930 rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*user);
931 if ((muxOperation && muxOperation->loop)
932 || rvsdg::IsOwnerNodeOperation<LoopConstantBufferOperation>(*user))
933 {
934 size_t capacity_diff = max_cycles - capacity;
935 capacity +=
936 PlaceBufferLoop(inner_loop.input(i)->origin(), capacity_diff, true);
937 buffer_capacity[inner_loop.input(i)->origin()] = capacity;
938 }
939 else
940 {
941 // don't put buffers on decouples, streams, and addrq stuff
942 }
943 }
944 }
945
946 AdjustLoopBuffers(&inner_loop, output_cycles, buffer_capacity, true);
947 for (size_t i = 0; i < inner_loop.noutputs(); ++i)
948 {
949 frontier.insert(&inner_loop.output(i)->SingleUser());
950 }
951 changed = true;
952 inner_break = true;
953 }
954 });
955 },
956 [&](rvsdg::Region * region)
957 {
958 rvsdg::MatchTypeOrFail(
959 *region->node(),
960 [&](LoopNode & loop)
961 {
962 rvsdg::MatchVariant(
963 loop.mapResult(*in),
964 [&](const LoopNode::BackEdgeVar & backedge)
965 {
966 frontier.erase(in);
967 auto out = backedge.pre;
968 buffer_capacity[out] = buffer_capacity[in->origin()];
969 if (stream_backedges.find(in) == stream_backedges.end())
970 {
971 frontier.insert(&out->SingleUser());
972 }
973 changed = true;
974 inner_break = true;
975 },
976 [&](const LoopNode::ExitVar & exit)
977 {
978 frontier.erase(in);
979 auto out = exit.output;
980 JLM_ASSERT(out);
981 buffer_capacity[out] = buffer_capacity[in->origin()];
982 // don't continue frontier out of loop
983 changed = true;
984 inner_break = true;
985 }
986
987 );
988 },
990 {
991 auto rr = dynamic_cast<rvsdg::RegionResult *>(in);
992 {
993 frontier.erase(in);
994 auto out = rr->output();
995 JLM_ASSERT(out);
996 buffer_capacity[out] = buffer_capacity[in->origin()];
997 // don't continue frontier out of loop
998 changed = true;
999 inner_break = true;
1000 }
1001 });
1002 });
1003 if (inner_break)
1004 break;
1005 }
1006 } while (changed);
1007 // TODO: is "changed" even necessary or can we just wait for frontier to be empty?
1008 // benefit of it is we won't get infinite loop in case something violates this
1009 JLM_ASSERT(frontier.empty());
1010 // TODO: take iterative intensity into account. E.g. we can use half the buffer capacity if II is
1011 // 2
1012 // TODO: remove buffers on cycles?
1013 // TODO: don't place buf2 on const buf if it gos up to another const buf - even through a branch?
1014 // TODO: still run buffer resize pass, but without upsizing?
1015
1016 if (!analyze_inner_loop)
1017 {
1018 for (auto [o, l] : buffer_capacity)
1019 {
1020 tail_label[o] = std::to_string(l);
1021 }
1022 }
1023}
1024
1025static void
1027{
1028 for (auto node : rvsdg::TopDownTraverser(region))
1029 {
1030 if (auto loop = dynamic_cast<LoopNode *>(node))
1031 {
1032 // process inner loops first
1033 CalculateLoopDepths(loop->subregion());
1034 std::unordered_map<rvsdg::Output *, size_t> output_cycles;
1035 CalculateLoopCycleDepth(loop, output_cycles);
1036 std::unordered_map<rvsdg::Output *, size_t> buffer_capacity;
1037 AdjustLoopBuffers(loop, output_cycles, buffer_capacity);
1038 }
1039 }
1040}
1041
1042BufferInsertion::~BufferInsertion() noexcept = default;
1043
1047
1048void
1049BufferInsertion::Run(rvsdg::RvsdgModule & rvsdgModule, util::StatisticsCollector &)
1050{
1051 const auto & graph = rvsdgModule.Rvsdg();
1052 const auto rootRegion = &graph.GetRootRegion();
1053 if (rootRegion->numNodes() != 1)
1054 {
1055 throw std::logic_error("Root should have only one node now");
1056 }
1057
1058 const auto lambda = dynamic_cast<const rvsdg::LambdaNode *>(rootRegion->Nodes().begin().ptr());
1059 if (!lambda)
1060 {
1061 throw std::logic_error("Node needs to be a lambda");
1062 }
1063
1064 AddBuffers(lambda->subregion());
1065 MaximizeBuffers(lambda->subregion());
1066 CalculateLoopDepths(lambda->subregion());
1067}
1068
1069}
std::vector< rvsdg::Node * > nodes
std::variant< EntryVar, BackEdgeVar > mapArgument(const rvsdg::Output &argument)
Definition hls.cpp:221
rvsdg::Region * subregion() const noexcept
Definition hls.hpp:800
Region & GetRootRegion() const noexcept
Definition graph.hpp:99
Output * origin() const noexcept
Definition node.hpp:58
bool IsDead() const noexcept
Determines whether the node is dead.
Definition node.hpp:688
rvsdg::Region * region() const noexcept
Definition node.hpp:761
InputIteratorRange Inputs() noexcept
Definition node.hpp:622
size_t ninputs() const noexcept
Definition node.hpp:609
size_t noutputs() const noexcept
Definition node.hpp:644
rvsdg::Input & SingleUser() noexcept
Definition node.hpp:347
rvsdg::Region * region() const noexcept
Definition node.cpp:151
UsersRange Users()
Definition node.hpp:354
Represents the argument of a region.
Definition region.hpp:41
Represents the result of a region.
Definition region.hpp:120
Represent acyclic RVSDG subgraphs.
Definition region.hpp:213
RegionArgumentRange Arguments() noexcept
Definition region.hpp:319
TopNodeRange TopNodes() noexcept
Definition region.hpp:356
rvsdg::StructuralNode * node() const noexcept
Definition region.hpp:301
Graph & Rvsdg() noexcept
const SimpleOperation & GetOperation() const noexcept override
NodeInput * input(size_t index) const noexcept
NodeOutput * output(size_t index) const noexcept
StructuralOutput * output(size_t index) const noexcept
StructuralInput * input(size_t index) const noexcept
Represents an RVSDG transformation.
Iterator begin() noexcept
#define JLM_ASSERT(x)
Definition common.hpp:16
static rvsdg::Input * FindUserNode(rvsdg::Output *out)
static size_t MemoryLatency
static void CalculateLoopCycleDepth(LoopNode *loop, std::unordered_map< rvsdg::Output *, size_t > &output_cycles, bool analyze_inner_loop=false)
static void AddBuffers(rvsdg::Region *region)
static constexpr uint32_t round_up_pow2(uint32_t x)
static std::vector< size_t > NodeCycles(rvsdg::SimpleNode *node, std::vector< size_t > &input_cycles)
static void CreateLoopFrontier(LoopNode *loop, std::unordered_map< rvsdg::Output *, size_t > &output_cycles, std::unordered_set< rvsdg::Input * > &frontier, std::unordered_set< rvsdg::Input * > &stream_backedges, std::unordered_set< rvsdg::SimpleNode * > &top_muxes)
static void PushCycleFrontier(std::unordered_map< rvsdg::Output *, size_t > &output_cycles, std::unordered_set< rvsdg::Input * > &frontier, std::unordered_set< rvsdg::Input * > &stream_backedges, std::unordered_set< rvsdg::SimpleNode * > &top_muxes)
static void OptimizeBuffer(rvsdg::SimpleNode *node)
static std::vector< size_t > NodeCapacity(rvsdg::SimpleNode *node, std::vector< size_t > &input_capacities)
static void PlaceBuffer(rvsdg::Output *out, size_t capacity, bool passThrough)
const size_t UnlimitedBufferCapacity
static void CalculateLoopDepths(rvsdg::Region *region)
static size_t PlaceBufferLoop(rvsdg::Output *out, size_t min_capacity, bool passThrough)
static void OptimizeLoop(LoopNode *loopNode)
static void OptimizeAddrQ(rvsdg::SimpleNode *node)
void setMemoryLatency(size_t memoryLatency)
static void MaximizeBuffers(rvsdg::Region *region)
static void AdjustLoopBuffers(LoopNode *loop, std::unordered_map< rvsdg::Output *, size_t > &output_cycles, std::unordered_map< rvsdg::Output *, size_t > &buffer_capacity, bool analyze_inner_loop=false)
const size_t MaximumBufferSize
void MatchTypeWithDefault(T &obj, const Fns &... fns)
Pattern match over subclass type of given object with default handler.
decltype(auto) MatchVariant(T &&obj, Fns &&... fns)
Pattern match over variant.
NodeType * TryGetOwnerNode(const rvsdg::Input &input) noexcept
Checks if this is an input to a node of specified type.
Definition node.hpp:872
Variable exiting the hls loop.
Definition hls.hpp:737