The Stack Monoid
raphlinus.github.io
raphlinus.github.io
Inverse semigroups are used in the geometry of interaction to give a parallelizable interpretation of lambda calculus. I don't have a great introductory reference. Here's some people who have implemented a system based on the theory: https://www.cambridge.org/core/journals/mathematical-structu...
Sorry for the citation dump, I'm short on time and I thought they would be interesting to those who found this submission interesting.
References
I'll have to check out the Kmett talk - from the description it sounds very interesting.
For example in this problem https://codeforces.com/contest/1285/problem/E, one solution is to parse and count the number of top-level parentheses after small modifications. For example the original string might be "(()())()". Then "(()())" is 1, "(())()" is 2, "()()()" is 3.
This is solved with the following monoid to make incremental reparsing fast:
neutral = Node(0, 0, 0)
def combine(x, y):
# Each node tracks numClose, numCount, numOpen:
# e.g., )))) (...)(...)(...) ((((((
if x.open == y.close == 0:
ret = x.close, x.count + y.count, y.open
elif x.open == y.close:
ret = x.close, x.count + 1 + y.count, y.open
elif x.open > y.close:
ret = x.close, x.count, x.open - y.close + y.open
elif x.open < y.close:
ret = x.close + y.close - x.open, y.count, y.open
return Node(*ret)
Reference solution: https://codeforces.com/contest/1285/submission/92169958Monoid cached trees are extremely popular in competitive programming, but never by that name. It's always referred to as a "segment tree" without using any mathematical terminology more advanced than "associativity".
Due to its popularity, all kinds of crazy monoid states have already been explored (though you need to squint a bit to see them due to the terminology mismatch): https://codeforces.com/blog/entry/15890. I've never seen stack used though. You almost always want to compress the state as much as possible and the problems I've seen only needed stack depth (aka the bicyclic semigroup), not contents. Requiring O(N) to merge also highly limits its use cases.
Anyway, not sure how relevant this is to you since the use of monoids in data structures and for parallelism is slightly different (binary merges all the way down versus stopping at some chunk size).
I was interested in this problem a while back and the papers I remembered being useful were:
"Automatic Inversion Generates Divide-and-Conquer Parallel Programs": https://core.ac.uk/download/pdf/192663016.pdf
which is based on "The Third Homomorphism Theorem": http://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/th...
The motivating example is whether you can generate a mergesort (where the monoid is combine two sorted lists) given an implementation of an insertion sort (where the sequential program is inserting one element by one into a sorted list).
I remember being disappointed by the answer after reading these papers but in the citation graph there are a bunch of relatively recent program synthesis papers. Maybe there's something better now.
I think examples like generating mergesort from insertion sort are maybe trying to be too clever. I mostly just want something that lets you slice up the problem into chunks, process each chunk in parallel, then combine them back in ways that don't kill performance on modern GPU hardware.
> I am even more convinced than before that efficient parsing is possible on GPU.
IMO, the place to start looking for GPU-friendly parsing algorithms is Valiant's algorithm, which is asymptotically the fastest known general CFG parsing algorithm, and is implemented in terms of Boolean matrix multiplication.
The intuition is that bottom-up parsing is basically a transitive closure computation, which is basically the same as solving systems of linear equations over a Boolean ring, which can be sped up using Strassen's algorithm.
A bit of Googling suggests that people have looked at optimising Boolean matrix multiplication on GPUs, but I have no idea what the state of the art here is.
Bonus clip: https://www.youtube.com/watch?v=j4YNPhllDXU
I think there are probably a number of on-ramps. One easy way to get started is shadertoy, which if nothing else should give familiarity with GLSL syntax and intuition for performance in the simplest case: each "thread" computes something (a pixel value) independently of the others. You'll quickly run into limitations though, as it can't really do any of the more advanced compute stuff. I think as WebGPU becomes real, an analogous tool that unlocks compute kernels (aka compute shaders) could be very powerful for pedagogy.
I think most people that do compute on GPU use CUDA, as it's the only really practical toolchain for it. That has a large number of high quality open source libraries, which tend to be well-documented and have good research papers behind them. You can start by using these libraries, then digging deeper to see how they're implemented.
As I've been going on about, I believe this space is ripe for major growth in the next decade or so. As a rough guide, if you can make your algorithm fit the GPU compute model nicely, you'll get about 10x the compute per dollar, which is effectively the same as compute per watt. Why leave such performance on the table? The answer is that programming GPUs is just too hard. In certain areas, including machine learning, an investment of research has partially fixed that problem. But in others there is fruit at medium height, ripe for the picking. And in some other areas, you'll need a tall ladder, of which I believe a solid understanding of monoids is but one rung.
Most universities don't really teach parallel algorithm development in any way despite the core count increasing in both CPU and GPU world.
Current languages like GLSL and CUDA are very abstracted in the sense that they don't really tell why some approach is better or worse than the other. To me, if you can create as general cellular automata -like solution, then such logic can be converted and is generally fine performance-wise for SIMD orientated programming.
I made this [1] simple shader because I couldn't find the effect in Open-Source video software...
https://www.infoq.com/presentations/Thinking-Parallel-Progra...
ps: you can see they were using mathematical structures (ring, monoids) in your video anyway
Functions form a monoid as well. Function composition is associative and id is the unit.
A monoid is a concept from math. They have a set of objects (like the integers), and a binary operation (like addition or multiplication over integers). The operation is associative (like addition where the order you compute a set of additions doesn’t matter, you get the same sum). And there’s an identity element (in addition this would be 0).
For CS there are some nice results where the operation can be automatically parallelized. Useful for things like MapReduce if you need to run things at large scale, or in this case for distributing the work over the many shader units of a GPU.
The author defines a monoid early on, in terms of the three properties that someone else also posted.
I understand this definition but am still trying to make the leap to how it applies in the original article.
Edit: fixed some autoincorrects
They're useful because knowing them allows for identifying and mechanically applying transformations to better leverage parallelism and easily implement incremental computations†.
I highly recommend this article and the section on monoid homomorphisms in particular, which covers why you should care about monoids and gives a practical example: https://fsharpforfunandprofit.com/posts/monoids-part2/#monoi...
If you've been programming for a while, I can guarantee you already intuitively understand the concept. Giving it a name allows you to automatically recognize and notice that you've already solved this problem before in another guise.
Alternatively, having named the concept, it's now chunked into your toolkit for use whenever applicable. Structuring your solution so you know it's a monoid allows leveraging all those benefits almost for free.
†With divide and conquer, add memoization and pay attention to the order of subproblems such that you can reuse earlier work, and if you can, we're nearly at dynamic programming. We can specify a bit more structure, to get semirings and collapse a whole heap of disparate seeming machine learning algorithms. But now I have gone too far afield.
https://mathoverflow.net/a/338282
The paper it mentions and links (The Early Development of the Algebraic Theory of Semigroups) is freely available and has a bunch of discussion of terminology.
+ is a monoid. It adds to numbers together. (3+3)+4 = 3+(3+4)
But also concatenation is a monoid
"a" + ("b" + "c") = ("a" + "b") + "c".
Monoids have an empty thing you can add to anything.
E.g. 0 for + and "" for concatenation.
In Haskell, Monoids are abstracted out into a typeclass so that you can write code on any monoid. For example fold up a list of monoid elements into a single element.
The action takes a state from Q, and a symbol from E, and returns a new state. ie f:QxE->Q. In the case when you have some "no op" action the semigroup action is actually a monoid action.
This all corresponds to the DFA moving through states as it "eats" the string.
https://web.archive.org/web/20200325013049/http://dave.fayr....
* First, give a definition
* then some basic examples to clarify the concept
* then some advanced usage to demonstrate the advantages.
I tend to do it myself, maybe because a math concept has a clear definition and math courses do it too. However, it scares away a lot of people, including myself when I'm on the receiving end.
http://cwyman.org/papers/hpg16_oitContinuum.pdf
The GPU / accelerator BVH traversals are perhaps more applicable (e.g., https://www.embree.org/papers/2019-HPG-ShortStack.pdf) but they also have a different "I can restart my computation" property that say JSON parsing wouldn't have (though maybe if you're willing to do a lot of restarts once you parse the inner portions and push them onto a heap...).
Anyway, cool problem!
The problem is slightly different, though, because the major focus for transparency is concurrent writing from potentially many different shaders, while in the stack case each thread can build its data structure without any concurrency or need for atomics; once an aggregate is built, it is "published" (see my prefix sum blog for details on that) and treated as immutable.
These kinds of concurrent approaches do become important for later stages in the JSON parsing process, like building hashmaps for keys in a dictionary. I'm pretty sure the whole problem is tractable and have thoughts how to solve it, but deliberately kept the scope of this blog post limited, as I figured it was challenging enough. But it's very cool to reach people who appreciate the ideas, and if you find anything else that's relevant, I'd love to hear from you.
[1]: https://on-demand.gputechconf.com/gtc/2014/presentations/S43...
I'm not quite sure how it would work on this problem, but it might be interesting to look into.