A 1000-processor chip powered by an AA battery (2016)
ucdavis.edu
ucdavis.edu
Epiphany did have (slower) access to external memory too, but the challenge of these architectures is finding problems that requires too much branching to be able to take proper advantage of GPUs, yet so parallel that relatively low single-core performance is outweighed by throwing more cores at it and/or where the low power usage of the Epiphany would be worth it, and it seems they just didn't find enough of a market for it.
I have two of the kickstarter boards sitting around. Wish they'd gotten larger scale versions out.
Nothing really prevents an architecture where part of the address space is core-local (with low latency) and not directly accessible from other cores, and the rest points to a (much higher latency) shared memory pool. I have a feeling this would be much nicer to program than a Cell or the average GPU.
Many GPU's have this kind of local scratchpad memory. Nvidia GPU's even allows the programmer to partition the local SRAM between the scratchpad memory (called shared memory in CUDA docs) and L1 cache.
Minimizing ownership transfer cost across hardware elements seems like a fundamental concept to me that future architectures will need to specifically optimize in order to maximize the benefits that can be reaped from "task specific" hardware.
The more cores we add the more we have to gain by allowing them to be different from each other I think ...
This sounds like transputers. IIRC they had direct memory linkage too.
TAOS was definitely ahead of its time, but rarely gets mentioned unfortunately.
This is one: https://sites.google.com/site/dicknewsite/home/computing/pri...
I wonder whether OCCAM would be usable here, or if not, then something based on the lessons learnt from it.
(https://en.wikipedia.org/wiki/Communicating_sequential_proce...)
No idea how it has progressed or if sample units became accessible - which would be nice.
> The chip is designed as a massively parallel processor array, with 992 cores arranged as a grid 32 by 31. Eight additional cores are found along with 12 memory modules of 64 KB SRAM each (for a total of 768 KB). Communication between cores is done via a dual-layer source-synchronous circuit-switched network and a very-small-area packet router (see wormhole routing). The circuit-switched network supports communication between adjacent and distant processors, as resources allow, with each link supporting a maximum rate of 28.5 Gbps. Maximum throughput is 45.5 Gbps per router. Both network types contribute to an array bisection bandwidth of 4.2 Tbps.
24 cores of failure out of 1000 would be an error rate around 2%, which would help in yields.
I had assumed there were 1000 instruction memory SRAMs in there but of course that'd be impossible considering the leakage numbers and there's actually just 12 x 64KB SRAMs shared by all the cores:
Per core
640 bytes (128x40-bit) local instruction memory
512 bytes (256x16-bit) local data memory
768 KB SRAM on-die
12 shared SRAM memory modules, 64 KB each
Those local per-core memories are tiny and presumably implemented with registers. But can you really do much with just 128 instructions? The following makes it sound like they might be instruction caches:> Instructions may come from the local instruction memory or they may be fetched from one of the independent memory module
But then goes onto say:
> the KiloCore's processors do not contain traditional caches.
Hopefully their paper has more info.
The survey that the "World’s First 1,000-Processor Chip" claim is based on (http://vcl.ece.ucdavis.edu/misc/many-core.html) doesn't seem to include any network processors, some of which are technically programmable. But even if it had, they wouldn't have found any of the information required for the table and definitely not a datasheet!
Or is the 16 bits CPU too limited for training operations?
But CUDA and ML have steamrollered everything!
Actually, at the time, what I did was a 3-way comparison between GPU, XeoPhi and classic CPU (was it 8 cores?) running the same OpenCL code. Anyways, the "many-core" specimen, XeonPhi, benched at the geometric mean between classic CPU and GPU - so closer to classic CPU! (we were using beta hardware, our intel specialist - nice, helpful guy actually - went mute after I got those results :D ) On top of that, reimplementing the same algo in CUDA gave ~10% advantage over OpenCL on NVidia hardware. I wonder how it would fare on a FPGA of the same price/TDP?
And more relevant for 2021: - I wonder if we would get the same kind of results using Vulkan?
- Speaking of which, does anyone know of any good books / tutorials / runnable examples of Vulkan in a compute-oriented application?
Cheers!
Not only you can decide on CPU vs GPU, you have more control over the kind of parallelism and intermediate results.
Is there any language where you can just go:
parallel-for:
do-my-stuff
and have the programming language Do The Right Thing with no further hassle? The right thing usually being blocking execution until every parallel do-my-stuff finishes execution, in my experience.In most programming languages I've seen this is a 20+ line boiler plate full of caveats and risks.
1: tmpA := A(input)
2: tmpB := B(tmpA)
3: tmpC := C(tmpB)
4: result := D(tmpC)
it's impossible to calculate a line before the previous line has finished. You cannot do this calculation concurrently. On the other hand if it was like this: 1: resA := A(input)
2: resB := B(input)
3: resC := C(input)
4: resD := D(input)
You can calculate all 4 concurrently and achieve a great speed-up if you have resources to do it. Except if B, C, or D somehow access the result of a previous line. For mathematical notation this is simple, but for general programming languages there are plenty of ways to do this.We know that an optimal optimization, i.e. finding the fastest way a program can be executed is known uncomputable. Not difficult, not brute forceable, but impossible to compute with traditional computers. The best we can do is identify _some_ concurrency optimizations, but so far it has not been a fruitful adventure for general programming languages. So what we can do is use patterns and constructs that rely on dividing the problems into concurrent pieces and use proper guarding on shared state. This is however notoriously easy to get wrong.
When programming the level of friction is from annoying to high unbearable, depending on the programming language. It should be trivial to implement these solutions, as trivial as the sequential solutions.
It's not. Until this is the case in mainstream programming languages, I doubt we'll have CPUs with 1000 cores in our smartphones or laptops. I mean, we'll have them but they'll be useless because 99% of software today is practically single-threaded. The reason multiple cores are good for laptops, for example, is because we're multi-tasking between multiple applications, not because those cores are frequently used by day-to-day apps. I'm talking about random small apps, not compute intensive ones that have to use multi-threading intensively (rendering, compiling, what have you, professional level software).
I used to joke developers should get workstations with SPARC Niagaras or Xeon Phis for that reason: core count is going up and a CPU with a dozen small cores is much cheaper to build than one with 4 beefy ones. Now some of the low-end chips Intel is pushing have 4 SMT2 cores. HEDT is on the 16 SMT2 core range and, if our software continues to be single threaded, there will be a lot of silicon being used for nothing more than spreading heat.
The incentive to do it when most computers have two SMT2 cores is small, but as the average device starts getting 4 or 8 SMT2 cores, or 8 asymmetric cores, the incentives to make things run faster in parallel get better and better.
Consider this: We mostly parallelize compilation today at the unit of a single compilation unit.
We do this for two reasons: It's simple, for languages where separate compilation is possible, and it's good enough when you have a small-ish (smaller than the number of compilation units) number of fast cores.
But a lot of compilation can be reduced into far more fine-grained parallel steps.
E.g. parsing can be turned into a chain of array operations along the line of finding possible start/ends of tokens, finding possible starts and ends of quoted segments, finding possible starting points for matches against various rules based on the possible token starts etc. You can further build syntax trees as arrays of vectors. If you have many enough cores it may pay off to subdivide the work to extreme levels like that.
But it's hard to reason about. We're not used to it, and most of us are no good at conceptualising it. Some are.
For a real-life example of a compiler that actually runs on GPU's and aims for massive parallelisation (it compiles a subset of Dyalog APL), check out "co-dfn" by Aaron Hsu. It's very terse and complex to read, and frankly I've only spent time looking at it very superficially, but it's an amazing piece of work. Now, if it was easier for mere mortals to actually write code like that, and we had a more compelling reason (many, but slower cores), we'd probably see a lot more parallelisation. In the short run it'd be extremely painful, but in the long run it might well benefit us.
I'm really curious about the proof of this, since there's only a finite number of ways you can re-arrange instructions in a program. Likewise there's only a finite number of ways you could shard them across threads.
For this to be true you'd clearly need to phrase the problem in a way that allows you to have an infinite number of possibilities, then prove you can't arrive at the optimal solution through some means that doesn't involve checking an infinite number of them. (Edit: Or prove that it is impossible to decide on which one is the fastest at the time the optimizer runs. When does the optimizer run?).
Was the assumption that you'd also be (infinitely) unrolling loops or maybe 'rewriting' the program into something equivalent but faster? What does "finding the fastest way a program can be executed" mean? Just re-ordering instructions and distributing them across threads, or something more?
But even so, minimizing the program size is uncomputable as well. Given a Turing Machine M, create a program P(i) that simulates M for i steps and return true if the machine reaches an accepting state. If you provide an optimizing program OPT, then OPT(P) would be "return false;" if and only M does not halt. I.e. the optimizer would solve the halting problem which we know is not possible.
And not only finding parallelizable instructions either, I assume.
Thanks for the clarification, because such a proof would have surprised me a lot.
The assumptions and optimizations the optimizer is allowed to use and what kinds of programs we're talking about, and what even is considered an "optimal program" is also still unclear (Edit: I'm assuming lowest number of instructions executed serially?).
Please do refer me to a paper. Please do not link me vaguely related Wikipedia articles.
A trivial example of such a program is a function that sorts it input and returns the sorted result, as no sort is optimal for all inputs.
You're trying to prove that no optimal solution can exist, not that it's impossible to find one. Which is fine, because it's a stronger claim.
But it heavily depends on your definition of optimal. If you define optimal as "lowest average number of instruction across all possible inputs", then there exist optimal sorting algorithms.
If you define optimal as: "A solution is optimal only if for every possible input there exists no solution that requires a lower number of instructions.", then yes, there can be no optimal sorting algorithm.
I would argue that in practice only the first definition is useful.
Further, you're assuming that "optimizations" that replace an existing algorithm with an equivalent one are allowed. Which brings me back to one of my original questions: What optimizations was hvidgaard allowing to be me made when he claimed an optimal solution is incomputable.
I did not prove no optimal solution can exist. I gave an example of a subset of programs which would resist optimal optimisation. For a lot of programs an optimal version will exist. E.g. "return false" is obviously trivial to generate an optimal version of.
[I'll also note hvidgaard provided a far simpler proof this problem is equivalent to the halting problem that invokes the halting problem directly as part of the solution by forcing the optimiser itself to solve the halting problem to determine the valid resolution, but my example shows that the optimiser can not solve the general case even if the program being optimised is guaranteed to halt]
> But it heavily depends on your definition of optimal. If you define optimal as "lowest average number of instruction across all possible inputs", then there exist optimal sorting algorithms.
The commenter you replied to gave a definition: "finding the fastest way a program can be executed".
However, I believe your definition won't help (even if I ignore the "lowest average number of instructions" - nobody cares about the number of instructions executed, but at about wall-clock time or space complexity, or possibly the size of the generated code), because the input to the program and the input to the sorting routine here are not necessarily the same. You can extend the sort problem to a more complex program that can incorporate the element that reads back in its own binary and perturbs the inputs fed to the sort component. Indeed to "borrow" from hvidgaard, you have no guarantee that the program in totality will halt at all, but even if you disregard not halting, as long as the program can read its own code, the problem remains.
> I would argue that in practice only the first definition is useful.
In practice that definition is very often totally useless, because we very often have knowledge about the expected input data for which the way we express an algorithm and the specific choice of algorithm is the only way most programming languages allow us to express the necessary expectations.
Effectively most optimisers aren't advanced enough anyway that this is a consideration, because we're hitting space-time complexity tradeoffs for the cost of running the optimisers already when trying to do unambiguously beneficial optimisations.
> What optimizations was hvidgaard allowing to be me made when he claimed an optimal solution is incomputable.
Given the claim was stated without any constraints, it is natural to assume the claim was general. That is, that a valid output of the optimiser is a program that given the same input produces the same output. The point of the claim was to point out the sheer complexity of aiming for auto-parallelisation beyond smaller transformations.
In any case your response is not very readable - some sentences are cut off halfway through or mangled. Maybe you could fix it up and I'll give it a second reading afterwards.
> Given the claim was stated without any constraints
The original commenter was suggesting there is some mathematical proof for the statement: "finding the fastest way a program can be executed is known uncomputable."
Given the context it would seem likely that the optimizations they were talking about are specifically about finding which instructions can be executed in parallel.
Consider their next sentence: "The best we can do is identify _some_ concurrency optimizations."
We won't be getting anywhere like this either way. This started with me asking for clarification and a reference, and you keep trying to present me with something that is neither. I can appreciate you wanting to add some insights, but only hvidgaard itself would know what they meant to say. Edit: They posted a response 3 hours ago which I did not see until now. That settles that.
> The original commenter was suggesting there is some mathematical proof for the statement: "We know that an optimal optimization, i.e. finding the fastest way a program can be executed is known uncomputable."
And they have provided a proof for that, which was short and concise.
> Given the context it would seem likely that the optimizations they were talking about are specifically about finding which instructions can be executed in parallel.
I'd rather - given that it matches their followup proof - assume that the statement is intended to be taken as written rather than assuming additional constraints that they did not state anywhere.
> Consider their next sentence: "The best we can do is identify _some_ concurrency optimizations."
Yes, because we can not create an optimal program. This is not attaching any limitations to the hypothetical optimiser from the previous statement.
> This started with me asking for clarification and a reference, and you keep trying to present me with something that is neither.
I stated a proof by giving an example that can not be optimally optimised according to the criteria hvidgaard stated. I'm sorry if it wasn't clear enough for you. It was more complicated than hvidgaards very elegant proof, granted, though it also goes further, in showing that even for programs that do terminate there exists a subset which there is no solution that meets hvidgaards criteria.
> I can appreciate you wanting to add some insights, but only hvidgaard itself would know what they meant to say.
hvidgaard has also stated a proof, by showing that you can devise a program that requires the optimiser to solve the halting problem to return the optimal program, that just like mine made no assumptions about the set of allowed optimisations.
I don't know what you're still arguing here given that whatever you think of my sort example, hvidgaards own proof for their statement is simple an unambiguous. This is also not at all a new discovery either, but a well settled topic of computer science.
And yes, I'm well aware that the issue is settled for general optimizations.
At risk of repeating myself for a third time: I was curious whether, when only using optimizations that involve re-ordering instructions/finding instructions that can be executed in parallel, there is proof that there exists no optimal solution.
If that's still not clear, I'll quote myself from my original comment:
> Was the assumption that you'd also be (infinitely) unrolling loops or maybe 'rewriting' the program into something equivalent but faster? What does "finding the fastest way a program can be executed" mean? Just re-ordering instructions and distributing them across threads, or something more?
Quite disappointingly that wasn't what they were speaking of, because I would've been quite surprised by such a proof for reasons outlined in my original comment.
I would say that a reasonable definition of an optimal solution to a problem, excluding concurrency for now, is the same as minimizing the number of steps needed to complete the algorithm. Take my proof from the other comment, it states that if you have a perfect optimizer, you have solved the halting problem.
Adding concurrency to the optimization, i.e. the OPT function can return a program that executes concurrently in which ever way possible on some given hardware. It will still return "return false;" if and only if the original algorithm never terminates.
I was curious whether, when only using optimizations that involve re-ordering instructions/finding instructions that can be executed in parallel, there is proof that the optimal solution is incomputable.
In context it appeared that may be what you meant, but you clarified that you didn't.
Under these constraints one might say it appears somewhat obvious that the optimal solution, i.e. a solution that executes the most instructions in parallel is computable. It's done by building the dependency graph of instructions, then ordering/distributing the instructions across execution units. For anything more than that you will run into the halting problem, but anything more would involve changing/removing/adding instructions or otherwise making changes to the program logic, which is not allowed.
One might complain that the optimizer now in some cases can't know whether two instructions depend on one another without running the program (writing, then reading unknown memory addresses for example), but any attempt to optimize cases where we can't statically infer or where the memory addresses are only sometimes the same would result in changing or adding instructions anyways (inserting a branch)[1], so as far as we are concerned these always depend on one another. We're not allowed to add/change instructions after all.
A valid criticism of this class of optimizers is that they fail spectacularly for self-modifying programs or programs that read their own instructions. A contrived example would be a program that reads some parts of its own code. We can't really do anything about this since we're not allowed to rewrite these instructions to make them still work correctly - if we even can. It's more useful to just exclude that class of program.
Anyways, a short proof that it is possible might look like this: There is only a finite number of permutations of the original program that satisfy these constraints (re-ordered/parallelized instructions), generating them all is trivial and choosing the one with the most instructions executed in parallel is as well.
Which brings me back to:
> The best we can do is identify _some_ concurrency optimizations
So my point was really, that assuming "concurrency optimizations" only includes re-ordering instructions and distributing them across execution units using whatever technology, intuitively it would seem that we can identify all of them.
But that's not what you meant. You included things such as completely replacing the algorithms with equivalent ones in "concurrency optimizations".
[1] There is an assumption here that one could disprove by example. The assumption is this: You can statically infer two values are the same when they are always the same. If this doesn't hold and the instructions using them could be run in parallel but aren't, I'm just wrong.
Though none of these are quite as convenient as I think they could be. I suspect there is a path for Rust to add parallel iteration syntax in the future [5]. There are quite a few steps needed to get there, but the result could look something like this:
let mut listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Listening on {}", listener.local_addr()?);
par for stream? in listener.incoming() {
println!("Accepting from: {}", stream.peer_addr()?);
io::copy(&stream, &stream)?;
}
[1]: https://devblogs.microsoft.com/cppblog/using-c17-parallel-al...[2]: https://docs.rs/rayon/1.5.0/rayon/
[3]: https://dotnettutorials.net/lesson/parallel-for-method-cshar...
[4]: https://devblogs.microsoft.com/powershell/powershell-foreach...
[5]: https://blog.yoshuawuyts.com/parallel-stream/#future-directi...
If you do not include the standard library _AND_ if you use these features, then you'd need to provide your own parallel run-time or your application won't compile.
There are many valid implementations of these run-times tuned for different applications, these run-times can be as simple or complex as you'd like, in some cases some run-times only work on particular bare metal targets, and in most cases you just pull a library with the implementation that you'd want to use.
Since a valid implementation of the run-time is to just run all tasks sequentially, you can provide a tiny (often "zero-size") run-time for those environments on which code-size trumps everything.
You can also provide the standard parallel run-time, but this might require operating-system like functionality, like thread, mutex, etc. support. If your embedded target does not provide those, you'd need to provide implementations of these yourself.
The important thing is that you don't need to change any code that uses the run-time, e.g., all the app code can still use fork-join parallelism, but you control how that's executed by swapping the modular runtime component.
I'd imagine something similar could be introduced for threadpools / runtimes.
It’s not like separate parts of a program can each optimize for the number of threads and their memory for their goals. Optimization/tuning has to happen globally because the resources used (threads, memory) are shared between those parts.
I think most solutions have some global state that magically decides how many threads to run in parallel (more for I/O bound code), how many to assign to each part (which don’t even have to be perfectly separate. Part P might parallellisme a loop where each iteration calls into part Q, which also runs a parallel loop. If each of these decides to run one thread per CPU for N threads in total, the result still c/would be that N² threads run in parallel, likely with suboptimal results)
use rayon::prelude::*;
fn sum_of_squares(input: &[i32]) -> i32 {
input.par_iter() // <-- just change that!
.map(|&i| i * i)
.sum()
}Anyway, there's a language called ParaSail[0] that's doing stuff in that direction. A little bit back the guy behind it got hired by AdaCore (Ada compiler folks) and they seem to have adopted it and continuing with it. I haven't heard too much about it though.
Think of Occam programs as big Factorio factories.
https://en.wikipedia.org/wiki/Occam_(programming_language)
Alot the ideas that came out of Occam and CSP have been brought into various coroutine libraries though. Kotlin Coroutines, Goroutines, etc. So its definitely something that is explored.
Nothing prevents a map function to run in parallel.
There are other things that can happen in parallel, however - the whole instruction flow is reordered in the CPU before it is executed and if you have enough execution ports and enough in-flight instructions to keep them fed, you can accomplish a lot in a single clock cycle before your source code even needs to acknowledge that instructions don't happen exactly in the sequence they are written.
For instance 3D-API shading languages do just that under the hood, just without the "parallel-for". Instead you tell a 3D-API what code should run per-vertex or per-pixel in (usually) a traditional sequential C-style language, and the GPU driver and hardware care about the parallelization and scheduling.
You need a C/C++/fortran compiler that supports openmp, which is most of the mainstream ones (I believe MSVC still only support an older standard, still more than enough for parallel for). Compilers that do not support openmp are supposed to ignore the pragma and the code will still run fine (although serially) and be fully compatible.
Other than that and possibly passing the correct command line flag, openmp is fairly low friction. A parallel for can provide significant speedup for embassingly parallel problems and these days it even support custom random access iterators in C++.
Of course it is more involved to express less embarrassingly parallelizable problems, but it is still doable.
Multiple independent programs sending messages to each other fits well into multiple independent cores sending messages to each other.
Serial:
list.map(_ + 42)
Parallel: list.par.map(_ + 42)
In C# (https://docs.microsoft.com/en-us/dotnet/standard/parallel-pr...)Serial:
for (int i = 0; i < matARows; i++)
Parallel: Parallel.For(0, matARows, i =>
(Function takes a lambda that gets called for each value)Swift has DispatchQueue.concurrentPerform (https://developer.apple.com/documentation/dispatch/dispatchq...). That is a bit less clear and less flexible (won’t iterate over an array’s values, only over its indexes) but also fairly easy to use.
There probably are many more languages with similar constructs.
The problem is that not all problems are easy to parallelism at the algorithmic level, and for some it may be impossible.
Alongside massively multi core chips I would also love to see more efforts to push the envelope on single threaded performance.
Arrays.asList("hello", "world").parallelStream()
.forEach(s -> System.out.println(s));
If you want to calculate stuff, you can List<String> suffixed = Arrays.asList("hello", "world").parallelStream()
.map(s -> s + "_suffix")
.collect(Collectors.toList())As to languages built around this, there are quite a few, e.g. Intel have Data Parallel C++
But not every problem (i.e. the Algorithm for the problem) can be divided easily without any coordination (i.e. communication) between the chunks. This overhead can quickly become significant.
There is a reason why desktop software (particularly the computationally intensive) is having a hard time exploiting multi-core benefits (besides the programming language and habits in general).
Of course when you start doing things in parallel there are automatically a number of caveats and risks.
It gets difficult once you have side effects and/or synchronization (join) of the results are required.
From the docs:
The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop.
Yes, but functional languages solve this problem.
... ran a special FORTH if I am not mistaken ...
I cannot remember where I found a very nice presentation the gent gave about it ... :/ )
The most astonishing part was that he rolled his own VLSI CAD system: https://mschuldt.github.io/www.colorforth.com/vlsi.html
Thank you.-
Right on the dot ...
(I found it fascinating, when I first saw it :)
Nyuzi – An Experimental Open-Source FPGA GPGPU Processor
But as another poster said, the difficulty with those kinds of architecture is the memory hierarchy: you don't want all the cores to access the DDR directly, as that would be a massive bottleneck. And this becomes a larger issue as you increase the number of cores.
Readily available in The Cloud(TM) and everything.
Either way still not close to 1000 independent cores.
if(blockIdx.x == 0) doThing1(); else if(blockIdx.x == 1) doThing2(); ...
etc., and the doThings could run at the same time, even on the same SM. Not super practical but it's allowed.