Conor

O of n where n is the number of elements, and then you're gonna have to store like a min heap, right? Yeah. So it's actually it's actually gonna be like uh O of N log K, I think. Uh, because in it like you can't just store the top K values, you have to store them in such a way that you're able to pop off efficiently. Welcome to ADSP the podcast episode 187, recorded on June 11th, 2024. My name is Connor, and today with my co-host Bright, we continue our chat about implementing the Top K algorithm, aka Nth element in parallel.

Bryce

Did did you did you listen to my uh the my last message that I sent you yet?

Conor

Yes, I did. So I guess we just released episode 185, right? And that was phone tag. Yeah. And then the Friday that we released, which is a couple days ago now, because it's Tuesday, you recorded half of a phone tag. I did listen to that, but I've been so crazy busy for like the last three days or whatever, since uh Friday.

Bryce

I have yet to act more than the last three days, buddy.

Conor

Three or four days. You wouldn't even Yeah, it's it's it's been nuts. And so the to answer your question, yes, I have listened to it, but no, I have not recorded my half to that.

Bryce

Well, you're gonna have to get around to that.

Conor

And I guess the listener now, this is what episode 187, so they're they've already heard both your half that is currently as of this recording, has been recorded, and they will have also heard my half that has not yet been recorded as of this recording. But by the time the listener has heard this, all of the recordings and publishings will have happened.

Bryce

So so where where where else have you been? Because you've been busy, you've been like on the road, right?

Conor

Uh not really. I mean, I've been stopping back in Toronto. So I was at first I was at uh a KraftConf. It's a conference in Budapest, Hungary. That was just a two-day conference, phenomenal conference. Um, really cool because I got to meet I think you've probably met Kevlin Henney a bunch of times because you've gone to ACCU.

Bryce

I have, yeah, he's cool.

Conor

But I've actually never, and he's one of my favorite speakers. I've I've never actually met him in person.

Bryce

Uh you know who we should have on the podcast. You know who I would be hilarious on the podcast.

Conor

Oh man, that's why I haven't Yeah, that guy is such a good smoker. My brain is not working right. Uh I'm on like three hours of sleep after a lot of travel, so uh bear with me, listeners, over the next two episodes here. But yes, we should definitely have him on. He's he said one of my favorite things in a couple of his talks. He actually said it at in one of his talks at KraftConf, which I think it was entitled Um Testing with Guts, G-U-T-S, Good Unit Tests. And he I heard this for the first time, and he he basically said, It really bothers me when people say it's just semantics. And immediately I was like, absolutely. Oh my god, this I've been thinking this like my whole life. Finally, do people do people say that? Yeah, and it's like think about what you're saying. It's it's only I've never heard the meaning of the words. Like, it's like, what are we talking about?

Bryce

I did have a conversation, and I I suspect it was one of my coworkers, and if the coworker is listening, I apologize in advance. But I was having some conversation with somebody where they kept saying semantics, but they meant syntax.

Conor

Oh.

Bryce

Um like it wasn't like an extended thing, but it was like over the quiz like two minutes, and this doesn't mean like they said something to the effect of like, well, it's just semantics, and I'm like, wait, I think you mean it's just syntax.

Conor

Yeah.

Bryce

Like I've never heard people say it's just semantics.

Conor

Oh no, people say it is.

Bryce

It's that's exactly the point. It's like uh I think I think it's a I think that it is a phrase that come that uh comes from non-programmer speak, right? Like it it like I could imagine I I can think outside of the context of programming, like the phrase it's just semantics is like a common phrase, which kind of means like it's just details, right?

Conor

Yeah.

Bryce

And so people might be saying that to mean it's just details, whereas I think I I don't know, I'm a spec, I'm like a programming language spec person, so so so maybe maybe I have a warped perspective on this. But uh I think most programmers will would not would would interpret that differently. But I think it's just a common phrase in like the English language in general.

Conor

Yeah. And I think it's similar to when it's like a different way of saying tomato tomato, which I also take issue with. Yeah. Because nobody says tomato.

Bryce

Like tomato, your program has undefined behavior. Tomato.

