Vivado is shit at timing optimization? - Page 1
Forum section: electronics / general

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 01:33:50 pm
content: So I've encountered this issue more than once already, where I attempt to fix a design with a timing bottleneck to push the Fmax further. I look at the bottleneck path, and vivado shows:OK fair enough, there isn't a register between that LUT and the multiplier. I then go back to the code and shortly realize I had already put not one but TWO registers there. Then I check out that multiplier and...Just why? There are two register stages but they've both been absorbed into the multiplier. I have tried different implementation strategies and settings but this kind of crap always shows up. Is vivado this dumb?

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 01:38:13 pm
content: And just to be sure, we aren't short on flipflops:There are plenty of flops in between that could have been used.

author: asmi
author_rank: Super Contributor
timestamp: June 25, 2019, 03:25:42 pm
content: Quote from: OwO on June 25, 2019, 01:33:50 pmHave you tried using "keep" attribute to prevent register's absorbtion into DSP tile?Quote from: OwO on June 25, 2019, 01:33:50 pmIt's never the tool that is dumb.

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 03:43:44 pm
content: Quote from: asmi on June 25, 2019, 03:25:42 pmK I'll try that. Still, that is a vendor specific directive that shouldn't usually be needed. Related to this is I had to use a Xilinx specific attribute to force some 16-deep rams to use LUTRAM instead of block ram, otherwise it infers block ram and fucks up the timing because the block rams are too far awayAlso more than once I've seen significant LUT reduction just by moving logic in and out of entities. I know you can just enable flatten hierarchy but really I think the tools should be smarter than that.Quote from: asmi on June 25, 2019, 03:25:42 pmIs that a serious attempt at a thinly veiled insult?

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: June 25, 2019, 04:30:47 pm
content: Quote from: OwO on June 25, 2019, 03:43:44 pmThere's a common saying in English: "Only poor craftsmen blame their tools."

author: asmi
author_rank: Super Contributor
timestamp: June 25, 2019, 04:33:55 pm
content: Quote from: OwO on June 25, 2019, 03:43:44 pmDSP tiles are just as vendor specific. The reality is if you want to achieve the best performance, you will have to use vendor-specific stuff. Same thing applies in software world in most cases. As the saying goes, "generic code results in generic performance".Quote from: OwO on June 25, 2019, 03:43:44 pmThe first thing I always do in my projects is disable messing with hierarchy, as it gives me total control of where optimizations can happen, and where they can't. The tool can't possibly know what your intent is. The reason I disable these optimizations is I want my modules to be self-containing and not depend on specific way of connecting them to outside world, and as such they can easily be dropped in to any project and will not cause any timing issues by themselves.But you are free to use third-party synthesis tool, or even develop one yourself that will somehow figure this out. Synthesis is sufficiently decoupled from implementation, so you can totally use your own synthesis tool while leaving implementation to Vivado. This is an officially supported workflow.

author: MagicSmoker
author_rank: Super Contributor
timestamp: June 25, 2019, 04:36:34 pm
content: Quote from: Sal Ammoniac on June 25, 2019, 04:30:47 pmAnd there's another common saying in English that applies just as much: if you don't have anything nice to say then say nothing at all.OP - I can't help you with this problem, but it is something I have heard many people complain about over the years...

author: tggzzz
author_rank: Super Contributor
timestamp: June 25, 2019, 04:36:57 pm
content: Quote from: OwO on June 25, 2019, 03:43:44 pmWell, we don't know your coding style, but if you need specific functionality then it is traditional to instantiate specific vendor/device specific blocks. Whether or not a tool can infer yourintentionis and will continue to be rather hit and miss. That's why vendors typically produce application notes indicating coding idioms that their tool will optimise.As for timing, adding constraints to a design is somewhat of an artform.The other obvious question is what experience do you have with hardware design languages and tools?QuoteI interpret it as axiomatic, salesmens' statements notwithstanding.

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: June 25, 2019, 04:50:40 pm
content: Quote from: MagicSmoker on June 25, 2019, 04:36:34 pmAt least I said it nicely, which can't be said about the OP, who used works like dumb and shit.This isn't directed at the OP specifically, but I've known way too many people who are quick to point fingers at their tools when it's their own work that needs closer inspection.

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 04:57:19 pm
content: The "keep" attribute fixes it. I get that you can always add constraints to get whatever you want, and in real production designs I wouldn't care, but the point here is I'm trying to write code that is as vendor neutral as possible. The project in question:https://github.com/owocomm-0/fpga-fftThe multiplier structure is necessarily vendor specific and configurable when generating the core because you have to order the pipeline in a specific way to take advantage of the adders in the DSP48E1 block. However I still think something like whether to absorb a register into the DSP block isn't a design decision, it is an optimization decision that should be left to the tools. The tools inferred my intention correctly, that is having two register stages between the LUT and the multiplier, but where it went wrong is exactly where to put the registers physically which I shouldn't have to specify.

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 06:05:41 pm
content: I also disagree with saying the tools are never to blame. In some C code you can easily demonstrate that gcc is unable to optimize loops properly because a simple textbook for loop formulation likeCode:for(int i=0; i<len; i++)arrayA = some_expression(arrayB);is demonstrably slower than a formulation like:Code:int* arrayEnd = arrayA + len;while(arrayA < arrayEnd) {*arrayA = some_expression(*arrayB);arrayA++; arrayB++;}The two pieces of code are semantically the same, but gcc is too dumb to figure it out and forces you to uglify your code to get it optimized. Code should be designed to be readable to humans, not machines.

