Bryce

For anybody who's familiar with NumPy style broadcasting, where when you're working with two arrays of different already sh It's Aridi, not Artie Artie sounds like Ariana Grande's cousin. Alright, we're we're cooking here.

Conor

We might have explained the scatter completely wrong.

Bryce

We're just two b two men sitting here talking into uh ChatGPT.

Conor

What I should have been saying is gather, but I'm not even confident about that because my confidence is shot because I totally messed up the description of an algorithm.

Bryce

So get out to yourself, man. We're cooking here. And so once again, I shall tilt it windmills on my quest to figure out whether we can do this on a single pass algorithm. Uh is it poorly named?

Conor

Maybe it's actually it's actually well named and scatter is poorly named. The point being is I don't like the asymmetry between scattered and permutation iterator.

Bryce

When you launch a CUDA kernel, you have to specify how many blocks you're running with. This this microphone's a little bit waterproof, right? I don't know. Okay.

Conor

We continue our conversation from last week about the replicate algorithm and two other algorithms, scatter and gather. Alright, folks, so right off the bat, I do need to apologize. In this video, for the majority of it, I refer to the scatter algorithm, when really what I meant was gather, and I got incredibly confused. We rectify this at some point in the conversation. Scatter is an algorithm that exists in Thrust, doesn't exist in the C standard library, but I'm sure it exists in uh other libraries and languages. You're given a sequence and uh like uh your your data sequence, which could be, you know, numbers, whatever, any type, and then you're given, I believe it's indices, and I'm gonna feel bad if uh this is incorrect, but I'm almost positive it's a sequence of integers uh that uh correspond to indices that are between zero and one minus the length of your data sequence. And uh, or that's what the values of the the indices have to be, obviously. It has to be a valid index. And the length of your, I believe it's what they call um an index map, um, the length of that, alright, perfect. Let us sit. Is this gonna be wet though?

Bryce

Although this is a it's a theater, which is not open.

Conor

It's clear clearly a restaurant, but hopefully just no one will come and serve us. Although this is wet as well. These ones are dry.

Bryce

These ones are well, these can't possibly be wet.

Conor

They are But these tables are not clean, so I think this is totally fine. No one's gonna come and serve us here. A little dark, but that's okay. We got a nice view. Um so the length of your in so you've got your data, and you've got an index map that can be any length you want. So if the length of our data is three ABC, we can have uh index map that consists of just a single one, and what it does is it scatters the element at index one to your output sequence. So if you scatter, if if you so scatter, you've got your data, and you've got an index map, which is a sequence of indices. And scatter basically goes, it iterates through the indices and then copies them to so it's like a copy if, except that a copy if just takes a predicate and then it it's a stable copy where the relative orders are the same, obviously, because you're just getting rid of some of the values, and then anything that corresponds. This is not how permutation iterator works. You're just gonna permutation iterator. I'm I'm trying to explain that sequence, if for those that aren't familiar with uh thrust sequence, thrust sequence is the equivalent of stdiota, the algorithm in C. It generates incrementing integers starting from a value, typically zero. And in C20, we added the view iota, which does the exact same thing just lazily. In thrust, the two names for these things are thrust sequence and the thrust counting iterator. The same way that there's a relationship between sequence and counting iterator, there's a relationship between scatter and the permutation iterator. So scatter takes your data and an index map and it copies from the indices that and the values that correspond to those to an output sequence. So if you've got ABC and uh one, you just get the character B. If you've got ABC and 3, 2, 1, you end up reversing your sequence. If you have ABC 1, 2, 3, 3, 2, 1, your output sequence is ABC C B A. You're basically, you know, it's pretty straightforward. A permutation iterator, potentially better named as the scatter iterator, is the same thing, but you're just bundling an iterator that points to your data and an iterator that points to this index map. That's it. And so if you're trying to do any kind of operation lazily that is a specialization or some form of permutation, you can do it with a permutation iterator. You need to reverse a sequence, you can do that with a permutation iterator. You need to cycle a sequence, you can do that with a permutation iterator. You need to replicate a sequence, you can do that with a permutation iterator. A lot of the times, it's not the most performant way to do this stuff because of the fact that a permutation iterator is so generic, there's some overhead there. But the point being is that a permutation iterator, it's is it poorly named? Maybe it's actually it's it's actually well named and scatter is poorly named. The point being is I don't like the asymmetry between scatter and permutation iterator. And anyways, this is all to say that the very first implementation of the replicate uh integer overload just made a call to the permutation iterator where the index map was just cycling indices. Which, how do you do cycling indices in thrust? You can just do a counting iterator composed with a transform iterator that does modulus over the length of your sequence. So you generate the numbers, you know, if you want to uh replicate it. Well, I guess. Oh, actually, that's a good point. Oh no, right, because n is the same for everything, right? So if you're trying to replicate everything three times, you do a counting iterator, zero to nine or zero to eight inclusive, and then you do modulus three, and then you're gonna end up with zero one two, zero, one two, zero, one two as your index map, and that is the uh equivalent of a replicate when you apply, you know, uh a copy if using that per so that actually that's a that's a great way of framing it. Scatter can be implemented and probably is implemented, well actually I don't know, I don't know that, as thrust copy if with a permutation iterator. That's just what scatter is. Anyways, I'm gonna stop talking.