Conor

And uh anyways, we'll we'll definitely get Kevin on. I think he'd be a blast to chat with. And then after that, I was at Should I should I email him? What?

Bryce

You want me to email him?

Conor

Yeah, sure. You can email him right now.

Bryce

Like before we forget, yeah. Yeah, well I mean, not not like right now, but I'll start the draft of the email.

Conor

Sounds good. You heard it here first, folks. ADSP, probably before episode 200. We'll have Kevlin on.

Bryce

See, see, but the pro the problem now is that like if he if he uh if he rejects us, you're gonna have to cut all this out.

Conor

No, no, no. He I mean, it's a bad look. It's a bad look because we're I'm I'm I'm a big fan, you know.

Bryce

So uh I I I have a follow-up for one of our past episodes uh where we talked about uh top K algorithms and we talked about um uh radix partitioning, and uh I mentioned that I was chatting with my our coworker Elias, um, and he wrote me on Slack um and he said, maybe one remark on why an LSD Ratex partitioning pass doesn't work. An LSD partitioning pass tells you nothing about the final rank of an item because you're just looking at the least significant part. This partitioning pass is only helpful to resolve ties between items that compare equal on all the more significant digits. At this point, you just don't know yet whether there are ties on the more significant digits because you are yet to look at those. Remember, this is why LSD Radix sort requires stable partitioning passes, while the MSD doesn't, unless you want a stable sort. Uh and then he gave an example that's going to be hard for me to verbalize. Um but uh I think that that's uh uh a pretty good uh uh uh explanation of like what we we talked about this, like why why do we do LSD for the radix sort versus for the uh like versus it being problematic for uh the radix partition. And so oh he writes at the at the bottom of his example um that the MSD radix sort uh gives uh however gives you already some idea about the final ranks of an item. An item won't cross the boundaries of the bucket it has been assigned to. In subsequent passes, it will just refine its position within the boundaries of its bucket.

Conor

So wait, how um so that makes sense, but where does this leave us with respect to the top K question?

Bryce

Well, I I I think we may have talked about it on the phone tag episode, but I I had suggested that it may be um it may be worth it for a uh Radix partitioning algorithm to do a pre pass where you you do a max. And then um the max tells you how many uh uh like w what is the the relevant, the most relevant digit on the on the most significant side. And so you like, you know, if if your maximum integer is you know like a hundred, then there's a lot of digits that you can skip over, right? And so so let so like in this in this it it depends on on your cost function of how much uh uh well no actually it doesn't because it's not just a computational cost thing, right? Um it's not just the cost of doing the extra passes that are that uh you know are maybe not so useful. Uh no, actually I think it is. I guess it is. Um but you're also paying it would I think you would pay an additional cost, and the additional cost is um like if if your maximum integer if you've got like 32-bit integers and the maximum integer in the set that you're partitioning is an integer that's 100, and you do radix partitioning starting with the most significant digit, like the first like more than half passes is going to put everything into the first bucket. And I don't know if we do actual data movement to put things into buckets, or if we just do it with flags. Um, but certainly it's like a lot of wasted computation there, and depending on how expensive those passes are, it might make it might be worth doing that first like max pass.

Conor

So essentially you're just trying to you're trying to take your sort algorithm and basically just like minimize the amount of computation that you would have to do. But then what happens when you get like, you know, your K is going to be in one of the bins?

Bryce

No, I'm I'm talking about specifically for partition.

Conor

Oh, so just for partition.

Bryce

Yeah, right. Because for for for as he was explaining, uh for for a radix sort, like you're sort of going through everything anyways, and um uh uh like it's it's fine for the sort, for sort. It but for partitioning, like you want to um like split things up into reasonably sized buckets.

Conor

But yeah, so but like that's kind of like a non-deterministic algorithm, right? Based on the distribution of values, if you're looking for top K, you're gonna have to continuously do that partition uh until you're able to determine your your k th value, which for some which I guess is what you're saying. So up front, if you're doing some pass to get you to the maximum or some other like metadata on the distribution of your values, you can then make sure that like in the worst case you're not just doing uh a bunch of extra work.

