Thank you. Sorry that the only thing standing between you and your dinner is me rambling
about high performance is the Iran times in embedded systems, but since I'm going to speak
about radio 3.8, 3.10 sorry for the two futures DR, I'm half new as the Iran time I'm
developing. I would like to be with this job, it's a mandatory quote from SKCD, right?
So the story goes like this, last summer I was doing a project for the radio for the two,
which was kind of kicking the tires and trying to make a back and modem radio, you see
in for the toe. And through that I got quite familiar with how for the toe works. I've been
using Radio 3 for a long time also, so I could compare between the two. I've also been interested
in futures DR since it launched really because I like developing in brass, but I've never used
it before engaging on this project. So I was kind of interested in comparing all these three
SDR frameworks and I was really thinking, well which is the one I should use, which is the best,
right? I was asking these kind of questions to myself. And of course this is a silly question
because it's not a technical question that you can answer and SDR randoms are just tools. So
maybe for different problems you should use different tools. And so the more technical question I
asked myself is about performance. So if I want to have a really fast SDR framework or SDR
runtime, what other things I should do on what other things I should not do because they go
so much performance. And as I said, I wanted to look at these three SDR runtime because they are
the ones I am more interested in, I'm probably the most popular, so I think right now.
And when I was thinking about all these runtime, I realized they looked the same. And here you have
Generated Companion. This is only available in 3 to 10, really, or the Generated 3 series.
But you can do the same thing with code on the other randoms. You write a flow graph. But that's
no what I mean. What I mean, but they all look the same is not the blocks. So there's a graph.
But I mean the arrows. And this is like a stupid thing to say. But here what we draw as an arrow
is a circular buffer, which is connecting the blocks. So right, the connections in the graph
are circular buffers. Usually there are single producer, multiple consumer because you want to
find out these noise source could go to multiple blocks downstream. And that's how these works.
And I was starting to think, well, I know a way to do differently. Maybe it's not better,
but it's different. And perhaps it does well in some respects. So the disadvantages of using
these circular buffers for the connections are these two. The first is, if you look at these
multiplyconst and block, its work function has an input buffer. It has an output buffer. And it's
moving the data from the input to the output and multiplying by 42. And what happens is for this
block to be very, very fast, you need both the input data and the output data to be in L1 cache.
You could multiply in place, right? Multiply my 42 is a simple operation. You could have a
single buffer, which is your input and your output and you put it there. But by having separate
input and output buffers, you're effectively reducing the size of the cache that these block can
work with by half. So that's one thing. The other thing is, trust me latency. And I always point
people to the same talk, Madadu's cave at GRCone. I think it was maybe four or five years back,
where he considered the following program. If I have instead of the null sync and SDR sync,
which is consuming data at a certain rate. And I have a bunch of blocks. So I have like a long chain
of blocks. What happens is the way the back pressure works in this kind of system is because buffers
get full. So when you have many blocks, you have many buffers between all these blocks. And that
drives your transmitt latency up. And if I work to change at some point this 42 by any other number,
I might see the end effect at my RF output couple seconds afterwards. And that's something that
happens with 3.10. It's something that can happen with 4.0. So it's like some challenge that maybe
this buffer approach is not good to control latency. And to have something which you can control
on the final latency on trying to get low graphs. So my alternative idea was to send samples in packets.
Rather than a half like a continuous stream which is realized by several buffers, you send them
in packets. And the idea is you make a circuit of packets. Why do you make a circuit? So the thing
is you have some fixed number of packets which are constructed at the beginning of a flow graph.
And they are located and they get recycled. Let's say we have just one packet. So it will be one
packet. First is on this noise source. If you use the packet to write, it's output then the packet
goes here. It's going to multiply in place by 42 because we want to do things in places much as
possible. And then it goes to the noise source which doesn't do anything. And then the packet goes
back to the noise source so that it can generate more noise and write it on the packet. And rather
than having just one packet in a circuit, you can have multiple of them. And you have these blocks
on different threads working at the same time on different packets. So this is the idea.
And I know that you say this is new. This is this is not really new. The advantages are many
blocks can work in place on a packet. And so you are effectively making more use of your
CPU cache. Latency is determined by the number of packets. You have a circuit. That's
really trivial. If I only put four packets on the circuit, transmit latency is going to be
at maximum the time it takes to transmit four packets through the final sync.
Also something else. As I was working with packet modems, not only in the
four to toe project, but as well, I was realizing that many of protocols have packets even if they
don't have like a packetized nature, they have sections or frames. And oftentimes you need to
mark those with tags. You might need to align your work functions to the tags. So something good
about packets is that maybe they can be more natural when dealing with this kind of data.
So there is potentially less need for tags and these kinds of things.
It is also more similar to a handcraft implementation. If I told you write these for me from scratching
any language you want, you would probably have like one single buffer maybe and then you pass the
buffer to a noise during the function and then pass the buffer to a multiply constant function which
works in place. And then you're done. And if I tell you, I wanted multi-threaded. Maybe you have
several buffers for the noise source, a multi-plied constant to be able to work at the same
time on multiple buffers, right? So this packet I've been calling in quotation marks, I want to
call it a quantum. First because it's cool people get excited about quantum physics or any
physics related. I know this is not the only use of quantum in SDR applications but
but anyhow. And there are also two serious reasons for calling quantum. The first is that
if I were to call this packet, there are packets elsewhere because packet is a heavily overloaded term.
You have multiple protocol layers. Each of them has their own concept of packet L1L2 etc.
And if you're SDR is on IPSDR which is network connected, it also has like UDP packets. So
let's not call this thing again a packet because that's very confusing. And the second reason is
well the inspiration for this is the idea is not to consider samples as a continuous stream of
data rather to chunk it up in packets. Is the same as with energy in quantum physics, right?
You have these photons which are the quantum which energy can be transmitted. So it's kind of
the same thing. And I want to contain a buffer with flexible margins and I want to explain these
margins within example. So think about computing and adding a CRC. Can you do that in place?
Well no because you need to grow the size of the packet. But if you know before hand that you
want to add a CRC then you can prepare your buffer so that it has some extra capacity at the end.
And then you can do it in place by growing the buffer. This is like the difference in
C++ between a vector size and a capacity. If you design your capacity right from the start
then you can grow your packets by some post-tambles or pre-ambles. So you have like the left margin
and the right margin to be able to add things like a synchronization work or a header to the
beginning without reallocation or going to the different buffer. And you can also strip those down.
So when you receive if you say I'm no longer interested on the header you just say the
margin now somewhere else. So that's the idea. You can also have tags in here marking anywhere
which is a interface within the packet and perhaps author metadata. Here I have a more complex
example photograph. This is not a reall application but it's like a realistic looking application.
And what is different between this photograph and the previous one is two things.
First, in here there's a block which is doing a decimation by two. And in here I have a fun
out connection. So I have a one output to inputs. How do we deal with this?
The idea is to go like this. So let me explain before how I deal with decimation. So what we do
is if I want to decimate by two then it's no longer reasonable to do it in place because the size
of the output is going to be half in this case. So what's going to happen is there's a packet
which is the input. There's a packet which is the output. It's going to be half the size of the input
because you always do like one packet per work function even if you are decimating. And then
that means you have different circuits. So because this is a decimation this is basically
being the sink of one circuit and it sends it back and it's being the source of these other
circuit. So that's the way you deal with decimation. How did you deal with one too many connections?
Because the idea is not to pay for things you are not using all the time. So doing things in place
is great for one-to-one connections. Of course you cannot do it here. And the idea is rather than
to send the packet you cannot copy it out to two different locations. You send just the reference.
So you send in a reference to these two blocks. It's a read-only reference. So these also
cannot work in place. They read from the input which is just a read-only reference. And the right
onto an output packet which has been recycled from here. And then what happens is because of the
way these structures for these reference are organized. The last block which keeps alive the reference
when it destroys it. It's supposed to return the packet to wherever it needs to go to be recycled.
So that's the way you can deal with more complicated flow graphs by still using the idea of packets.
I've gone on many few months working on this idea and I've implemented something I call QSDR.
The Q is for quantum force. This is up in GitHub. Maybe since a month ago or so.
And I'm doing this in Rust. I'm using Async. So this is the same as Futures DR.
But that's just the same commonality. The rest looks quite different I think.
But most importantly and this is how I really began this project. In these QSDR repository there are some
benchmarks comparing goneradio3.10 for the toe and Futures DR and these QSDR implementation.
So you can see where things are regarding performance. This is highly experimental at the moment,
highly working progress. Nothing. Custom schedulers. This is like a huge topic, right?
For the toe supports custom schedulers. Futures DR supports custom schedulers.
And QSDR also. This is my approach. Custom schedulers. Because I wasn't really too happy with the
way for the toe or Futures DR are approaching this idea of custom schedulers. And the reason is yes,
you can write your own scheduler. But it's kind of hard. Before this predict, I haven't written any
custom schedulers for the toe or Futures DR. And it's hard. Now I've written some simple ones.
You basically need to know like the internals of how the DR runtime you are dealing with works.
You are basically calling some functions or something about the API of that particular DR runtime
to write your scheduler. And that makes them not so easy to write. On the other hand, on QSDR,
these schedulers are based on Rust streams, which is a concept I will explain in a second.
And this makes them completely independent of the DR framework, completely independent of anything.
These schedulers are things which run Rust streams. Those could be anything else. They do not
need to do SDR or DSP or anything. So it's like a more general concept. If you want, if your
custom scheduler really needs to, it might dig in the internals of what's going on that is SDR related.
But if not, it doesn't need to do. It's the same as 3.10. 3.10 uses the Linux kernel scheduler.
It's tasks, right, processes or threads. And Linux doesn't know anything about SDR. So it's this
in kind of a DR. But without using the operating system. So quick, primary on Rust streams.
Stream is an asynchronous object which can produce some sequence of values. And in this example,
this is a stream iterator which is going to produce the values 1, 2 and 3. And basically we have the
object. We call it next method. And every time we call it, it returns us 1 values. So it returns
1. Then it returns 2. It returns 3. And the next time we call next, there are no more values. So it
returns non-same unfinished. There is nothing more that I can give you. And something important here is
this a weight. And that means if you are familiar with a weight in any language which supports
async, this means that at this point the stream might not be ready to give us some value. So
we are kind of waiting for it. And if this is a task, then some other piece of code might decide to run
some other task while we are waiting for this. Without the intervention of the operating system
scheduler. You can imagine a more elaborate example where this stream might produce 1, 2 and 3.
But rather than immediately it waits for 1 second between each of the values. So when I call
a weight, I'm going to block for 1 second and maybe some other coding my application runs.
That's the idea. So in Cures, the R, each block has a work function. It is an async function
because it needs to wait for the input to be available. So it's the same as here. We, we,
wait. And that's the reason it's async. The work function processes in general 1 input quantum
to produce 1 output quantum. So it's like 1 quantum per work pull. And then it returns either run
which means call me again. I can do more work. That's like the use of thing or it says don't
I'm finished. Maybe I'm a file source. I've reached the end. I'm a head block. I've already reached my
code. Or maybe there's a file error. Those are the three things that can happen.
Acures the R block. Once you have it connected in the flow graph is converted to a stream.
And each time you call next on the stream, next is going to call the work function once. And
it returns either an error or nothing. And the difference between run or don't, you have it on
this non versus some difference. So it either says, I'm really finished or it says,
I've done some work. I don't have any output to give you like any number. I've done some work.
And you could call next again and expect more work to happen. So that's kind of worse,
streams appear as a natural concept when you are thinking of scheduling work functions.
Then I'd use the R scheduler. It's just some code that calls multiple streams either in
parallel or in one thread or in any specific order. And those streams might be anything.
The natural concept of streams appears when you are thinking about channels which are ubiquitous
in graphs. So if you have a channel, there is like a transmitter or receiver object.
And then you send some data and you get it here. And of course receiving is asynchronous.
It is like a weight received because maybe the transmitter hasn't send the data yet.
And the receiver is a stream object because you can keep receiving messages
until the transmitter is done and decides to close the channel. So any code you have
like a web application framework which is designed to ram multiple channels in part,
you can use it as a QSDR scheduler and there will be an example. There's nothing
custom about QSDR schedulers. And I think that makes me way easier to write.
Something which is very useful to write simple schedulers is stream communicators. The idea of
stream communicators is some operation which takes some streams and produces another stream.
So let's think about if we have two streams which produce either nothing or an error.
And the way this is written in graphs is this result empty type or an error.
If I have these two streams, I can produce a new stream whose next method means
pull the first stream and get something. If it doesn't produce an error, call this a
stream and get something. And if it doesn't produce an error, then that's what you're next.
Function did if some of those produce an error, that's the error. Basically you're trying to
receive two empty messages and if there's an error you really fail when you see the error.
And this is called sequence 2 because it's basically like random two blocks always first block
work, second block work and there are many situations in which you would like to do so because it
makes sense for the flow graph. And yeah there is sequence 3, sequence 4, it's a data combine
more blocks. There is also help for function which is called ran and it takes a stream and it
produces a future. A future is and a synchronous value. So you all wait on it and it returns
something. This run is going to call the stream until it is done or until it produces an error.
Thinking again of the message receiver, it's basically going to drain the message receiver by
receiving all messages until an error or until there are no more messages. And this is the
way you run a flow graph. Basically you call run on your blocks or combinations of blocks
to get them to work until they say they are done because some thing is done and it's saying
that it's not going to produce any more output. And so the block cannot have any more input and
so it is done as well. And this run function is usually the top level for schedule on its thread.
Code in example just to give you an idea of how friendly or ugly the code it is. I want to
dwell on a couple specific points. So in here we are defining a type for the buffer. This is
like a short term to avoid writing too much. It's the same as using in C++. And of course we
have like custom buffers in QSDR because everyone has and this is like the basic cash line
buffer for floating 0.32. Here we are defining our buffer. So we create an iterator which is going to
instantiate number buffer. So four buffers of 1996 flows each of them. But this isn't creating the
buffers at this point because this is an iterator. iterators in Rust are lazy until you do not
execute them. Nothing happens. We create a flow graph. We add some blocks. Something to point out here
is I have this single-producer single-consumer and single-producer single-consumer reference. This
are the input and output challenge for this particular head block. This is null source, head and null
sink. The most simple photograph you can have. And the thing is many things are like have type
parameters. In the C++ world you would say they are templates. And the data types or the buffer
types are part of the template or type parameters and so are the child types because if single-producer
single-consumer is the most efficient channel. Why would you pay for a more general child to do
connections where that suffices? So that's the reason you see here different channels specified.
Then we do this circuit. So we have these closed circuits. To do connections we create a new circuit.
Use that circuit to make a connection and then the last connection we do. We need to say where
the packets need to return to be recycled. So basically I have my source. It's connected to the head.
Then the head gets connected to the sink. And the return point for that connection because the
sink basically is the finishing point for that chain. It is to go back again to the source to recycle.
Then we validate the flow graph to make sure there are missing connections etc. And then there is
this piece of code which is somewhat technical and probably we could not need this if we use a macro
to generate this for us. This is because when we are adding the blocks we are not really adding the
blocks as they will exist when the flow graph runs because for example the chance that these blocks
will use the communicate they do not exist until you do the connections. So we are adding like some
seed for the block something that it's going to become the block once it's properly connected and
everything. And in here we are finalizing the block into something which can really run. That's the
reason we need this. And then finally what happens here is we convert each of the blocks into a stream
and we use our sequence 3 to say it's always going to run source head sink. Run and block on
this is nothing about queues. This is a async executor in Rust which means run this thing on the
current thread until it's done. And this is really a custom scheduler. This run sequence 3 is set custom
scheduler a very simple one but it's a custom scheduler for queues. The R which says random blocks
in this order always. How do you write the blocks? There's a whole bunch of template parameters
or Rust type parameters here that you can basically copy and paste from a similar example. And then
what you have are ports. This is kind of inspired by a grader for the toe who also has the ports
like class members. But the nice thing about Rust is that these are really zero-sized objects.
They do not do anything at runtime. They are just to make our life easier a compile time.
When I was saying at first this is like the seed for a block, then it's going to be coming to the
block which can run and there are channels. These will be replaced by proper channels,
centers or receivers to send these quantum between the packets. And then we can have any local
variables we want. This is a head block. So we have the remaining number of items. We have a constructor.
This is like a regular constructor. And then we have the work function. There are like different
flavors of work functions. You can have the pen in on what you need to do. And in this case this
is work in place because it's going to work in place on the item. So it's going to work in place on
this quantum or buffer. In the particular case of head, it doesn't do anything with the data.
It's just going to say it's done at some point. And that's also another place where this concept
it's nice. It doesn't need to do a memory copy. So in here basically I get a mutable reference
basically a pointer to my input and output data. I could go in there on multiply by 42 every element.
If that's the kind of thing I want to do. In this head I'm just counting elements. And when
if reach zero I say I'm done. But you could multiply by 42. An example multiply constant would be
like iterating over the samples in here. I'm multiplying by your constant. And it would be a
very similar example. Okay, so benchmarks. This is probably the most interesting part of the talk.
So to do benchmarks, this is the process I wanted to follow. First choose a family of simple
programs because if it's a complicated photograph it's hard to understand where the bottlenecks are.
And you also need to implement it in four different NSTR runtime scientists point. So it's going
to be a lot of work. And maybe you don't implement it in a very smart way in one of them. And you
are misperforming or underperforming because of some choice you made or some mistakenly. So
simple photographs. And first, I'm very importantly right by hand an implementation of this problem
that performs the best you can. Without using NSTR framework just ask yourself how fast the hard work
can actually run this task. And write it and benchmark it. Then you can go and write implementations
using photographs in these four different NSTR runtime. And you basically want to measure the rate
at which your photograph is running. So basically the sample rate. The benchmarking platform
I've chosen is the embedded ARM CPU, which is on the science MPSOC. And that's a quad core
cortex A53 CPU. It's 64 bit ARM. It runs with one dot 33 gigahertz clock. If you are thinking
in Raspberry Pi terms, this is a CPU on the Raspberry Pi 3. Maybe not exactly. So maybe not the
same cache size, but it's the same CPU core, the A53. The reason I'm interested in this is because
I'm interested in mostly embedded systems. These are the kind of systems I can launch into space.
And also because if you look at the newest generation of HDRs, they usually use these science
FPGA SOCs, which has these cortex A53 platform. So here this is the X400 series,
USR P. This is from analog devices, the Jupiter's DR. And this is a development platform for
the IRF versus C, which is called the 4 by 2. And these two are IRF versus Cs. And this one is
not an IRF versus C. It's an MPSOC with some other ADI RF chip. And I know what you are thinking.
These platforms, they all have an FPGA, you should be doing your compute on an FPGA. Sure, sure,
I mean, but the cortex A53, it's rather capable. It looks like an old ARM CPU, but you can do
nice things with it if you program on an efficient way. And then the advantage is it's much easier
to write software than FPGA. You do not need an FPGA engineer. So by having a very efficient
DR runtime, to use on these platforms, you can solve problems that maybe you do have needed to go to
the FPGA. For this, I'm using Cria KV 260 board. It's $250, which is some money, but not so much
compared with all of these. It doesn't have any RF hardware in it, but you can run software. You could
also program the FPGA if you want something nice. Also, if you want to replicate these benchmarks
and run the same thing, you can get the same board. If I were to run this on my desktop computer,
it would be hard for you to replicate exactly my desktop computer. This is how the flow graph is
going to look like. So there is a math kernel, which is going to be the SACS P kernel. And this
basically takes an input, it multiplies it by a constant, and it adds a constant. And here's
note, usually in the literature, where people speak about SACS P, they mean in here a vector.
I want to have a constant, because I want to have just one input, one output. So this is just
like multiply constant, but you add also another constant, not particularly relevant for SDR in general.
I thought it was just more interesting than a simple multiply constant. And there is also
this fancy FMA operation on many CPUs. So here is your FMA operation. Unfortunately, in our
FMA is not very good to implement this, because the A doesn't mean add, it means accumulate. And that
means that the B goes in the same register where you put your result, and that's no good for implementing
this operation. But anyhow, the family of programs is just an old source, which is a real
null source, not like the one in your radio. It doesn't do anything. It doesn't mean that the output
it just pretends, it's produced some output immediately. Then we have a chain of SACS P kernels,
and we will play with how many. I'm finally a benchmark sink, which is just counting how many samples
I'm calculating the sample rate. SACS P kernels implementation. I said before, do this the best
way you can. And the best way I can is I go, I hunt right assembly, using neon CMD for this particular
CPU, knowing the exact limitations the hardware has, I know I can do as good as one floating point
number per clock cycle. And that's two floating point operations, because you have the multiplication
and you have the addition. And I have a list of limitations in this CPU here. If you're interested,
I can go in more details about why this is the best you can do with the hardware. For comparison,
an optimal memory copy is only slightly faster. It's 1.33 floats per clock cycle. And the reason
for that is basically on your CPU you have a 64-bit load path, and I want to do a bit store path,
it's kind of a symmetric for a weird reason. And if you want to main copy, you need to go through that path,
and you cannot store on load at the same time. That's how the main copy performance comes.
In here maybe, another note, I've seen people using main copy to benchmark SDRs,
like do memory copy blocks. And I don't think that's a good reason for two reasons first,
you do not know what's exactly on your C library, do a main copy. Maybe I move to a different
distribution, it's slightly different main copy, and it does something else. So that's one reason.
Second reason is I'm always saying on this talk, let's do things in place, and main copy in place doesn't
make sense. What else, this hundred and neon assembly is twice as fast as the code you'd get
from GCC clang, rust, compiler, everything if you write this. And the reason for that is,
well, LVM has a nice tool which is called LVMMCA. It will show you how your machine code is
executing through the CPU pipeline, how many cycles it takes to run, et cetera, et cetera. And that's
the same kind of tool it uses to generate code for you, which is efficient. But if you look at
these for the A53, the results are completely wrong in LVMMCA. Really, LVMMCA has no clue how this
CPU executes code. And the main reason for that is the documentation about how this CPU runs code
is not public. The things that people know are because they've read in the A's or because they've
just don't reverse the engineering. And there's like some full color knowledge of how some
instructions can't view all issue and some of them cannot. So that's the kind of things you need.
This is to give you an idea of how the current looks like. So it's a whole bunch of code. And this is
not because the loop is unrolled. There is a branching here which goes somewhere there. So the main loop
is like half of this code. And you need to have all of this to actually be that fast.
Some other nice details, we have these preferred memory instructions which are free to call
because they do a issue with the presenting instruction. And that makes it more likely that the
data we need is in cache and L1 cache by the time we need it. So we want to benchmark these. And
the way to benchmark it is, we call this function many times on the same buffer of a fixed size. And
we measure how much samples per second. And as I said, this is a 1.33 gigahertz CPU. The thing
ideally runs at one floating point sample per second. So my top maximum, which is the max on this
plot and all the ones which are in the rest of the top is 1.33 giga samples per second. That's
the maximum hardware limit. And you can see when the buffer is molded overhead and I'm doing
less performance than that. When the buffer increases, then I almost reach it and then there is a drop
and then there is a drop. And I can ask you, what's the cache size on this machine? And it's
pretty clear from the graph, right? It has an L1 cache which is 32 kilobytes. It is per CPU and then there is
an L2 cache which is shared by all the cores, the four cores in the system. And basically you can see
when you're running a L1 cache, you're in here L2 cache, you're in here. And DDR is playing full
is low, you are in there. So cover quick comments about SDR runtime performance. We want to spend most
of the time running the work functions because that is where our problem gets solved. The rest is
just deciding what is going to run copying data from point A to point B etc. SDR runtime performance
depends on the number of work calls per second because if you are doing many work calls, you need
to schedule many things per second and you're going to have much over it. So, and if idea would
be, I can reduce the number of work calls if I use larger buffers, but then no because you need to
stay within L1 cache otherwise you're going to be paying full is low as you saw. So you definitely
do not go, do not want to go to DDR. The worst example you can benchmark are really simple math
kernels which can run really fast of L1 cache and so basically your work function is very fast
and you are going to need to call it many, many times per second. For example with his sax
speaker now, if you do a 16 kilobit buffer, which is half the L1 cache, that's just 4000 floats.
So it takes three microseconds per work call. If you think about this and this is an embedded system,
if you ever call the Linux kernel to sleep or to do something for you, that's a huge overhead
compare with three microseconds. So I have some figures in here and maybe go over. So what I'm doing
here is I'm basically running the kernel on multiple CPUs. So on two CPUs, the one on the left,
three CPUs, the one in the middle, four CPUs, the one on the right. And the thing that I see
is a reduction in performance as I increase the number of CPUs and this is because of communication
between CPUs because the data which is in L1 cache of one CPU needs to be read by another CPU.
And it's quite tricky how it gets there. There's not enough documentation from
ARM for us to be able to know what happens on a cycle by cycle basis, but basically this is
the boilerplate. There is a price to pay for CPU to CPU communication.
This I probably also want to close over because I can explain it in the context of SDR runtime.
So let's benchmark SDR runtime. First I want to do a single kernel, single core.
Very simple. So the photograph is null source, sackspy, benchmark sync. And I use the simplest way
I can to run everything on the same CPU that depends on the SDR framework. I can give you
letters if you are in details if you are interested. And this is the sample rate I get. Again,
the maximum possible is the top of the graph. Futers DR is here. It's doing 50%.
And maybe the reason is that Futers DR is also using Rust channels. And for QSDR I really
need it to write custom Rust channels, like hyperformance channels because the ones which are normal
used are not fast enough. So maybe Futers DR would benefit from using the same kind of optimized
channels. We have Galerated 3.10 here. It's awful. I don't know why it's awful with this problem,
but if you look at this, it's running the 3 threads in the same CPU. This is supposed to do
all the math. These are supposed to do nothing. The 3 threads are using 33% of the CPU that tells
me there is a huge overhead in everything besides my warp function. And then we have going to
ready for the toe, which is doing pretty well. And here's DR, which is also doing pretty well.
In this one slightly less than 4 to 0, but we will see cases in which it does better.
So now it's the same kind of problem. I can probably explain it directly with 4 CPU cores.
So what I do is I have 4 CPU cores and I'm going to run the null sync several
sacksp kernels and then the benchmark sync. And the way I can do this is I can pin my sacksp kernels
to fix CPUs. So for example, if I only have 4 kernels, I pin each of them to 1 different CPU.
If I have 5, then I need 2 of them to go on 1 CPU and the rest can go on their own CPU.
That's cutting my performance by a factor of 2, because now my bottleneck is I have 2 math kernels
sharing the same CPU. But that's like a simple thing to do. And you can see that maximum
performance you could achieve with that is this line here. Basically you are cutting off it by a
factor of 2 when 2 of them need to share at least 1 CPU and then this is 3 of them need to share
at least 1 CPU. So you have this staircase pattern. There's something else which is a work stealing
runtime. And that means it's basically figuring out where in which CPU tasks can run a
depending on usage. And then if I for example have 5 kernels to run on 4 CPUs rather than always
running 2 kernels on the same CPU, it can sort of run probing. And so basically the performance is
like 1 divided or 4 divided over the number of kernels you have. So it goes like this.
And then what are the things we have in here. So basically in this light blue is my hand implementation
of things. This pink is curious the R with the custom scheduler. They're really simple sequence
thing. We saw only in each CPU core. This gray thing is curious the R with the Async executor.
This is like a regular Async executor from rats that people use to run any kind of Async code.
So it can be used as a runtime or a scheduler. And then we have gone radio for that over here.
This is basically a custom scheduler. I wrote which is supposed to be doing the same as the
custom scheduler. On here's the R and futures the R. And the simple scheduler you get by default.
And this is quite a little bit for months. If you compare it for example to 2 CPU cores where it
does a better or even 1 CPU cores where it does quite comparable where it is. Yeah, it's the
brown line here. So that's comparable to peers. They are on one CPU core. On four cores I don't
know why it's performing quite worse. And then we have futures the R somewhere in here it's the blue
and orange. It's always like half the performance of Guru Radio 4.2. And Guru Radio 3.10 basically
performs the same no matter what kind of program I give it within this constraints is always
as low. And you can see no difference. And that's basically 8 about benchmark and I run in
out of time. So just to think as this graph show there's a lot of room for improvement in
SDR runtime performance. And who knows where the project this curiously I project might
develop in my stay an experiment or in my go further. I don't know.
I think everyone agrees that they can take one or two three questions.
Yeah, not the question just just to feedback from the customer works and I kind of
shut it down to the resource. But the approach is very similar just to convince the recycling
the rocks. My personal experience is sometimes it's used this compared to the reality.
Because if you use the same size in fact the memory I look into a basic variable. Yeah, it depends
on that. And the second point is in my case I need multi-channel rocks,
which is called mantons. So you have SDR providing a synchronized multiple channels. Look for example
a later F. Yeah. And then one key point is how do you manage synchronized channels. And
I came to the view that was more efficient to put in the same quantum the two rocks from the
channels. Yeah, you can have like a tensor in this case it would be like two by and many samples.
Yeah, because then you brocks like a DOA or stuff like this. Don't need to go away from
form two streams. We'll see the two rocks which are synchronized. Yeah, just the fact. Yeah, thanks.
There were some hands in there.
Yeah, so the question was how using external accelerators affects all of these? And yes, I haven't mentioned,
but I always had like the FPGA accelerator use case in this case. Yeah, so the question was how using
external accelerators affects all of these? And yes, I haven't mentioned, but I always had like the
FPGA accelerator use case in mind when you are using an FPGA. You might be using packets over an
access stream boss. And that's also where these whole idea comes. Things can be quantum on your
software side. Then they get transferred to the FPGA by DMA or maybe as VDP packets, it depends.
And then they are access to impacts on your FPGA. And in the GPU, you have like a device to close
the scope. It's a dining sub buffer. All the questions, Johannes? Yeah, so as you look at the flow
drop, like the first few stages for the four and six or nine, those are mind-bд ones where you
have rather look at this than the continuous question, like actually like basically your example here.
But usually in the digital communication, you synchronize and then every packet is anyway.
So is that more of a saying, hey, we need to look at different parts of the flow drop differently?
Yeah, so the question was, when you have communications receiver at some point, it's a continuous
stream of samples, but then you kind of have packets. And my answer is, yes, so at some point,
you really really need to use packets because that's the kind of thing you are processing.
But also in the beginning, where it's a continuous stream of samples, maybe if you chunk it up,
you are like defining what's the buffer size you want to work with because I think high
performance, maybe you've benchmarked these or have it as a parameter you can tune,
so it can also make sense. For example, I don't know, or if Dn receiver, it's going to be half
fixed FFT size, maybe you do things, which are in blocks that come out nicely when you get to the FFT.
Yep, we mentioned the best about the buffer and the number of flows you can produce,
was it a memory constraint or a computational constraint?
In here you mean?
None of the many protocols, yeah, yeah, this one. So you mentioned that the buffer was the catchest
in the FFT to deal with, is it, so the core algorithms for both log graph was a constraint by
how much you read from the memory at the past, or how fast you can, yeah, it should be
pretty different. Yeah, so the question is about whether compute or reading from memory
its limitation, for this particular math operation, both reading the data into the CPU registers
and computing is the thing that sets the limit, and that's what gives you the maximum one float per
cycle, provided the CPU can read the data in one cycle because it's in L1 cache. If you
descend in L1 cache, you're going to be installing the CPU because this is an in order
execution CPU for as many cycles, and that's where these performance drops come. So in in this
part of the graph, you do our memory bandwidth constraint, other questions? Okay, thank you,