Bryce

You did a good job of explaining it. I I I would um yeah, for the for the integer version, I mean I guess you you can just think of it as as just being that you're you're you're um you're copying from the array, but uh like like you've I I have an array of length, you know, n um and then I um I want to replicate each element m times. So then I've got an array of length n times m, and when I index into that array of length n times m, I'm indexing into the original array of length n, and I'm just moduloing so that I wrap around. And that's that's that's it. And that's probably the m fastest way to do it.

Conor

Except I totally messed up. As some of you are screaming in your head right now, that doesn't make any sense because it doesn't. What I just described is cycling. If you're replicating, you want to be doing the opposite of modulo, which is integer division. Right. So you want to go from zero to eight inclusive, not to zero one two zero one two zero one two because that results in ABC, ABC, ABC, which is cycling. You want to have zero zero zero one one one two two two, which is integer division, and then you end up with A, B, B, B, C, C, C.

Bryce

Yeah, yeah, it's sorry, that's I I should have also. But but um but and permutation iterator should boil down to just being the integer division. Although you were saying earlier that you hit some perfection, right?

Conor

Permutation iterator doesn't do any of that work for you. You have to give it the index map. So it can be it can be completely a random sequence, right? If you want to do a shuffle.

Bryce

But so for the index map, you're passing in some sort of transform iterator that does the division?

Conor

Exactly. So the the transform, it's a counting iterator with a transform, so it's counting iterator, piped or whatever, to a transform iterator that does the integer division, and that iterator, those that iterator that is the composition of those two iterators, you pass to a permutation iterator. And so if you want, you you could just do like a random shuffle by generating a random sequence of indices, and then that you know, call a copy if with that permutation iterator, and that would be a shuffle, right? You can do anything almost with a permutation iterator. You can do a copy with a permutation iterator where your index map is just a counting iterator. You wouldn't want to do it that way, because that's ridiculous. But like the point being is that like the permutation iterator is like the most powerful of the iterators. Like if you think we've got constant iterator, we've got counting iterator, we've got transform iterator.

Bryce

To to be uh an important clarification I just realized is we mentioned a replicate iterator before, and we mentioned two overloads of replicate. The replicate iterator only exists for the integer version, right?

Conor

Wait, wait. There's no replicate iterator. There's two overloads of the replicate function. One like they both take the data sequence, one takes an integer and the other one takes an array.

Bryce

But but but sorry, to clarify further, one could imagine writing a replicate iterator for the integer version um because you can do that lazily. I don't think you can do it for the array version.

Conor

Yes, if I understood what you just said, the integer version is lazy. Yeah. The array version is not.

Bryce