Bryce

And it is worth noting that Thrust's um Radix sort uh takes a bit like an optional bit start and bit end parameter to tell it for you to tell it uh like what uh you know bits do you want to actually sort of.

Conor

Ignore these bits because you know they're never used.

Bryce

Yeah. Yeah.

Conor

It's still it's still kind of somewhat unsatisfying because of the like uh I don't know, non-determinism of it.

Bryce

Yeah, I mean it's probably better than sample sort though.

Conor

Yeah, I mean like it's definitely for for a k value that's not sufficiently small or sufficiently large to do the either reduction or sort, it's definitely gonna be better than doing a full-blown sort.

Bryce

Yeah. You know, what we were talking about last time with um with like the small k case, um it got me thinking a little bit more that um if k is like uh small but not small enough that you would want to pass it by value. Like if k if k is like 10, or okay, let's use a simple example. If k is one, you know, it's just a max reduction. And so like it's pretty pretty clear that that you don't need any uh like like additional overhead or or or anything. There's there's no state at all. Um if k is two, you need some state, right? Your your operator needs some state to remember what the the largest was because it's searching for the second from largest.

Conor

Right. I mean technically uh max reduction also requires state, but it's just the accumulator value. So it's it's it doesn't need actually additional state.

Bryce

Yeah, yeah. And so the the amount of state that you need grows linearly. Um uh and so like if it's two, uh and and because this is an accumulator value, you know, you're cop you're passing this around by value. Um you know, we're talking about state that gets passed passed around through the reduction. Um so it needs to be something that you're comfortable passing around by value on the stack. If it's two, you're probably comfortable with that. If it's like 10, you know, maybe also that's fine. Um if it's a hundred, that's starting to get to be a place where that's too large. Um but I was thinking if the K is 100, is it possible that there's some algorithm where you have side storage of uh uh you know some array of 100 elements and then you have some synchronization protocol um for uh everybody to sort of collectively update that? And is that possibly faster?

Conor

I don't know. It's an idea. I mean it's very I mean it's very side effecty, like the very is un unnecessary. It it is side effecty, but it could I don't know, it could be faster.

Bryce

But it it could be I mean what what's the what's the complexity and number of comparisons of you know a max reduction.

Conor

Yeah. I mean yeah, if you if you know you have a s like uh enough arguments and at a certain point No, no, that was a question.

Bryce

What's the complexity of a max reduction?

Conor

Sorry, what's the complexity of a max reduction? Like a parallel max reduction?

Bryce

Yeah. Or just no, just like a max reduction.

Conor

It's just uh line it's linear, linear in time and oh one in space.

Bryce

Right. And like and like radix sort, which is the special case, radix sort is like what? Uh it's like OW times n, uh, where n is the number of keys and w is the key length, so it's like kind of uh kind of like linear E. Um but like merge sort or any anything for anything that can't be radix sorted. Um what's more merge sort? Merge sort is like uh you know, n log n. Um so I guess it is better, right? Right, right, right, right. Radix sort is worse complexity-wise. Because the best of the okay, so maybe this doesn't this never make sense, right? Because any form of the reduction-based approach is gonna be linear. And if you do something that requires synchronizing this, you know, global array, that's gonna probably be expensive. Um radix sort is actually worse than like the best case sorting algorithms, right? Because it's it's linear too.

Conor

Yeah, the only the only thing that changes is the the space complexity. It goes from O of one to O of K.

Bryce

What do you mean?

Conor

Yeah, so like you're definitely correct that a max a max reduction, whether you are uh accumulating a single value, a pair of values, or a vector or array of k values, it's always linear in time complexity. Is that actually true? I think for k actually, it does change. It's gonna be O of N where n is the number of elements, and then you're gonna have to store like a min heap, right? Yeah. So it's actually it's actually gonna be like uh O of N log K, I think. Uh because an like you can't just store the top K values, you have to store them in such a way that you're able to pop off efficiently.

Bryce

So I get I guess the question here is how fast of an atomic min heap or max heap can you implement?

Conor