author: dmills
author_rank: Super Contributor
timestamp: June 25, 2019, 06:13:19 pm
content: If you read the DSP block manual, you will find that when inferring DSPs up to IIRC three levels of input pipeline are automatically moved into the DSP block because this is required to optimise the speed of that part (Actually it generally puts on in as the 'sum of products' stage and one at the input.Basically the DSP block has three stages of internal optional pipeline that help a lot with speed (Especially, pipeline the sum of products makes a big difference), and if you are inferring a DSP block the tool will attempt to use as much of that as it can, this is usually the right thing to do.TBH, if you are using placement constraints then you are well into chip specific stuff and should be expecting to have to work with what the tool infers (which is not always obvious).Regards, Dan.

author: OwO
author_rank: Super Contributor
timestamp: June 25, 2019, 07:07:43 pm
content: Quote from: dmills on June 25, 2019, 06:13:19 pmYes, that's the A/BREG, MREG, and PREG. Those are all utilized in the design but what's happening is one additional register before the input got collapsed into the AREG (which is either 0, 1, or 2 register stages). I think I'll stick with the "keep" fix for now. The design (size 32k 32-bit FFT) now meets timing at 456MHz on Kintex 7 speed grade 1. The timing bottleneck is now the carry chains in the adders so there isn't much more room for improvement. Actually you can't push it further because the block ram is limited to 458MHz.

author: asmi
author_rank: Super Contributor
timestamp: June 25, 2019, 10:24:29 pm
content: Quote from: OwO on June 25, 2019, 04:57:19 pmIf you can add another pipeline register, it will both fill DSP tile to the brim with regs as well as leave one more reg to ensure timing. Same goes of outgoing register BTW (P) as it's timing is also probably very marginal. With that you can probably achieve even higher Fmax as DSP will work at full frequency as long as you don't use pattern matching functionality.But like I said, I prefer to disable messing with hierarchy because this way the module becomes more independent from outside world. At the very least make sure that you register inputs right away with no logic based of actual inputs.Optimizing designs is more of an art than science, however there are few rules which you just need to follow. For example, if your logic for certain signal fits within a single tile, it will work at MUCH higher frequency than if it's spread across several tiles. This is of course architecture-specific, but if you want to achieve the absolute best, you will have to take these things into account.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 25, 2019, 10:31:47 pm
content: If I have a slightest doubt, I just instantiate what I need and then it simply works. IMHO, it is not true that instantiating something is somehow inferior to inference.Same with C. If I want something special, rather than trying to infer it through various C tricks, I just use assembler. Saves me lots of time.

author: OwO
author_rank: Super Contributor
timestamp: June 26, 2019, 05:03:17 am
content: Quote from: asmi on June 25, 2019, 10:24:29 pmTried that actually, and ended up with a bunch of SR16LsIn related news, just upgraded from vivado 2018.3 to 2019.1 and the timing worsened from WNS -0.091ns to -0.214ns (456MHz to 432MHz). I had ran an implementation right before upgrading and one right after, nothing was changed. The failure is in an adder carry chain. Will investigate further...

author: hamster_nz
author_rank: Super Contributor
timestamp: June 26, 2019, 09:17:43 am
content: It is a bit harsh to say "I want to push an FPGA to its performance limits, but I want to do it without any device-specific code, and I don't want to give the tools any device-specific hints about my use case", and then call the thread "Vivado is shit at timing optimization?".The heuristic of absorbing input and output registers into the DSP block is usually a good one. Speeds up the DSP block, lowers FF resource usage, allows optimal performance when RTL fits the pattern of the underlying DSP blocks, their interconnects with each other and the neighboring BRAM blocks.Now you understand that this is what Vivado's behavour is , just keep throwing registers at it until it stops absorbing them.I wouldn't want it any other way...... or if I did, I would use KEEP attributes

author: OwO
author_rank: Super Contributor
timestamp: June 26, 2019, 09:41:31 am
content: Yeah I used to just throw registers at it until the problem is fixedbut now I realize that it's just as important to save on registers for both timing and resource usage reasons. There are only 2 registers per LUT6 in each slice, and using additional registers means a lot of LUTs are rendered unusable.If the core's overall register to LUT ratio is more than 2, it's very unlikely that unrelated logic can be packed into those slices with unused LUTs because other logic tend to be far away. If you look at the layout of a typical FFT core there are "bubbles" within with unused LUTs.This is also bad for timing because an area with "register congestion" means vivado is unable to allocate a register close to the related logic. For example the bit 0 input register before an adder is critical and must be placed as close to the carry chain as possible. Multiple times I've seen timing improvements after removing registers rather than adding them.Another thing I've discovered you need to be careful about is clock enables on registers. In 7 series devices all registers in a slice share a clock enable, meaning registers from other logic with a different clock enable can not be packed into the slice. One time I had an adder followed by a register with a clock enable, and since the carry chain generates 4 bits per slice it meant only 4 out of 8 registers in a slice are used, and the remaining 4 are permanently wasted in the design because they can not be used for packing other registers in. Fixing this improved the timing and tightened the layout. This is fixed in ultrascale which has several independent clock enables in a slice, but us plebs are still stuck with 7 series parts

author: FrankBuss
author_rank: Unknown
timestamp: June 26, 2019, 10:04:12 am
content: Quote from: OwO on June 25, 2019, 06:05:41 pmIf compiled with gcc 9.1, both versions have the same number of assembler instructions for the loop (7). I didn't count the instruction cycles, but I don't think they are much different. Seeherefor the first version, andherefor the second version. If using clang 8.0.0, the first version is even one instruction shorter than the second version.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 26, 2019, 02:04:10 pm
content: Quote from: OwO on June 26, 2019, 09:41:31 amI found that on 7-series LUTs are always the bottleneck, never flops. If you have long chains of flops, they often can be absorbed by shift registers, which are made of LUTs. Each slice has 8 flops and only 4 LUTs. Even when each LUT uses a flop, the remaining 4 flops can be used for other things.I think most of the time you can fully pipelne whatever is pipelinable without worrying about too many flops. Your -1 grade Kintex can run fully pipelined DSPs at 550 MHz. There's no reason why you cannot pipeline to that speed. BRAM shouldn't be a concern because you can speed it up 2x by using dual reads.However the faster you go, the less hope that the tools will do a good P&R job, so you may end up with lots of manual placing (may be even some manual routing). Thinking that you can have a 550 MHz design which you can port to Altera without modifications is absolutely delusional.

author: Wiljan
author_rank: Regular Contributor
timestamp: June 26, 2019, 06:23:46 pm
content: Quote from: NorthGuy on June 26, 2019, 02:04:10 pmInteresting, I could use that, do you have a link showing how to do this?

author: 0culus
author_rank: Super Contributor
timestamp: June 27, 2019, 03:29:18 am
content: I had some issues with Vivado's optimization passes when I was working on my research. Basically, the quick fix was to disable all optimization to try to figure out what was going on because Vivado was simply taking some crucial parts of my design and running it through some kind of opaque optimization that produced a circuit that wasnotfunctionally equivalent to the circuit I specified.Beyond that, I've found keep attributes to be required for many situations.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 27, 2019, 03:40:03 am
content: Quote from: Wiljan on June 26, 2019, 06:23:46 pmI do not have a link, but it is not hard to explain. 7-series BRAM can be accessed through 2 independent ports. If you run at frequency f and want to access BRAM, which runs at f/2, then you do:1. De-multiplex the address A, so that you now have Aeven (for even clocks) and Aodd (for odd clocks)2. Bring Aeven and Aodd from f clock domain to f/2.3. After clock domain crossing, you supply Aeven to one of the dual read ports of BRAM and Aodd to the other read port4. After due amount of clock cycles, you get the data Deven and Dodd from the corresponding BRAM ports5. You bring Deven and Dodd from f/2 to f clock domain6. You multiplex Deven and Dodd, into the final data vector D, where D is either Deven (for even clocks) or Dodd (for odd clocks).The whole thing appears as RAM which works at f (which is 2 times faster than the BRAM speed f/2). This, of course, introduces considerable latency, but if you know the address upfront (as is the case with FFT), this is not a problem.If you wish, you can run one of the BRAM read ports with 180-degree shift relative to the other BRAM read port (say one port is clocking by rising edges of f/2 and the other port is clocked by falling edges of f/2), which may help to make pipeline shorter and thus decrease latency.

author: Someone
author_rank: Super Contributor
timestamp: June 27, 2019, 04:18:10 am
content: Quote from: OwO on June 26, 2019, 09:41:31 amAre you just running the default implementation flows? The tools are more than capable of finding the critical paths and balancing/placing them well if you give them the time and space to find those points (or just use directives or constraints to save implementation time).Quote from: hamster_nz on June 26, 2019, 09:17:43 amAdding more registers is a brute force way to overwhelm the capacity of the more complex primitives (add enough and the tools can't even fit into a single SRL) but its both wasteful of resources, power, and implementation effort. The correct answer is to learn how to use viviado and the ways it resolves these timing failures (the steps of the implementation), or how to direct it to do specific things in advance (doesn't have to be directives in the source files).

author: Wiljan
author_rank: Regular Contributor
timestamp: June 27, 2019, 06:12:54 am
content: Quote from: NorthGuy on June 27, 2019, 03:40:03 amThx NorthGuy, Makes fully sense, it's a bit like serial =>parallel => serial, will do some test and see if I can live with the latency in the pipeline

author: OwO
author_rank: Super Contributor
timestamp: June 27, 2019, 08:45:38 am
content: Quote from: NorthGuy on June 27, 2019, 03:40:03 amThat would work if it's a readonly block ram, but for the transposer memory needed in the FFT it has to write at the same rate as it's reading (the requirement is the same in a single path delay feedback FFT architecture), so you would need quad port memory (2 read ports and 2 write ports). I'm not aware of any Xilinx FPGA with quad port block rams, but it's doable in LUTRAM.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 27, 2019, 02:30:36 pm
content: Quote from: OwO on June 27, 2019, 08:45:38 amI don't think you can do it with LUT RAM. To write at 2x speed you need 2 write ports, but LUT RAM has only one write port. In fact, they're not real multi-port at all. All their so-called multiport configurations are multiple LUTs written with the same data and then read independently.I think BRAM has more potential. Apparently, you will have multiple BRAM block in your memory array. Each of them can be accessed independently. You even can make two simultaneous writes to the same block (but only if it is not read at the same time). If you arrange your algorithm so that there's no consecutive writes to the same BRAM block, you can get 2x write speed. Given that you can freely shuffle address lines, this might be doable.

author: asmi
author_rank: Super Contributor
timestamp: June 27, 2019, 02:57:40 pm
content: Why can't you just use two BRAM blocks in parallel to double the word width and get 2x bandwidth this way? There are plenty of BRAM blocks in modern FPGA (even relatively small A35T has 50/100 of them!). When I need more than two read ports, I just write the same data into multiple BRAMs at the same time, and then you can read with however many addresses at the same time by reading from different BRAMs that all have the same data.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 27, 2019, 05:13:56 pm
content: Quote from: asmi on June 27, 2019, 02:57:40 pmDitto.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 27, 2019, 06:07:26 pm
content: Quote from: SiliconWizard on June 27, 2019, 05:13:56 pmIf your consecutive memory accesses always use consecutive addresses then you can get 2x bandwidth by widening your memory. However, if you need random access, widening will not help because the 2 consecutive accesses may work with data at two different addresses.As to multiple copies of the same memory to create multiple read ports, they can help with reads, but not with writes, because you need to write to all copies anyway.

author: asmi
author_rank: Super Contributor
timestamp: June 27, 2019, 06:31:41 pm
content: Quote from: NorthGuy on June 27, 2019, 06:07:26 pmIt works for random access just as well. You will just have addressable elements's size doubled (i.e. a single address will correspond to 64 bit-wide word instead of 32 bit one). You pass the same address to both BRAMs, take half of data bits from the first BRAM, and the second half from another one.Quote from: NorthGuy on June 27, 2019, 06:07:26 pmI can't remember any situation when I needed many write ports at the same time as many read ports. There were however quite a few cases when I needed multiple read ports and a single write one.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 27, 2019, 06:59:49 pm
content: Quote from: asmi on June 27, 2019, 06:31:41 pmExactly. If you want to read it from the same address it'll work, but if you need one 32-bit word from one address and another 32-bit from a different address (which is called random access), 64-bit wide memory won't help.

author: asmi
author_rank: Super Contributor
timestamp: June 27, 2019, 07:21:53 pm
content: Quote from: NorthGuy on June 27, 2019, 06:59:49 pmThis is not a random access. Random access implies access to random addresses sequentially, not simultaneously. And doubling the data means you process double word per cycle by doubling your processing pipeline.

author: NorthGuy
author_rank: Super Contributor
timestamp: June 27, 2019, 08:38:53 pm
content: Quote from: asmi on June 27, 2019, 07:21:53 pmYeap. Originally, we want to fetch 32-bit datasequentiallyat random addresses (such as 0x04, 0x20, 0x1e, 0x03 etc.). But we want to do it 2x faster than the memory can sustain.You suggest using fetches from 64-bit memory (two halvessimultaneously) to solve this problem. This way you do get two 32-bit data pieces at once, thus meeting the speed requirement. But your method implies that both upper and lower halves of the 64-bit data you have fetched must go from the same place (e.g. if we fetched the first 32-bit data from 0x04, the second one will not be from 0x20), which is not what we originally wanted. Thus the random access requirement is not met.