And the the reason the reason for that is because for the array version, it's trivial a priori to know ahead of time like the length of the sequence. And so then thus to know like the array version. The integer version. It's it's it's trivial to know ahead of time the length of the sequence. And it's important to know the length of the or not just about knowing the length of the sequence, it's trivial ahead of time to know the placement of you know of where every element's gonna be in the output. Whereas when you've got variable size uh replicates, you don't necessarily like the it's no longer an independent problem um, you know, the the of where each element is in the output, and that makes it some form of scan, um, some form of copy of. And and and but you mentioned earlier running into perf issues with the permutation iterator. Was that the case? Or I I think that the implementation you just described should be the impl the optimal implementation.

Conor

Using the permutation iterator, which is a very generic utility, is I have found a lot of the times comes with overhead because we're using a permutation iterator to do something more specific.

Bryce

Yeah, yeah, yeah.

Conor

What we really want is a custom iterator that doesn't incur the overhead of something so generic. And so I a lot of the times things like cycle iterator and cycle iterator, replicate iterator, a couple others, I would all I would always implement in terms of the permutation iterator because that that was the utility there. But one time I was profiling my work versus Jax, which I consider or uh anecdotally in my profiling, I've uh the the Jax jitting typically is the best for this kind of stuff. And there was one example that was uh Jax was beating the work that I was working on. And so I started doing some tuning, and I found like three different times switching the cycle iterator to basically implement a bespoke thing that didn't rely on permutation iterator and doing the same thing for the replicate iterator, each of those led to like a performance improvement. And I was kind of surprised, but it it does like anytime you're reaching for something that is not not in terms of like a generic reduction, because you're just passing a binary operation, but something like a permutation iterator that has to be capable of more, similar to when we talk about using reduced by key, if you want to do like a row-wise reduction, reaching for reduced by key, which can handle variable length segments, whereas the rows of a matrix always have the same length. Anytime you're doing that, it shouldn't be surprising that it incurs some kind of overhead.

Bryce

So uh but it it is weird because um because it's just this like iterator abstraction, um, it should just melt away. But I I'm not I'm not shocked to hear that you did run into perf issues with it, because we're talking about an operation that uh uh we've talked about two operations like the the cycle and and replicate, both of which boil down to doing uh integer division or integer modulo. Um and so it's possible that the kernel ends up being uh like integer throughput constrained or maybe even register constrained, and that uh some of the abstraction cost of permutation iterator, which again should be able to boil away, um uh may still be present. I guess it's i I I I I think it's maybe also possible that that the permute the use of permutation iterator throws off uh like vectorization, uh, and maybe that's causing problems. But um, but anyways, uh it would be interesting to look more into that, but but I think we digress.

Conor

Yeah, and I think what'll happen here, folks, is we recorded the episode in Aarhus last. And so I think we've we figured out that you were gonna be listening to that on October. Let's let's check the calendar, folks. We've got too many apps, the calendar's way down there. You will have already listened to the Orhus episode on October 5th. We are going to actually, so wait, this episode's gonna come out on the 12th. So maybe I was gonna say we're we plan on recording at least once when we're at NDC Tech Town. We might do it on the Wednesday night. They've got like a little social afterwards, and it'll our our workshops will be done, your talk will be done.

Bryce

So I think probably It'll be interesting to see whether uh either of us still have a voice after two days of eight-hour courses.

Conor

Yeah, it's uh it's a good point. Pending that we have voices, we will record uh probably in a couple days on the Wednesday, the first day of NDC Tech Town. And so the the seven episodes ago when I mentioned that, like, oh, remind me that we got to talk about replicate, is that it is, I think, one of the most interesting uh algorithms that I've implemented because it is it has such an interest like the going from the integer overload, which is pretty trivially done and not only done but done lazily, to the array overload, it's compl it's almost a completely different algorithm because you can't do it lazily, or you definitely can't do all of it lazily. And uh so think about it. You know, Bryce already gave you a hint, a scan is involved, which it's almost become oh, I guess it's the integer. I think it is just I think it is strictly just run length decode. I mean, but run run length decoding though is cheating.

Bryce