I mean they they are don't they already exist? Is it like C has a priority queue, isn't that just like a Yeah, but it needs to be it needs to be atomic. Oh, you you mean for the version where it's it's uh stored like outside of the It's not even it's actually we're not even talking about a reduction, right?

Bryce

At this point. If like if you're if you're still a reduction. You have sides stored. I mean No, it's not it's not it's not really a reduction. It's everybody everybody is just putting stuff into it into the queue.

Conor

Yeah, that's more of like a for each, but technically you could like in s in non-parallel land, or even in parallel land, you technically could do it. You're just like you're suggesting an alternative implementation where instead of storing the state in the accumulator, you're storing it outside and then having some kind of locking and synchronizing. Um but you technically could in C non-parallel land just like accumulate a priority queue or min heap max heap, whatever. And then I think in C 17, I don't fully I remember watching Ben Dean's talk. Uh and he he they added like uh move semantics to the accumulators, so you could actually like do a reduction on a string uh that was doing plus equals to the string, and so basically you know, adding characters or strings to strings, and previously that was like terrible. It's better to do that in a for loop because you're gonna end up copying the string every single time if you try and do it inside of reduction, but then in C17 or 20, I think they added the capability of uh the lambda that you're passing to your accumulator reduce, they added the ability for that to have copy elision. So technically, like you could implement what you're talking about without storing it in like side memory or whatever you want to call it. But yes, the version that you're talking about.

Bryce

I'm not sure I understood that explanation. You want to try that again?

Conor

So it's at some point in C 17 or 20, they basically added the ability for the return type of the lambda that you passed to your reduction algorithm for it to take advantage of copy elision. So like pre-C 17 or 20, I can't remember which one it is. If you are doing a reduction on a string where your initial string is empty and you basically just want to join a bunch of strings, that was like extremely inefficient because every single time you did your plus, you would basically do a copy of your string. But like they added an optimization where you can take advantage of move semantics, a copy elision, where now you actually can't perform a reduction on like a list of strings to build up one string, and you're not gonna end up with a bunch of copies. Um which for certain.

Bryce

So here's here's the problem with that. Um if you're doing this in parallel, like yeah, it's like I was if you're you're what you're saying is like if you did like a serial fold left, like you could you could have your state in the the accumulator type and you would never allocate uh more, right? Because you could just always move this accumulator object thing, you'd never make a copy of it. Yes. Problem is that we're taking the input sequence, we're dividing it up, and we've got a whole bunch of people operating in parallel.

Conor

Yeah, yeah. So in parallel, this doesn't work at all. Yeah.

Bryce

And the the the real problem actually is that uh uh like you could kind of do that in parallel, but any form of like like I was thinking, okay, what if you had some side array? Um, and then like what if you had like you know one side array per like group of threads, but then you have to merge the side arrays, and then that's annoying, and you know, like you can do that like the you know, there's parallel merge algorithms, but it's it's annoying in another cost, and then that means like essentially you you end up building building the the the uh something that looks a lot like a bad sort.

Conor

Yeah, actually I was while you were say right before you said that, I was literally thinking, actually I in the yeah, in the parallel algorithm where you store K, when you are combining the the you know, min heaps or whatever in between the chunks, uh you're gonna end up having to do like a std merge, which is like an algorithm that like I've never used in my life, but I know exists for like combining two sorted lists. Yeah. Yeah. Yeah. My my point was just that like uh when you were saying this isn't really reduction, I agree. It's more of like a for each side effecty thing. Yeah. But like in theory, you could do this kind of thing efficiently in, like I said, non-parallel C land. Yeah, I think I think you could, yeah. I'm I'm sure there's a paper out there. We should just we should do some research. Or we should get the listener to do some research. Go find us the paper that on on top K. On parallel top K. I'm sure it's I'm sure it's like it's a common.

Bryce

