Overview of Parallel Computing Architectures
Overview of Parallel Computing Architectures
Module 1
Introduction to parallel
programming, Parallel hardware and
parallel software
Topics to be covered
• Classifications of parallel computers,
• SIMD systems,
• MIMD systems,
• Interconnection networks,
• Cache coherence,
• Shared-memory vs. distributed-memory,
• Coordinating the processes/threads,
• Shared-memory,
• Distributed-memory.
[Link] of parallel computers
Types:
• Uniform Memory Access (UMA):
All processors access memory with the same latency and bandwidth.
• Non-Uniform Memory Access (NUMA):
Memory access time varies depending on the memory location relative to the
processor (e.g., local vs. remote memory).
• Characteristics:
[Link] to program (due to shared address space)
[Link] for tasks with frequent data sharing
Examples: multi core desktops, symmetric multiprocessing (SMP) systems
2 . Distributed Memory Systems
Each processor has its own private memory. Processors communicate by passing
messages using a network.
Characteristics:
[Link] explicit communication (e.g., via MPI)
[Link] to very large numbers of processors
[Link] complex programming model
Examples: clusters, massively parallel processors (MPPs)
3. Hybrid Systems
These systems combine features of both shared and distributed memory systems. A
common example is a cluster of multicore nodes.
Characteristics:
[Link] memory within a node
[Link] memory across nodes
[Link] may use both threads (e.g., OpenMP) and message passing (e.g.,
MPI)
Flynn’s Taxonomy (by Michael J. Flynn)
They are designed to operate on entire vectors (arrays) of data with a single
instruction, making them highly effective for data-parallel tasks, especially in
scientific and engineering applications.
1. SIMD Architecture
• Executes one instruction on multiple data elements simultaneously. Ideal for
operations like adding two vectors, multiplying arrays, etc.
2. Vector Registers
Vector processors use special vector registers, each capable of storing an entire vector
(e.g., 64 or 128 elements).These registers allow fast access to vector elements
compared to memory.
3. Pipelined Functional Units
• Vector processors often include deep pipelines that allow high throughput.
• Different pipeline stages can process different elements of the vector at the
same time (like an assembly line).
• Example: While one element is being added, the next is being fetched, and the
previous is being stored.
Can access data with a fixed stride (e.g., every 2nd or 4th
element).Some vector processors support indexed addressing,
allowing more flexible memory patterns.
• GPUs excel at data-parallel tasks, where the same operation is performed on many
data elements simultaneously. They follow a SIMD/SIMT execution model, running
thousands of lightweight threads in parallel.
Ideal for tasks like vector operations, matrix math, and image processing
[Link] (Multiple Instruction, Multiple Data) as the most flexible and
widely used parallel computing architecture.
This means:
• Each processor executes its own instruction stream
• Each processor operates on its own data
• So, different processors can be doing completely different tasks at the
same time, or the same task on different parts of the data.
1. General and Flexible Architecture
• MIMD systems can support task parallelism and data parallelism.
• Suitable for a wide range of applications, including irregular and complex
computations.
2. Implicit Communication
• Data is shared simply by reading and writing shared variables in memory.
• No need for explicit send/receive operations (as in MPI).
3. Synchronization Is Required
• Since multiple threads/processors can access the same memory simultaneously,
synchronization mechanisms (like mutexes, semaphores, or barriers) are needed to avoid
race conditions.
4. Ease of Programming
• Shared memory systems are generally easier to program than distributed systems.
• Languages like OpenMP and Pthreads are commonly used for shared memory
parallelism.
5. Scalability Limitations
• As the number of processors increases, memory contention and bandwidth become
issues.
• Typically scales well to tens of processors, but poorly to hundreds or thousands.
6. Memory Access Models
Uniform Memory Access (UMA):
All processors access memory at the same speed.
Non-Uniform Memory Access (NUMA):
Memory is closer (faster) to some processors than others. Performance depends on
memory locality.
Distributed memory system
a parallel architecture in which each processor has its own
private memory, and processors communicate explicitly by
passing messages.
2. Explicit Communication
• To share data, processors must send and receive messages.
• Example: using MPI_Send and MPI_Recv to exchange data between processes.
3. Highly Scalable
• Since memory is distributed, the system can scale to thousands or even millions of processors.
• Common in large HPC clusters and supercomputers.
• The interconnect plays a decisive role in the performance of both distributed- and shared-
memory systems
• a slow interconnect will seriously degrade the overall performance of all but the simplest
parallel program.
The size of shared-memory systems has increased, buses are being replaced by switched
interconnects.
switched interconnects use switches to control the routing of data among the connected devices.
The individual switches can assume one of the two configurations shown in Fig. 2.7(b).
With these switches and at least as many memory modules as processors, there will only be a
conflict between two cores attempting to access memory if the two cores attempt to
simultaneously access the same memory module.
For exam ple, Fig. 2.7(c) shows the configuration of the switches if P1 writes to M4, P2 reads from
M3, P3 reads from M1, and P4 writes to M2.
Crossbars allow simultaneous communication among different devices, so they are much faster
than buses.
A small bus-based system will be much less expensive than a crossbar-based system of the same
size.
Distributed-memory interconnects
Divided into two groups: direct inter connects and indirect interconnects
this is the smallest possible number of links, and the bisection width
of a square two-dimensional toroidal mesh is 2√p.
Bandwidth and Bisection bandwidth
• The bandwidth of a link is the rate at which it can
transmit data. It is given in megabits or megabytes
per second.
• Bisection bandwidth is used as a measure of
network quality. It’s similar to bisection width.
• For eg. if the links in a ring have a bandwidth of
one billion bits per second, then the bisection
bandwidth of the ring will be two billion bits per
second or 2000 megabits per second.
Fully connected network
• Each switch is directly connected to every other
switch. Its bisection width is p2/4.
The hypercube is a highly connected direct
interconnect that has been used in actual systems.
A one-dimensional hypercube is a fully connected
system with two processors. A two-dimensional
hypercube is built from two one-dimensional
hypercubes by joining “corresponding” switches.
Similarly, a three-dimensional hypercube is built
from two two-dimensional hypercubes.
• a hypercube of dimension d has p = 2d nodes,
and a switch in a d-dimensional hypercube is
directly connected to a processor and d switches.
• The bisection width of a hypercube is p/2, so it
has more connectivity than a ring or toroidal
mesh, but the switches must be more powerful,
since they must support 1 + d = 1 + log2(p) wires,
while the mesh switches only require five wires.
• So a
• hypercube with p nodes is more expensive to
construct than a toroidal mesh
A generic indirect network.
• The switches are two-by-two crossbars. Observe that unlike the crossbar, there are
communications that cannot occur simultaneously.
• For example, if processor 0 sends a message to processor 6, then processor 1 cannot
simultaneously send a message to processor 7. On the other hand, the omega network is less
expensive than the crossbar. The omega network uses 1 2p log2(p) of the 2 × 2 crossbar
switches, so it uses a total of 2p log2(p) switches, while the crossbar uses p2.
Cache coherence
Suppose we have a shared-memory system with two cores, each of which has its own
private data cache. As long as the two cores only read shared data, there is no problem.
• This unpredictable behavior will occur regardless of
whether the system is using a write-through or a write-
back policy. If it’s using a write-through policy
• The main memory will be updated by the assignment x =
7.
• However, this will have no effect on the value in the cache
of core 1. If the system is using a write-back policy, the
new value of x in the cache of core 0 probably won’t even
be available to core 1 when it updates z1
• The programmer doesn’t have direct control over
when the caches are updated, so her program
cannot execute these apparently innocuous
statements and know what will be stored in z1.
• There are several problems here,
– the caches we described for single processor systems
provide no mechanism for ensuring that when the
caches of multiple processors store the same variable
– an update by one processor to the cached variable is
“seen” by the other processors. That is, that the cached
value stored by the other processors is also updated.
This is called the cache coherence problem.
2 main approaches to ensuring cache
coherence:
• Snooping cache coherence
• Directory-based cache coherence
Snooping cache coherence
• When the cores share a bus, any signal transmitted
on the bus can be “seen” by all the cores connected
to the bus.
• Thus when core 0 updates the copy of x stored in its
cache, if it also broadcasts this information across
the bus, and if core 1 is “snooping” the bus, it will
see that x has been updated, and it can mark its
copy of x as invalid.
• The broadcast only informs the other cores that the
cache line containing x has been updated, not that x
has been updated.
Points to remember
Snooping cache coherence
All caches monitor (or “snoop”) a shared bus for memory transactions A centralized or distributed directory tracks which caches have a copy
Basic Idea
to keep copies coherent. of each memory block.
Each cache listens for writes from other processors and Processor queries directory to check sharers/owners, then sends
Coherence Maintenance
invalidates/updates its copy if necessary. targeted invalidation or update messages.
No explicit list — coherence ensured by everyone listening to the same Directory maintains a bit vector or list indicating which caches hold
Data Tracking
bus. the block.
Latency Low latency for small systems (no lookup delay). Slightly higher latency due to directory lookup.
Traffic High traffic on the bus, especially as processor count increases. Lower overall traffic, but more complex message routing.
Implementation Simpler to implement — relies on shared bus. More complex — requires directory storage and lookup logic.
• // Thread 0
• for (int i = 0; i < N; i++)
• A[0] += 1;
• // Thread 1
• for (int i = 0; i < N; i++)
• A[1] += 1;
Even though A[0] and A[1] are accessed independently, they may lie on the
same cache line. This causes performance issues due to false sharing.
Avoiding False Sharing:
To parallelize:
• Suppose you have p threads or processes.
• Divide the array into p equal-sized chunks.
• Each thread/process handles its assigned chunk:
– process/thread 0 responsible for elements0, . . . , n/p − 1,
process/thread 1 would be responsible for elements n/p, .
. . , 2n/p − 1, and so on.
– This avoids any data dependency, synchronization, or
communication (in shared memory).
#include <omp.h>
#include <stdio.h>
#define N 1000000
double x[N], y[N];
int main() {
// Initialize arrays
for (int i = 0; i < N; i++)
{
x[i] = i;
y[i] = 2 * i;
}
// Parallel addition
#pragma omp parallel for
for (int i = 0; i < N; i++)
{
x[i] += y[i];
}
return 0;
}
• #pragma omp parallel for automatically divides the loop across available threads.
• No synchronization or communication needed — each thread operates on its own
part of the array.
1. What Makes Parallelization Easy
• In simple cases (like element-wise array addition):
• The programmer needs to:
– Divide the work among threads or processes so that:
(a) Each gets about the same amount of work → this is called
load balancing.
(b) There's minimal communication between threads or
processes.
• Load balancing: Making sure no thread/process is
idle while others are still working.
• These concerns are minor or trivial when:
The total amount of work is known ahead of
time.
The tasks are uniform and independent.
2. What Makes Parallelization Hard
• The vast majority of problems are much more
determined to resist our efforts to find a
parallel solution.
• In shared-memory programming,
communication between threads happens
through shared variables.
• This is called implicit communication — you
don’t need special functions to send/receive
data.
• Example:
– Thread A writes to shared_var
– Thread B later reads from shared_var
– This is implicit communication — nothing like
send() or receive() is used (unlike in MPI)
Advantages of Shared-Memory
• Easier to program simple parallel tasks, especially when all
threads run in the same process space.
• No need to write communication code (e.g., no message
passing).
Challenges
• Because multiple threads can access the same variables:
– Race conditions can occur (when two threads access a variable
concurrently and at least one writes).
– Requires synchronization mechanisms (e.g., mutexes, barriers, critical
sections).
#pragma omp parallel shared(a) private(i)
{
int i = omp_get_thread_num();
a[i] = i * i; // 'a' is shared, 'i' is private
}
• a is shared among all threads — used to store
results.
• i is private — each thread has its own copy, so
they don’t interfere with each other.
Dynamic Thread Paradigm
• Structure:
– There's a master thread that runs the program.
– When new work arrives (e.g., from the network), the
master dynamically creates ("forks") worker threads.
– Each worker:
• Performs a specific task.
• Terminates and joins the master once done.
• Benefits:
– Efficient resource usage: threads only exist and use
memory while needed.
– Good for event-driven systems (like web servers) or
asynchronous workloads.
Drawbacks:
• Thread creation and destruction (fork/join)
are not free — they introduce overhead.
• May not scale well if the workload is constant
and predictable.
Common Usage:
• Network servers, GUI event handlers,
background jobs, etc.
2. Static Thread Paradigm
• Structure:
– The master thread sets up the program and then forks all worker threads at
once.
– All threads run in parallel until the work is done.
– After completion, threads join the master, which may do cleanup and exit.
• Benefits:
– Better performance potential when:
• Work is predictable or long-running.
• Forking threads repeatedly would be expensive.
– Aligns closely with distributed-memory (MPI) models, where processes also run
from start to finish.
• Drawbacks:
– Idle threads still consume resources (e.g., stack space, thread-local data).
– Less flexible for irregular or unpredictable workloads.
• Common Usage:
– Scientific computing, simulations, parallel loops (e.g., OpenMP parallel for),
batch processing.
Non determinism
Introduction to MIMD and Concurrency
• MIMD: Multiple Instruction, Multiple Data systems
• Threads/processors execute asynchronously
• Leads to nondeterministic behavior in programs
• Same input → Different outputs possible
What is Nondeterminism?
• A computation is nondeterministic if:
– Same input
– Different outputs (execution-dependent)
• Caused by:
– Asynchronous execution
– Independent thread scheduling
my_val = Compute_val ( my_rank ) ;
x += my_val ;
As an example,
• suppose we have two threads, one with ID or
rank 0 and the other with ID or rank 1.
• Suppose also that each is storing a private
variable my_x, thread 0’s value for my_x is 7,
and thread 1’s is 19.
Simple Example of Nondeterminism
Critical Sections
A critical section is code that must not be executed by more than one thread
at once
Keep critical sections:
Short
Efficient
Infrequent
//example to show critical section
#include <stdio.h>
#include <pthread.h>
long counter = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void worker(void)
{ for (int i = 0; i < 100000; ++i) {
pthread_mutex_lock(&m); // enter critical section
counter++; // shared state
pthread_mutex_unlock(&m); // leave critical section
printf("counter = %ld\n\n",counter);
}
printf("counter = %ld\n\n",counter);
return NULL;
}
int main()
{
worker();
return 0;
Alternatives to Mutexes
Example for busy waiting
#include <stdio.h>
#include <stdbool.h>
#include <pthread.h>
int main() {
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
printf("Main: waiting for worker...\n");
while (!flag) {
// Busy wait
}