How is it cheating? It's the s it's the same inputs. It's gotta be just run length decoding.

Conor

I guess it is just run length decoding. Well, Bryce is currently looking up. Does Cub have a run length decode?

Bryce

Okay, now I gotta look at how it works.

Conor

Yeah, what's the API?

Bryce

It takes run value and it it takes values uh and uh as an array of n and it takes uh lengths as an array of n.

Conor

But we're still going to talk about how would you implement this in thrust if you were limited to using thrust.

Bryce

So run length decode and cub, um it decodes into like a window of a limited size, and then it's repeated until all the runs have been decoded. I'm almost certain that the way that Cub does it is not gonna be the way that you've implemented it, because you described implementing it in terms of a scan, which sounds right, but I think what Cub does is Cub picks a static fixed-size window um and it starts with the sequence and it decodes, it starts decoding what it's got until the window is filled, and then uh it must write the window to the output, and then it starts doing another window. Um why is this optimal?

Conor

That doesn't sound very parallel.

Bryce

That does not sound very parallel, that sounds very sequential. This is for the block length one.

Conor

I'm gonna have to look at what the I would guess that using a scan, you can figure out where in the data sequence the beginning of each window uh decoding starts. Because you can you can back that out with a operation that I will not reveal here, but I I think I think we're essentially saying the same thing?

Bryce

Uh no, no, no, no. I think we're essential so essentially we're we've come upon how do you implement efficient GPU decompression. So this what I just described is was was for the block run-length decode. Alright, you you talk about things while I look at what the uh the device-wide one does.

Conor

Bryce is deep reading GitHub code. And uh it's good to be in Denmark. Had a lovely run today. We did a long run. 30k. Started off a bit rough because I did not eat anything, but then I got a power aid at the 11k mark. Denmark is a very beautiful country, Copenhagen's a very beautiful city. We'll definitely be back. Yeah, beautiful evening. So good to be alive, folks. Good to be alive. Bryce wants the mic.

Bryce

So there's no there's a device-wide run-length encode in Cub. There's no device-wide run-length decode. There's just this block run-length decode that allows you to retrieve a window from the run-length decoded array. Which is I guess makes sense if you want to like do block, if you want to just like process like a chunk, like you want to be able to work with like a fixed-size chunk when you're doing like CUDA programming. But uh, okay, now I want to hear I want to hear how you implemented it.

Conor

Well, I need to uh I would need to go back and actually look because I like I said, the first time I implemented it, it was incorrect. But off the top of my head, whether this is the actual implementation, and this may this discussion may never see the light of day if it's not, you do a plus scan on the integers in your integer array. Bryce doesn't even listen anymore.

Bryce

I am, I am, I am. What are you doing?

Conor

He can multitask, he says. You do a plus scan on your integer array, and that gives you the in indices where each of your uh decoded segments start.

Bryce

Yes.

Conor

That's the first step. And then once you have that, you want to basically create an index map that corresponds to copying the first element at index zero to however many spots the first integer represents. So if your sequence is ABC and your index map is one, two, three, you want your resulting index map to be zero, one, one, two, two, two. How do you do this exactly? You want to. And this is the this is the tricky part.

Bryce

So you have your plus scan that is gonna give you a scan, right?

Conor

Yeah, well, I mean the plus span g gives you one.

Bryce

But the index map. You can make the index map with a scan.

Conor

I don't.

Bryce

You can do a single pat hang on, hang on, hang on. I think you you just described you need you need to figure out where you're running.

Conor

I don't remember, I don't remember. So you do your plus scan on your integer sequence, and that gives you one, two, three, six.

Bryce

And those are those are the lengths.

Conor

That's technically the the ending uh index plus one of each of your segments. So if you prefix that plus scan, if or technically, so I mean in in Thrust this is an exclusive scan, but I don't I don't disambiguate. I only have one scan, and if you want an inclusive scan, an exclusive scan, you just prefix it with a zero. So if you add a zero to your one, three, six, so what one three six to repeat and be clear, is the result of a one, two, three plus scan. So the first element is just your one, your second element is one plus two, your third element is one plus two plus three. So one, three, six. Add a pre-pan a zero, so you end up with zero, one, three, six. We're not gonna need the six, folks. That's a little secret. And so now what you want to do is you want to do a scan.