There's a bunch of uh uh papers out there, but I don't know that any I don't know that that I've seen any literature on doing like a uh a reduction-based approach for like small k and whether that pays out. Like because like if it's like 10, um, it starts to get interesting because 10 is like if let's say let's assume 32-bit numbers, like you know, 10 ints is pretty chunky to have to to to carry around, right? Because ten tents is, you know, for 40 bytes. Um but like it's not it's not ludicrous to to imagine just passing that by value. So it might pay off. And and what may be interesting here is that the The payoff might be very different for CPUs versus GPUs. Because for CPUs, you know, you're gonna have order of magnitude, hundreds of threads. So each thread is gonna be doing a lot more work. And so you'd have less actual copying around of your accumulator type. And so you could potentially handle a bigger, a bigger K this way. I mean, okay, again, you still have to do the merge. But for um for GPUs, you have a lot more threads. And so you you this only m makes sense for a much smaller k.

Conor

Yeah.

Bryce

What's the complexity of uh of pushing into like a priority?

Conor

It's log n, log n where n is the length of the the q.

Bryce

Right. Or n is the length of the q.

Conor

Yeah. I always it always irritated me back when I was in my interviewing days when you'd have some quadratic complexity question, and then you'd say where they'd say what's the time complexity, and then you'd say, ah, it's quadratic. But technically, like the n the two n's that you're multiplying are not actually the same. It's like n by m. So the complexity is not quadratic, it's n times m. I always thought that was just I mean, technically, n times m is correct, but you know, at a high level, it's quadratic, you know. You got the you got a rectangle of data, it's quadratic. But then they'd always they'd always be like, oh, it's not quadratic, and I'd be like, I mean, technically it's n by m, but like let's move on to the next question, folks.

Bryce

Uh no, you know what? I I think I miss I think I misspoke before. I think that this does maybe pay off. Because merge sort is n log n, which is worse than n. Yes?

Conor

Yes, definitely.

Bryce

I think I said, I think I mistakenly said earlier that it's not. So I think you can get to well, no, okay, alright, no. Because because think about it, if if what you have to do for every element is that you have to push heap. Well, no, actually, maybe not. Okay. So so if you for every element, if you have to push into a k-sized heap, then you're doing a log k operation for n elements, right? Which is better than n log n. And then the only other cost that you have is the cost of doing the merge.

Conor

Yes. Uh and also, too, like technically the time complexity is n log k, but you're not always going to be doing the insertion. Like the time complexity is the worst case, but like on average, you're probably only going to be doing the log k insertion, I don't know, like a percentage of the time. Is it half? Is it 20%? Is it 10? We don't know. It depends on the data. In the worst case, you'll have to do it every time in some like whatever reverse sorted or sorted sequence. But in the average case, you're going to do some check to see is, you know, when you if before you do the insertion, I think I think maybe when you actually push into it. I don't know if that how that actually works. So like it might already take care of it when you try to insert into a min heap and it's gr it's greater than the largest value or whatever, it uh it'll just discard it, and that's like an O1 operation, right? So it's really it's really gonna be better than like N log K.

Bryce

It's gonna be and we might be able to kind of ignore the cost of the merges of having to merge these heaps together. And the reason I say that is because the order of magnitude of like uh uh how many merge like uh the order of magnitude of the elements involved in the merges is much smaller than the order of magnitude of the inputs here.

Conor

So they're kind of MRT.

Bryce

Yeah, we're assuming in this problem that the order of magnitude of K is substantially smaller than N. And we're also assuming in this problem that the order of magnitude of the number of parallel execution agents is substantially smaller than um uh than n. So like even on a GPU, like let's, you know, you might have you know uh thousands or or hundreds of thousands of threads, but I think that you would only need on a GPU to have one of these heaps per um thread block. Uh and GPU's thread blocks are gonna be in the thousands.

Conor

Yeah.

Bryce

Um and you would have you know at least millions, but probably tens of millions or hundreds of millions of input elements. Um and so the merge costs may not you know be that expensive.

Conor

Yeah.

Bryce

Huh.

Conor

All right. Well, we're at the halfway mark of the recording. Do we want to do we want to wrap up this topic? Any final thoughts on the top K, parallel top K?

Bryce

Well, Elias, if you're listening, you should go prototype this.

Conor

Perfect. Delegating work. All right. That's episode 187, if I've got my numbering correct. Be sure to check these show notes either in your podcast app or at adsphepodcast.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.