Bryce

This is a very weird scan. Wait, wait, wait, wait.

Conor

I think we might have explained this we might have explained the scatter completely wrong. Because alright, I'll just think out loud. I'm allowed to make a mistake. So right now what I'm thinking is that with the n minus one values of your zero prefixed plus scan. So zero, one, three, six, if we drop the six, you want to scatter the indices basically zero, one, and two to those indices. You want to scatter zero to the zero index, one to the one index, and two to the three index, which is a that those indices you're gonna do a path later than you're gonna then you're gonna do a max scan. So if your output sequence is gonna be six values in length, you do two scans. You do two scans, you do a plus scan on the integer sequence, then you do a scatter uh to get the uh initial value of each segment in your output sequence, and then if you do a max scan, so what what your output sequence it starts is six zeros, then it starts as zero in the first index, one in the second index, and then uh two in the third index. So there's it's zero, one, zero, two, zero, zero, I believe, if I got I got that right. And if you do a max scan on that sequence, basically, and you know that because the values are indices, so they're always increasing, you're gonna end up with zero, one, one, two, two, two. So the one basically gets copied to everything up until the two, and then the max scan, the max kicks in again, and the one resets to a two, and then everything after that is a two. The thing that I've realized though is that I'm pretty sure. Well, actually, I'm now I'm confused. A scatter might not scatter indices to a destination. It it might it might scatter the elements to that index. We have to look that up. Actually, in my lookup, I'm just gonna ask Gemini. Actually, we'll we'll get Gemini on the pod right now.

Bryce

Write me a kernel that does device-wide run-length decoding using the card.

Conor

What does the algorithm scatter in the thrust parallel algorithms library do?

Bryce

We're just we're just two p two men sitting here talking into uh Chat GPT.

Conor

I'm a little bit surprised because in the past it usually reads it to me, and I guess I click some button that uh turned off the live reading. But it says the thrust scatter algorithm in the thrust parallel algorithm library is a powerful and efficient parallel primitive that reorders a sequence of elements. It copies elements from a source sequence to a destination sequence, but instead of a one-to-one copy, it uses a map to determine the destination index for each element. So, yeah, it's uh I I did uh no, no, no, I I messed it up. So it says the essence is that ThrustScatter performs the following. The destination of the map of the i th index is equal to the source at the i-th index.

Bryce

Which means write it again, but no comments and make it as concise as possible and remove all of the error handling.

Conor

So what does this mean? Do we have to do we have to cancel the whole scatter conversation?

Bryce

I don't know. So I think I think in most cases you will do a first pass, anyways, to determine the size of the decompressed sequence because you need to allocate memory for it. Like I I i I struggle to imagine a use case. I I I struggle to imagine a use case where you want to do the decoding, but where you don't need to know ahead of time how how large the thing is gonna be when decoded.

Conor

I mean that's just a I mean of course, but that's just a that's just a reduction.

Bryce

Right, right. But I'm saying if you have uh that in for or if you're going to do that reduction.

Conor

And also it's not even a reduction, it's just the last element of the scan that you have to do on the right.

Bryce

Right, that that that's what that's what I'm getting. That's what I'm that that's the point I'm getting to is that is that if you uh if you're gonna do the scan anyways, like the the way the way that uh when I asked Chat GPT, it did like a reduce first so that it could allocate the temporary storage, and then it did a scan to figure out you know the the the where shit's gonna go. Um and then it did a third pass to actually move stuff. Um and three passes seems like overkill. You don't need that first reduce, you can just do the scan. Um but I think that this may be a case where you can't get it down to one pass. And the reason you can't get it down to one pass is because I can't imagine it being useful to like be like, oh, I want to decode this thing, and here's like you know, a gigabyte of storage to decode it into, but if it's bigger than that, I don't care. Although now that I say it, now that I say it, if I'm maybe, maybe actually I'm wrong about that. Maybe like if I've got like some some thing, some image I'm decoding or something, where like I want to decode it into like a fixed-size buffer and then I'm gonna write it to disk, right? Because like I don't know how large this thing's gonna be when I decompress it, but I want to like decompress it in stages and then write it out to disk. And so once again, I I I shall tilt it windmills on my quest to figure out whether we can do this on a single-pass algorithm. I'm almost certain that there's a way to do this with a single-pass algorithm. However, it might require the use of some things that um that we've never talked about before, in particular a completely different model of GPU computing, which we don't typically think about, which I think may be required in this case, and that would be uh a cooperative launch. Um because if we have a bunch of groups of threads that are working on this decoding problem um like at the same time, we we either need to be able to have an unknown, like an arbitrarily growing number of blocks, like of threads, right? Like each block of thread decodes a known amount. Okay, maybe okay, all right, all right, all right. Let me let me I'm cooking here. So when you launch a CUDA kernel, you have to specify how many blocks you're you're running with. This this microphone's a little bit waterproof, right? I don't know. Okay.

Conor

It's starting to rain now, folks. We might need to kill this. And also too. I just have to say, uh, I don't know how I'm gonna edit this because I'm pretty sure every time I said scatter earlier, what I should have been saying is gather, but I'm not even confident about that because my confidence is shot because I totally messed up the description of an algorithm.

Bryce

So get out well to yourself, man. We're cooking here. I'm not gonna. So I can I can imagine two different types of implementations. So when you launch a CUDA kernel, you need to s like you need to specify statically how many blocks of threads are going to execute. And the best, there's two different ways that we could approach this problem. Way number one is the way that we typically think about CUDA computing, where each block of threads. Well, look at this isogram. This is each other. Yes, we're gonna we're gonna go in there. Let me finish my spiel. Each block of threads decodes a fixed chunk of work. Typically, when we're talking about these problems, we imagine a world where there's a fixed number of blocks of threads, um, and each block d like does a fixed amount of work. And to do that, you need to be able to know how much work there is in total. In this case, we don't know how much work there is in total. However, we could imagine that we could w launch a kernel that that where we assume uh we're trying to get undercover or we're going to the ice cream plate. All right, here.

Conor

We'll do them both, but it's it's too loud now.

Bryce

It's alright. So we could imag one approach I could imagine would be to say we're gonna do a fixed amount of work per block, we're gonna launch a fixed number of thread blocks, and then if that is not sufficient, we launch a second kernel that continues the decoding. Now that might mean that we end up like wildly, we might end up using a lot more compute than we planned because maybe the maybe the decoding takes a lot less work than we planned. Um but the second approach, the second approach is this cooperative approach. In this second approach, we launch a much smaller number of uh blocks of threads, but the amount of work that each block of thread does is variable. Uh that is we don't know ahead of time or how much work they're going to do. They're each gonna do roughly the same amount of work, but it's gonna be what what we sometimes call like a persistent kernel or a cooperative kernel, where it's going to continually, it's gonna work on a chunk of input, it's gonna do that chunk of input, and then it's gonna load the next chunk of input and start doing that next chunk of input. However, to write that type of kernel, you need to uh be able to have all of the uh blocks communicate with each other and synchronize with each other, because in this case, once you're done with your chunk of work, to be able to start in the next chunk of work, you need to know where everybody else wrote their chunk of work, uh, which would require you to do a scan. Uh, but in this case, we would maybe want to do a um uh a cooperative scan. Um and the advantage of doing that is that we can do the cooperative scan at the same time that we're loading the next chunk of input, because while we don't know where the next chunk of output will go, uh we know we can we can statically assign where the next chunk of input will come from, which means that we can be loading it concurrently with figuring out that scan. Okay. I think I may have lost some people there, but uh, we're gonna go get ice cream.

Conor

Yeah, he lost me, but it's hard to focus after the devastating mess up. I'm just really hoping that gather is it's possible that everything I said is true for gather, and then I can go in, and every time I say scatter, I'm just gonna cut in. I'm gonna cut out scatter and cut in gather.

Bryce

I I believe that the thing that you described is an algorithm within thrust and cup. Also, you gotta buy, don't move, but don't move.

Conor

You gonna get rid of this thing off my head?

Bryce

No, I'm just gonna take pictures.

Conor

What is it?

Bryce

It was a little flaw, I think. Don't worry, it's okay. It wasn't anything scary.

Conor

Alright, folks, hopefully that was audible because we're standing next to a bar with like a dance party, and uh we're ending this. Goodbye from Copenhagen.

Bryce

We're not done yet.

Conor

We're not done. Yeah, yeah, we're turning it. No, we're getting we're we're getting ice cream, then we're gonna continue. We're getting ice cream, we're not continuing. And uh we will pick this up again. That's actually a good question. If we're recording and we might have to record again before Wednesday. We're gonna pick this up again in five minutes. We will not. We will not. This is going in my backpack, it's raining. And good night from Copenhagen. Maybe we'll record next time. Uh we're not gonna have the energy after Monday and Tuesday. This has become very ADSBI. No, I'll have energy for this. For this? Yeah. All right. You'll hear from us. Today is Saturday.

Bryce

And just to be clear, this entire problem we've been talking about is essentially GPU decompression.

Conor

Yes. Yes. Yes. And it is, am I not correct, it is a very interesting algorithm.

Bryce

It is a very interesting algorithm. Oh, we didn't need okay, we we need to mention the final thing, which is why we were talking about replicate in the first place, which is because of array broadcasting. So imagine that you've got like a vector um and a matrix, and you want to uh like apply some operation and like you want to uh apply some element-wise operation like uh uh uh adding or uh you know you're explaining this incorrectly.

Conor

It's not when you want to do an element-wise operation. Uh it's when you want to do some kind of binary operation on a matrix and a vector. So the example that we were talking about earlier, uh Ashwin, who we're gonna be meeting up with tomorrow in Norway, posted uh internally something about a row-wise softmax, which is a computation and I don't know, machine learning stuff.

Bryce

But essentially you're performing a bunch of binary the fundamental operation to flash attention, which is fundamental to GPU inference, which pays all of our bills.

Conor

It's just so sad because they give it a name like softmax, and you're just like like all these things, softmax, relue. It's just a combination of a couple of binary operations. Like it's nothing fancy.

Bryce

We'll do that.

Conor

The three binary, it's three two binary operations and a unary operation. There's max, there's summation, I guess there's division, and then there's also exponentiation. But it's like you do those things in order, you're done. It's like it it's it's so boring. And like they're oh, softmax. You hear softmax and you're like, ooh, like how complicated that look. I'm just, I'm just, it's anyway, I just like these names, like, and also Relu, like I can't remember what Relu is, and there's a couple different versions of it, but like I'm pretty sure it's just like one over a number or something like that, or it's like capped after. Anyways, I just like as someone who studied Axe Royal Mathematics and I suffered through actually like difficult stuff, they give all these fancy names and it's just like it's just basic math. It's just basic math. Anyways, row-wise uh row-wise softmax involves basically needing to do an operation where you calculate the maximum of each row, and then you divide each of the values in the matrix by the maximum of the corresponding row. Bryce is dancing now. And so that means is that if I've got a you know a matrix, I compute uh however many rows uh maximums, and then I want to divide that maximum into uh each of the values in each of those rows. So it's not an element-wise operation, it's an element-wise operation on a row-wise basis, which means like if I've got a three by three matrix and three values for the rows, I want to divide the first row by the first max, the second row. Anyways, I think people get it. The point being though is you can use replicate to turn that vector into a matrix. Uh not a matrix, but like, or I guess basically a matrix that.

Bryce

And this is for anybody who's familiar with like numpy uh style broadcasting, hopefully we've not lost the We might have blown out the gain a little.

Conor

That might have been my bad.

Bryce

But but uh for anybody who's familiar with numpy style broadcasting, where when you're working with two arrays of different Aridi, the smaller array can be stretched.

Conor

It's Aridi, not Artie sounds like Ariana Grande's cousin.

Bryce

Alright, we're cooking here. Where it can be stretched to the uh to the rank of the larger um array. And you can use replicate to do that stretching. Anyways, we're gonna go get ice cream now, but that's why we were interested in talking about replicates.

Conor

It's actually not why we were interested in. I only realized when I was trying to implement the row-wise uh softmax that, like, oh, replicate's a very convenient thing if you don't have broadcasting in your library language. It just making the music loud. I had implemented replicate a month before because I was trying to solve some problem that clearly just needed a replicate. And then when it came to implementing, it was like literally some Perl weekly challenge or leak code problem that it was just like, oh, like clearly what I'm missing here is replicate, which is it's just an APL primitive, and I I hadn't added it. And uh this library that I've been working on, uh it's it's heavily inspired by all the array operations.

Bryce

Uh we may need to talk to the uh like the N we may need to talk to some of like the like NVJPEG and like the NVComp people, the compression library people. Well, yeah, I mean I think the people who make the compression library know about compression, yeah.

Conor

Do we need to talk to them? No. Will we talk to them?

Bryce

Maybe I do believe that a single pass algorithm exists here. I mean, I always believe a single pass algorithm exists here, but alright, Connor's fixing his sweater, his his tan NVIDIA sweater. It's gold. There is no world in which this is gold. It's gold. There is no world in which this is gold. Alright, we're gonna go get ice cream. And I am dancing now. Very poorly. But I did learn some dance moves when I was in Ethiopia. They taught me how to do the shoulder thing. Which I kind of remember how to do. Yeah.

Conor

There we go, folks.

Bryce

She fixed the sweater.

Conor

My only sweater and my only shorts that I brought to Norway at the middle. That's true.

Bryce

I do have a He brought shorts to Norway. He's gonna be cold. Have I been cold this whole time? But we're right now we're in Denmark, not Norway. It's gonna be colder in Norway. I have been in Norway in September. Trust me, it's gonna be cold.

Conor

I think we're gonna be just fine. Was I slightly underdressed for a couple of the restaurants we went to? It's not it's not a it's not a maybe, it's a most definitely yes.

Bryce

I took this man to the nicest restaurant in Aarhus, or however you pronounce it, wearing uh his Lululemon shorts and uh and a running t-shirt. Yeah.

Conor

Was it a running t-shirt? Oh no, I kept the hoodie on. I kept the I kept the hoodie on. Yeah, yeah, yeah. Listen, you know what? I would have brought nicer clothes if I had thought about the fact that Bryce wanted to eat at fancy places.

Bryce

I need you to say that again with a straight face.

Conor

Is this what so that we can put this on the podcast? I don't know what I'm doing with this, but uh so Bryce asked. Uh was it Chat GT or Gemini?

Bryce

Chat GT.

Conor

Uh for some humorous names for scatter and gather because of my misunderstanding earlier. Uh one of the initial unhumorous was scatter 2 and gather from, which I really like. I was already thinking about scatter 2, but anyways, the top humorous one was yeet and yoink. And then Bryce thought that was hilarious, and that's what we should call it. But my response was, well, that doesn't make any sense because yeet means to like delete, to get rid of, to toss it. You know, when they say yeet it, it means to get rid of it. And that's not what scatter's doing. So semantically, it has nothing to do with the actual semantics.

Bryce

So I believe you said specifically, yeet semantically has nothing to do with the actual operation.

Conor

Yeah, yeah, something like that.

Bryce

Oh god, I'm dying. I'm dying. I'm gonna read some of the other some of the other ones.

Conor

Be sure to check these show notes either in your podcast app or at adspthepodcast.com for links to anything we mentioned in today's episode, as well as a link to a GitHub discussion where you can leave thoughts, comments, and questions. Thanks for listening. We hope you enjoyed and have a great day.

Bryce

Low quality, high quality. That is the tagline of our podcast.

Conor

That's not the tagline. Our tagline is chaos with sprinkles of information.