Parallel Computing
Parallel Computing
PARALLEL COMPUTING:
TYPES OF PROCESSORS:
● The stack segment is used for function calls, temporary data, and local variables.
● The data segment stores static/global variables and is initialized when the program
starts.
Working Example:
CISC Instructions:
Key Points:
🧠 Meaning:
● Encourages instruction-level parallelism (ILP) because instructions are uniform and easily
pipelined.
● Each instruction typically does one task (e.g., load, store, add).
● Instructions are uniform in size and format, making them easy to decode and pipeline.
📦 Breakdown:
🔄 This allows pipelining: while instruction 1 is executing, instruction 2 can be decoded, and instruction 3
can be fetched — all happening in parallel in different CPU stages.
Real-Life Analogy
Imagine a factory:
● RISC: Each worker does one small job (like screw in a bolt), quickly and in sync on an
assembly line. Tasks can overlap across workers.
Each instruction is simple, has a fixed format, and can be broken into stages (Fetch → Decode
→ Execute → Memory → Write Back), so they are perfect for pipelining.
CISC:
● Uses complex instructions that can perform multiple operations.
● CISC instructions are complex — they can do multiple things at once (e.g., load, add, and store).
● These require micro-operations internally, which are broken down and executed over several
cycles.
📦 Breakdown:
1. Fetch instruction
2. Decode (more complex than RISC)
3. Load memory location A
4. Load memory location B
5. Add values
6. Store result back
🧪 Example in CISC:
Looks short, but takes multiple clock cycles internally due to complexity.
Real-Life Analogy
CISC: One worker tries to do the whole job (assemble the whole product), taking longer and holding up
the line.
🔸 Problem:
● Instruction length and format vary → Decoding is slower and less predictable
● Multiple operations inside one instruction → Must be broken into micro-operations (called
microcode)
● CPU can’t pipeline well because:
o It can’t start executing the next instruction until it finishes all steps of the current one
o Dependencies between the stages are too tight
o Variable instruction time = Pipeline stalls (bubbles)
Flynn’s taxonomy:
Parallel computing is a computing where the jobs are broken into discrete parts that can be
executed concurrently. Each part is further broken down to a series of instructions. Instructions
from each part execute simultaneously on different CPUs. Parallel systems deal with the
simultaneous use of multiple computer resources that can include a single computer with
multiple processors, a number of computers connected by a network to form a parallel
processing cluster or a combination of both.
Parallel systems are more difficult to program than computers with a single processor because
the architecture of parallel computers varies accordingly and the processes of multiple CPUs
must be coordinated and synchronized.
The crux of parallel processing are CPUs. Based on the number of instruction and data streams
that can be processed simultaneously, computing systems are classified into four major
categories:
Flynn’s classification –
The system performs different operations on the same data set. Machines built using the MISD
model are not useful in most of the application, a few machines are built, but none of them are
available commercially.
MIMD machines are broadly categorized into shared-memory MIMD and distributed-memory
MIMD based on the way PEs are coupled to the main memory.
In the shared memory MIMD model (tightly coupled multiprocessor systems), all the PEs are
connected to a single global memory and they all have access to it. The communication
between PEs in this model takes place through the shared memory, modification of the data
stored in the global memory by one PE is visible to all other PEs. Dominant representative
shared memory MIMD systems are Silicon Graphics machines and Sun/IBM’s SMP (Symmetric
Multi-Processing).
In Distributed memory MIMD machines (loosely coupled multiprocessor systems) all PEs have a
local memory. The communication between PEs in this model takes place through the
interconnection network (the inter process communication channel, or IPC). The network
connecting PEs can be configured to tree, mesh or in accordance with the requirement.
The shared-memory MIMD architecture is easier to program but is less tolerant to failures and
harder to extend with respect to the distributed memory MIMD model. Failures in a shared-
memory MIMD affect the entire system, whereas this is not the case of the distributed model,
in which each of the PEs can be easily isolated. Moreover, shared memory MIMD architectures
are less likely to scale because the addition of more PEs leads to memory contention. This is a
situation that does not happen in the case of distributed memory, in which each PE has its own
memory. As a result of practical outcomes and user’s requirement , distributed memory MIMD
architecture is superior to the other existing models.
🧠 1. Instruction and Data Stream Classifications (Flynn’s Taxonomy):
These are basic models of how instructions and data are handled in computer systems.
● Most modern multicore processors and distributed systems follow this model.
🧩 3. Diagrams Explanation:
▶️SISD Diagram:
▶️SIMD Diagrams:
● Register File ↔ RAM – Processor loads/stores data between fast-access registers and main
memory.
● ALU ← Register File – Data used in computations is fetched from registers.
📌 Clusters vs Grids:
● Definition: The ability of an operating system to execute multiple tasks (processes) at the same
time.
● Each task is given a time slice by the CPU scheduler to execute.
Key Component:
● Time-sharing: This is the mechanism that enables multitasking by giving each process a
small time slice or quantum of the CPU, then switching to another process.
● Hardware Considerations: For multitasking to work effectively, the hardware needs to
support fast switching between tasks. This may include things like a multi-core processor
or efficient memory management systems.
Limitations of Multitasking:
● Multitasking relies heavily on I/O (input/output) operations, so tasks that are CPU-
intensive may not benefit from multitasking in the same way.
● If multiple tasks demand too much CPU time, the system may become slower, especially
if there is insufficient memory or processing power
🔹 Types:
🟩 1. Preemptive Multitasking
✅ Definition:
The OS takes control of the CPU and decides when to pause (preempt) a running task and switch to
another.
Tasks don’t get to decide when to stop — the OS can interrupt any task and switch to another at any
time.
🧠 How it works:
● When the time is up, it pauses the current task and switches to the next.
📱 Real-Life Example:
Imagine you are in a classroom, and the teacher allows each student 1 minute to speak. After 1 minute,
the teacher says “Next!” even if the student isn’t finished. That’s preemptive.
But with preemptive multitasking, the OS automatically switches between these tasks without waiting
for one to finish.
🟨 2. Cooperative Multitasking
✅ Definition:
Here, each task must give up control voluntarily — the OS won’t interrupt it. If one task misbehaves or
takes too long, others have to wait.
The responsibility to share the CPU lies with the running tasks.
🧠 How it works:
● A task runs and must call a function like yield() to let others run.
📱 Real-Life Example:
You're in a group meeting, and you politely let someone else speak when you're done. No one forces
you to stop. That’s cooperative.
void task2() {
while (1) {
doWork();
yield(); // Giving control to other task
}
}
Here, each task must call yield() or similar to allow others to run.
Cooperative = Tasks must behave well and yield control → can cause freezes if not careful.
● CPU switches when one program is waiting (like waiting for input/output).
🧠 Think of it like:
🎂 Chef cooking 3 dishes, but only 1 stove. When one dish is boiling, chef switches to another dish.
📖 Example:
You’re typing in Word, listening to music, and downloading a file.
All seem to be working at the same time, thanks to fast switching.
🧠 Think of it like:
🎂 Chef switches between dishes very fast, so it feels like all are being cooked together.
Code
The actual code (instructions) of the program
Segment
Data
Global and static variables
Segment
🔐 Memory isolation:
● But if they want to communicate, they must use Inter-Process Communication (IPC) like:
o Shared memory (shmget())
o Pipes
o Sockets
● Definition: Multiple threads (smaller units of a process) run concurrently within the same
process.
● Threads share memory and resources, making communication faster.
🔹 Benefits:
● Synchronization Issues: Since threads share memory, access to shared resources must
be synchronized to avoid conflicts (like race conditions or deadlocks).
● Complexity: Managing threads requires more careful planning to avoid bugs like
deadlocks, where two or more threads are waiting for each other indefinitely.
Concurrency vs Parallelism:
● Concurrency is when multiple tasks are making progress at the same time, but not necessarily
simultaneously. In multitasking, this is often achieved by switching between tasks so quickly that
it seems as if they’re running together.
● Parallelism involves truly simultaneous execution of tasks, typically on multiple CPU cores or
processors.
"A clone of the server is made and then clients are assigned to each server..."
it doesn't literally mean copying the whole physical server — instead, it refers to creating a new thread
or process for each client connection.
💡 Translation:
● The main server (called the main thread) keeps listening for incoming client connections.
● As soon as a new client connects, the server creates a new thread (a clone in a programming
sense) to handle that specific client.
● The main server goes back to listening for the next client.
1. Start the main server: It opens a socket and waits for client connections.
2. Client connects: A user sends a request (like opening a chat).
3. Server spawns a thread: The server creates a new thread (a lightweight “clone”) for that client.
4. Thread handles the client: That thread talks to the client, receives/sends data.
5. Main server stays free: The original server thread is ready to accept the next client.
Most systems use the POSIX thread (pthreads) library. Important functions:
➡️That means:
Memory Shared by
Notes
Area Threads?
Code
✅ Yes All threads run the same code
Segment
Data
✅ Yes Global/static variables are shared
Segment
Stack
❌ No Each thread has its own stack
Segment
Because:
● Each thread can have its own function calls and local variables.
● 📌 Summary Table
Area of Shared in Shared in
Memory Multithreading Multiprogramming
Code Segment ✅ Yes ❌ No
Data Segment ✅ Yes ❌ No
Heap ✅ Yes ❌ No
Stack ❌ No (one per thread) ❌ No
🧪 Example:
Let’s say you write a C program with:
c
CopyEdit
int globalVar = 10; // in Data Segment
✅ So in simple terms:
● Processes (multiprogramming): Each is like a separate person with their own home and kitchen.
● Threads (multithreading): Like multiple people in the same house, sharing kitchen and living
room, but having their own bedrooms (stack).
🔹 Types
All processors are equal. They share the same memory, same I/O, and run any task.
Imagine a team of chefs 🍳🍳 working in the same kitchen, with the same tools and same recipe book.
Each chef can cook any dish — they are equal and help each other.
● ✅ Code Segment
● ✅ Heap
● ✅ I/O devices
int main() {
pthread_t cpu1, cpu2;
pthread_join(cpu1, NULL);
pthread_join(cpu2, NULL);
return 0;
}
🔍 Summary:
● CPU 0 → handles OS
They don't share the same heap or data — they communicate with message passing or shared memory
regions.
✅ Summary Table:
Feature SMP (Symmetric) AMP (Asymmetric)
CPU equality All CPUs are equal One master, others are workers
Task
Any CPU can do any task Specific task assigned to each CPU
distribution
Communicatio
Direct memory access Message passing or shared memory
n
Single Tasking:
● Definition: Single-tasking refers to the execution of one task at a time. It is the basic form of
computing where only one process runs on the CPU. Each process must finish before the next
one starts.
● Example: When you're working on a computer, and you're using a word processor, you're
focused only on that task until you decide to switch to another program.
Slide 2: Outlines
● What is Concurrency?
● Levels of Concurrency:
o Computer Hardware
o Operating System
o Software/Algorithms
● How to Achieve Concurrency:
o Multi-Programming
o Multithreading
o Multiprocessing
● Issues with Concurrency:
o Data Inconsistency / Coherency
● Solutions:
o Mutual Exclusion
o Synchronization
o Deadlock and its Solutions
● Summary
● Oxford Definition: The fact of two or more events or circumstances happening or existing at the
same time.
● Computing Definition: The ability to execute more than one program or task simultaneously.
o Concurrency is essentially about multiple tasks being handled at the same time, which
improves performance and efficiency.
● The real world involves multiple independent actors interacting with each other.
● Examples:
o Software Engineering: Gantt Charts show overlapping tasks.
o Expression Parsing: Mathematical expressions like ((x + y) * (a - b))^2 have
independent subexpressions that can be computed concurrently.
o Client/Server Computing: Clients and servers operate independently but interact.
● APIs:
o fork() for creating processes.
o pthread_create() for creating threads.
● Types of Applications:
o Client/Server
o Peer-to-Peer
● Types of Algorithms:
o Sequential: Runs one task at a time.
o Parallel: Tasks run simultaneously (e.g., on GPU or with multiprocessing).
o Distributed: Runs on separate machines connected over a network.
Slide 9: Summary
👉 Threads are lighter, faster, and share memory—ideal for parallel tasks in the same app.
A basic C program with just a main() function — this is single-threaded, with only the main
thread running.
Differences:
● Threads use Thread Control Blocks (TCBs) instead of Process Control Blocks (PCBs).
🧵 Single-threaded Program
Only one person can work in the shop at a time. That worker:
● Has their own notebook (stack) for keeping notes (function calls, variables)
✅ Advantage:
Threads can communicate quickly by reading/writing to the same memory without asking the
manager (operating system).
❗ Danger:
If two threads try to grab the same tool at the same time, things can go wrong — this is where
race conditions happen.
Slide 9: Why Multithreading?
● On multi-core CPUs, this means you can assign one thread per core and get tasks done faster.
Imagine splitting a big job (like sorting a massive list) into smaller tasks and having multiple people
(threads) do each part simultaneously.
2. ⏳ Blocking I/O
● When a program waits for input/output (like reading a file or waiting for user input), it stops
doing other work — this is called blocking.
● If you use threads, only that one thread is blocked, while others can keep working!
Example: While one thread waits for a file to download, another can keep updating the UI.
3. 💸 Cost Efficiency
● Databases: Perform complex queries, indexing, and caching using multiple threads in parallel.
Thread Function
c
Copy code
void *mythread(void* args) {
print(“%s\n”, (char*) args);
return NULL;
}
● args is a pointer to whatever argument you pass when starting the thread (in this case, a string
like "A" or "B").
● It prints the string and returns.
Each thread will run mythread() and print its assigned letter.
🧠 Main Function
c
Copy code
int main(int argc, char *argv[]) {
pthread_t p1, p2;
printf("main begin\n");
Pthread_create(&p1, NULL, mythread, "A");
Pthread_create(&p2, NULL, mythread, "B");
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("main end\n");
return 0;
}
🧾 What’s happening:
OR
css
Copy code
main begin
B
A
main end
🔍 Context:
This is a broadcast algorithm — it means one processor (P1) has a value x, and the goal is to send
(broadcast) x to all other processors (P2, P3, ..., Pn) using EREW PRAM model where:
● Exclusive Read (ER): No two processors can read from the same memory location at the same
time.
● Exclusive Write (EW): No two processors can write to the same memory location at the same
time.
● Only P1 has the value x in its private memory and writes it to L[1] (shared memory location 1).
● L[1] ← y where y = x.
(b) Iteration 1 (i = 0):
● 2^i = 1
● P2 gets x from L[1] and writes it to its private memory y, and stores it in L[2].
● 2^i = 2
● Loop from j = 3 to 4
● 2^i = 4
● Loop from j = 5 to 8
● This pattern ensures no memory location is accessed by multiple processors at the same time
(hence, EREW is maintained).
● First P1 → P2
At each stage, the number of processors who have x doubles, and each processor uses a different
memory location to ensure no conflicts.
Time Complexity:
💡 Real-Life Analogy:
● One person tells one new person at a time (no double whispering allowed).
● So it spreads to the whole class very quickly, even with the rule.
▶️Iteration 1 (i = 1):
● 2⁰ + 1 = 2, so j from 2 to 8
So:
● ...
🟦 First Row in Diagram: You see arrows coming from A[j-1] to A[j].
▶️Iteration 2 (i = 2):
● 2¹ + 1 = 3, so j from 3 to 8
So:
● ...
▶️Iteration 3 (i = 3):
● 2² + 1 = 5, so j from 5 to 8
So:
● A[1] = A[1]
● ...
Each A[j] now holds the sum of all previous elements, i.e., partial sum
📘 Algorithm: AllSums_EREW
plaintext
CopyEdit
for i = 1 to log n do
🔹 What this means:
👉 Each i controls how far back we look in the array while adding values.
plaintext
CopyEdit
forall Pj, where 2^(i-1) + 1 ≤ j ≤ n do in parallel
🔹 What this means:
✅ "in parallel" means all these processors work together at the same time!
j
2^(i-1) +
rang
1
e
2 to
2
8
3 to
3
8
j
2^(i-1) +
rang
1
e
5 to
35 8
plaintext
CopyEdit
A[j] ← A[j] + A[j - 2^(i-1)]
🔹 What this means:
💡 Example for i = 2:
plaintext
CopyEdit
A[3] = A[3] + A[1]
A[4] = A[4] + A[2]
A[5] = A[5] + A[3]
...
The distance 2^(i-1) is how far back in the array you look.
plaintextsymm
CopyEdit
endfor
endfor
🔹 What this means:
💡 What is MPI?
📦 In Simple Words:
Imagine you have many computers (or many cores in a computer), and you want them to work together
on one big problem.
🔍 Explanation:
● Used to combine values from all processes into a single result at one root process.
🧩 Parameters:
✅ Example:
makefile
Copy code
P0: 1
P1: 2
P2: 3
P3: 4
● Only the process with rank = root will store the final reduced result.
✅ Example:
Processes have values: [1, 2, 3, 4]
Then:
● P0: 1
● P1: 1+2 = 3
● P2: 1+2+3 = 6
● P3: 1+2+3+4 = 10
Each process gets the sum of all values before and including itself.
🟨 1. What is MPI_Scatter?
🔸 Concept:
Imagine you are a teacher and you have 4 question papers (A, B, C, D).
You want to give one paper to each of your 4 students (P0, P1, P2, P3).
✅ In MPI:
● This process sends out chunks of the data to all processes including itself.
📦 So:
● P0 sends A to P0
● P0 sends B to P1
● P0 sends C to P2
● P0 sends D to P3
🟩 2. What is MPI_Gather?
🔸 Concept:
Now suppose each student (P0 to P3) has solved their question paper and want to give it back to the
teacher (P0).
✅ In MPI:
● All processes send their result back to one process (usually P0).
📦 So:
● P0 sends A to P0
● P1 sends B to P0
● P2 sends C to P0
● P3 sends D to P0
Before Scatter:
P0: A B C D
P1:
P2:
P3:
📤 Step 1: SCATTER (left to middle)
P0: A
P1: B
P2: C
P3: D
Before:
P0: A
P1: B
P2: C
P3: D
📤 MPI_Allgather: Every process sends its value to all others
After:
P0: A B C D
P1: A B C D
P2: A B C D
P3: A B C D
Before:
P0: A0 A1 A2 A3
P1: B0 B1 B2 B3
P2: C0 C1 C2 C3
P3: D0 D1 D2 D3
After MPI_Alltoall:
This waits for the message to arrive completely before the program can move to the next line.
You're waiting at the door for a delivery. You can't do anything else until the delivery arrives.
perl
Copy code
P0 P1
| send |
| ------> |
| | recv
● P0 then calls MPI_Send — the message is transferred, and both processes synchronize at that
point.
● Both processes are "ready" to interact — P1 is waiting for data, and P0 is sending it.
● Both calls are blocking (in standard MPI), so they will pause until the message transfer
completes.
objectivec
Copy code
P0 calls MPI_Send
BUT
🔥 What happens?
● MPI behavior depends on the size of the message and internal buffering.
pa
Cluster View:
Copy code
0 1 2
3 4 5
6 7 8
✅ Example:
Copy code
Copy code
🔍 Explanation:
✳️2. By Exclusion
● You can create groups based on even or odd process IDs using range operations or loops.
✅ Example:
Copy code
✅ In simple terms:
MPI_Cart_create helps arrange processes in a grid-like structure (like 2D or 3D) so you can
work with neighbors easily — super useful for problems in scientific computing like matrix
operations, fluid dynamics, simulations, etc.
💻 Basic Example:
MPI_Comm cart_comm;
int dims[2] = {4, 4}; // 4x4 grid
int periods[2] = {0, 0}; // No wrap-around
int reorder = 1;
If you want, I can show you a full example or break down each parameter! Want me to?
When you start an MPI program, all processes are automatically part of MPI_COMM_WORLD. It’s
like the "global group chat" for your entire MPI program.
💡 What does it do?
📦 Example:
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
● periods: array that says whether the grid wraps around in each dimension (1 = yes, 0 =
no).
● mapping: allows MPI to reorder ranks (1 = allow, 0 = don't allow).
● If you know the coordinates, this gives you the corresponding rank.
● If you know the rank, this tells you the process's grid location.
MPI_Comm gridcomm;
● Dimension 0 (rows) = 2.
● Dimension 1 (columns) = 3.
int mapping = 0
Tells MPI:
● Based on MPI_WORLD_COMM.
● No wrap-around.
● No rank reordering.
● MPI_Cart_rank and MPI_Cart_coords let you map between rank and grid position.
● Communication becomes much easier for structured problems like matrices, image filters, and
simulations.
If you want, I can show you a full runnable code example using this exact setup!
Want me to write one for you?
4o
You said:
`periods`: array that says whether the grid **wraps around** in each dimension (`1 = yes`, `0 =
no`).
🧠 Simple Meaning:
● periods[dimension] = 0 → No wrap-around.
Edges have no neighbors beyond the border.
● periods[dimension] = 1 → Wrap-around is enabled.
The grid "loops around" like a torus — the last process is connected back to the first one.
💡 Example:
Let’s say you have a 2 x 3 Grid like this:
Row: 2
Column: 3
🔸 If periods[0] = 0 and periods[1] = 0
css
CopyEdit
[0,0] ⇄ [0,1] ⇄ [0,2]
↑ ↓ ↑ ↓ ↑ ↓
[1,0] ⇄ [1,1] ⇄ [1,2]
It’s like turning your flat grid into a doughnut shape (torus).
● For some scientific simulations (e.g., physics, weather, fluid flow) the world is modeled as a
continuous loop.
● Periodic boundaries avoid “edge effects.”
✅ Summary Table:
Value of Meaning Behavior
periods[]
Edge cells have no extra
0 No wrap-around (flat grid)
neighbor.
Wrap-around enabled
1 Edges connect back around.
(circular/torus)
You’re saying:
❌ No wrap-around in rows.
❌ No wrap-around in columns.
6 nodes: [0,1,2,3,4,5]
Connections: As shown by the lines.
💡 Code Breakdown
MPI_Comm graphcomm;
The index array tells how many neighbors each node has.
No Cumulative Edge
Meaning
de Index
This holds the actual neighbors for all nodes, one after another.
Example:
But to tell MPI "which house is connected to which," you need to give two pieces of
information:
💡 1️⃣ edges[]
This list stores the neighbors (connections) for all the houses — one after another.
💡 2️⃣ index[]
This array tells MPI where to stop for each house's neighbor list inside edges[].
It’s like marking the end of the list for each house!
🔍 Example:
diff
CopyEdit
index[6] = {2, 5, 8, 12, 14, 16}
It tells MPI:
✅ So in easy words:
● The index[] array tells where each house's (node's) neighbor list ends in the edges[]
list.
● MPI uses it to understand which neighbors belong to which node.
int mapping = 0;
Tells MPI:
✅ Definition of OpenMP
OpenMP (Open Multi-Processing) is:
● It allows developers to write parallel code using a simple and flexible syntax.
● Portability:
○ Code written with OpenMP is platform-independent with minimal modifications
needed.
● Efficiency:
○ Helps in efficient use of CPU cores on modern processors by running code in parallel.
○ Specl
ial instructions for the compiler to parallelize code.
○ Common functions:
3. Environment Variables:
○ Example:
● Think of threads like workers. If you say, "Use 4 workers", OpenMP will create 4 threads.
bash
Copy code
Copy code
You don’t need to set the thread count in your code if you're using OMP_NUM_THREADS from outside.
● Clean: Keeps your code simple while allowing control from outside.
🔷 Example:
Copy code
#include <stdio.h>
#include <omp.h>
int main() {
return 0;
}
● Output: The statement inside will be printed by multiple threads.
1. Environment Variable:
2. Runtime Function:
🔷 Example:
Copy code
#include <stdio.h>
#include <omp.h>
int main() {
}
return 0;
● This sets the number of threads to 4 at runtime and prints a message from each.
🔷 Example:
c
CopyEdit
// Loop body
}
🔷 Full Example Using 4 Threads:
c
CopyEdit
✅ Reduction in OpenMP
🔍 Definition:
A reduction operation allows each thread to maintain its own private copy of a variable, and combine
them at the end using an operation (like +, *, etc.).
🔷 Example:
c
CopyEdit
int sum = 0;
🔷 Full Example:
c
CopyEdit
int N = 100000;
arr[i] = 1;
omp_set_num_threads(4);
sum += arr[i];
⚠️Loop-Carried Dependencies
❗ Definition:
A loop-carried dependency occurs when one iteration depends on the result of a previous one.
CopyEdit
int N = 10;
int arr[N];
arr[0] = 1;
c
CopyEdit
arr[i] = arr[i - 1] + 1;
● Ensures only one thread at a time can execute the entire block of code.
✅ Example:
CopyEdit
arr[i] = arr[i - 1] + 1;
✅ Example:
CopyEdit
sum += arr[i];
Loop scheduling in OpenMP refers to how iterations of a loop are distributed among
available threads. The goal is to ensure efficient parallel execution by balancing the
workload across threads.
● Static Scheduling
● Dynamic Scheduling
● Guided Scheduling (mentioned but not elaborated on in the slides)
Static scheduling assigns a fixed block of loop iterations to each thread before
execution begins.
🔹 How it works:
● Once a thread gets its assigned iterations, it executes them without further
coordination.
🔹 Example:
If:
● Threads = 4
● Iterations (N) = 12
🔹 Code Snippet:
c
Copy code
sum += arr[i];
📌 2. Dynamic Scheduling
🔹 Definition:
Dynamic scheduling assigns a chunk of iterations to a thread. When a thread finishes its
current chunk, it requests another. This continues until all iterations are processed.
🔹 How it works:
● Threads that finish early can take on more work → better load balancing.
🔹 Code Snippet:
Copy code
sum += arr[i];
}
🔹 Real-life Example (from slides):
For:
● Threads = 4
● N = 25
● Chunk size = 3
Threads pick up new chunks dynamically after finishing the previous one.
● Race conditions can occur if multiple threads try to modify the variable simultaneously.
Example:
Copy code
int shared_var = 0;
📝 Line-by-Line Explanation:
🔹 int shared_var = 0;
● This variable is shared, meaning all threads in the parallel region will access and modify the
same memory location.
● This directive tells the compiler to start a parallel region using OpenMP.
● All threads spawned by OpenMP will run the code inside the block.
● The variable shared_var is explicitly declared as shared, so each thread will refer to the same
copy of it.
🔹 shared_var += 1;
● However, since shared_var is shared and multiple threads are modifying it simultaneously,
this causes a race condition.
🔸 Private Variables
Example:
c
Copy code
int shared_var = 0;
int private_var = 0;
shared_var += 1;
🔍 Explanation:
🔸 int shared_var = 0;
🔸 int private_var = 0;
● This is declared before the parallel region, but will be treated as private in the parallel block.
● private_var is private: each thread gets its own copy, which is not initialized unless done
explicitly.
● shared_var is shared: all threads access and modify the same memory location.
➤ private_var = thread_id;
● Sets each thread’s private copy of private_var to its own thread ID.
➤ shared_var += 1;
● Since there is no protection, this causes a race condition — the final result may not be equal to
the number of threads.
➤ printf(...)
● Prints the current thread's ID, its private variable value (which is the same as the thread ID), and
the current shared variable value.
When OpenMP gives each thread its own private copy of a variable (like private_var), that copy is
created without a value unless you explicitly assign one inside the parallel block. Even though
private_var was initialized to 0 before the parallel block, the private clause causes each thread to
create a new copy, and that new copy is not given the value 0. Its value is undefined — it could be
anything (junk from memory).
Syntax:
Copy code
Example:
Copy code
int shared_var = 0;
🔸 Atomic Operation
Syntax:
Copy code
shared_var += 1;
Example:
Copy code
int shared_var = 0;
shared_var += 1;
}
🔹 3. Example: Sum of First N Numbers using OpenMP
Problem:
Copy code
int sum = 0;
sum += i;
Copy code
sum += i;
🆚 private(var) vs reduction(+:var)
Feature private(var) reduction(+:var)
Combining You must manually combine OpenMP automatically combines values from
Values results if needed each thread using the given operator (+, *, max,
etc.)
Shared Final You need to manually store a Final result is automatically stored in the original
Result final result in a shared variable variable after parallel region
Typical Use Use for temporary variables or Use for tasks like summing, multiplying,
Case intermediate calculations min/max, etc. across threads
● If multiple threads are running on different cores and access the same memory location,
inconsistency may occur:
○ If one thread updates a value in its local cache, others may still see the old value from
their cache.
● MESI Protocol:
○ Modified
○ Exclusive
○ Shared
○ Invalid
These states define how a memory block is shared or updated between caches.
OpenMP does not directly manage cache coherency. Instead, it relies on the system’s
hardware and memory consistency model.
flush is a directive in OpenMP that forces memory updates to be visible to all threads by
synchronizing the thread’s view of memory with main memory.
🔍 Purpose:
● It ensures that shared variables are correctly updated and visible across all threads.
● Some OpenMP implementations may automatically handle flushing, but #pragma omp
flush gives explicit control.
● Helps manage cache coherency at the thread level when updating shared variables.
CopyEdit
int shared_var = 0;
CopyEdit
local += 1;
shared_var = local;
So yes — if you want the write to be seen by others, you should flush after writing.
🔁 Summary:
Purpose Where to Place flush(var)
🔎 Step-by-Step:
○ Any future access by other threads will see the updated value.
CopyEdit
#include <stdio.h>
#include <omp.h>
int shared_var = 0;
int main() {
if (thread_id == 0) {
shared_var = 10; // Thread 0 updates shared_var
if (thread_id == 1) {
return 0;
❗ Problem:
● Thread 1 might still read 0, because the cache hasn't been synchronized — classic cache
coherency issue.
CopyEdit
#include <stdio.h>
#include <omp.h>
int shared_var = 0;
int main() {
if (thread_id == 0) {
shared_var = 10;
if (thread_id == 1) {
return 0;
✅ Output:
java
CopyEdit
Now both threads see consistent data, thanks to the flush directive.
● SIMD (Single Instruction, Multiple Data): This refers to the ability of a GPU core to perform the
same operation on multiple pieces of data at once. In simpler terms, SIMD allows the GPU to
apply the same instruction (e.g., adding a number) to many data points simultaneously. This is
why GPUs are so efficient for tasks like image rendering, where the same operation needs to be
applied to many pixels.
● Multi-threaded execution: A single GPU core can run multiple threads at the same time,
enhancing its performance by allowing multiple calculations to happen concurrently.
● Memory (DDR5 DRAM): The GPU uses a high-speed memory called DDR5 DRAM to store the
data it needs for rendering. This memory is much faster than regular system memory (RAM),
providing the necessary bandwidth for fast data retrieval.
● Primitives: A primitive is a basic graphical shape, like triangles, points, or lines, created using
vertices. These are the building blocks of all 3D shapes in graphics rendering.
● Fragments: Once the 3D shapes (primitives) are processed, they are broken down into smaller
pieces, called fragments. Each fragment corresponds to a pixel in the final image.
● Pixels: The final output of the graphics pipeline. Once fragments are processed, they become
pixels in the final image displayed on your screen.
○ Input: A list of vertices in 3D space. These are points like v0, v1, v2, etc. that define the
shape of 3D objects.
○ These vertices are organized into triangles (each set of three vertices forms a triangle).
○ Given a camera's position, the system computes where the 3D vertices should appear
on the 2D screen.
○ This step transforms 3D points into 2D points (projecting them to fit the screen view).
○ The system groups the projected vertices into primitives (e.g., triangles).
○ The system generates fragments for each pixel that overlaps a primitive. These
fragments will later be used to calculate the color of pixels.
○ The final fragments are compared, and the closest fragment to the camera is kept. This
determines which fragments will contribute to the final image, creating the visual
output on the screen.
● Example materials: Different objects like shiny balls, matte surfaces, or glass materials reflect
light differently, and fragment processing adjusts the appearance based on this.
○ glLight: This function allows developers to set light parameters like the color, position,
and direction of the light sources in the scene.
○ glMaterial: This function allows developers to set material properties for objects, such
as color and shininess.
2. Shaders:
○ Shaders are mini-programs written by the programmer that define how certain stages in
the pipeline behave. Shaders run at specific stages, such as vertex processing or
fragment processing, and are crucial for customizing how graphics are rendered.
○ The pipeline maps these shader functions onto all elements of the input stream (like
vertices or fragments).
○ GPUs are highly effective at processing large collections of data (such as vertices,
fragments, and pixels) simultaneously, using data-parallel programming. This means
that the same operation can be performed on multiple data elements at once, making
GPUs excellent for graphics rendering tasks.
2. Data-Parallelism:
○ This was initially used for graphics, but researchers began to see that data-parallelism
could be leveraged for more general computational tasks. This led to the concept of
GPGPU (General-Purpose Computing on GPUs), which started using GPUs for scientific
computations.
○ GPGPU refers to using the GPU for general-purpose computation, beyond just rendering
graphics. GPUs were found to be efficient for tasks like simulation and matrix
operations, which were traditionally handled by CPUs.
2. Applications of GPGPU:
○ Examples of early GPGPU uses include simulation of coupled maps (a mathematical
model) and sparse matrix solvers (used in solving systems of equations).
○ Ray tracing for realistic lighting effects is another example, which was first explored
using programmable graphics hardware.
2. Stream Programming:
● CUDA (Compute Unified Device Architecture) is a C-like language developed by NVIDIA for
programming GPUs (Graphics Processing Units). It allows programmers to write programs that
run on GPUs for tasks such as parallel processing and computation.
● CUDA abstracts GPU capabilities to match their performance characteristics while maintaining a
low level of abstraction (close to hardware).
● Note: OpenCL is similar to CUDA but supports GPUs and CPUs from multiple vendors, while
CUDA is specific to NVIDIA GPUs. OpenCL has similar functionality but CUDA is more widely
documented and easier to use in the NVIDIA ecosystem.
● In CUDA, the code is separated into host code (which runs on the CPU) and device code (which
runs on the GPU).
● The GPU runs many threads concurrently, where each thread performs a small part of the
computation. These threads are organized into blocks and grids, allowing easy management of
the computation tasks.
CUDA Memory Model:
● CUDA uses different memory spaces for host (CPU) and device (GPU), which are managed using
memcpy for transferring data between these spaces.
● The GPU device has three main memory types visible to the CUDA kernel:
● __syncthreads() is used to synchronize all threads in a block. It ensures that all threads
have completed a certain task before proceeding.
● Atomic operations allow one thread to update a variable in global memory or shared memory
safely, ensuring no race conditions occur.
● Host/Device Synchronization ensures that the host (CPU) and device (GPU) work together
without causing conflicts in memory access or execution timing.
1D Convolution in CUDA:
● In CUDA, you can implement convolution by breaking it down into smaller tasks that are
processed in parallel by many threads. The example shows how a kernel is defined to process
each output element using threads. Each thread works on a part of the input and writes to the
output.
● In versions of CUDA for convolution, threads are used to load data into shared memory (faster
than global memory) for efficient computation. Synchronization ensures all threads in a block
load their data before processing it.
Let’s break down Version 1 and Version 2 of the 1D convolution in CUDA with simple examples.
Version 1: One Thread per Output Element
Example:
Let’s say we have an input array of size 6 and we want to apply a convolution using a kernel of size 3
(meaning we need to sum each element and its two neighboring elements).
Input: [3, 4, 2, 6, 8, 5]
Kernel: [1, 2, 3]
The result after convolution will be calculated for each element using the formula:
output[i]=input[i−1]+input[i]+input[i+1]\text{output}[i] = \text{input}[i-1] + \text{input}[i] + \text{input}
[i+1]output[i]=input[i−1]+input[i]+input[i+1]
Steps in Version 1:
1. Divide the work: Each thread will handle one output element (i.e., one element of the
output[] array).
○ Each thread gets a unique index to process (like thread 0 for the first output, thread 1
for the second output, etc.).
2. Compute the output: Each thread will take the element it is responsible for and apply the
convolution with its neighbors.
○ And so on.
3. Store results: Each thread writes its result directly into global memory.
cpp
Copy
Host Code:
cpp
Copy
int N = 6;
Input: [3, 4, 2, 6, 8, 5]
Kernel: [1, 2, 3]
Steps in Version 2:
1. Use shared memory: Instead of each thread directly accessing global memory, we first load the
necessary input data into shared memory. This is a faster type of memory that all threads in a
block can access.
○ Thread 1 will load: input[1], input[2], input[3] into shared memory, and so
on.
2. Synchronization: After loading the data into shared memory, all threads wait for each other
using a barrier (__syncthreads()) to make sure all data is loaded before any computation
begins.
3. Compute the output: Each thread will now perform the convolution, but instead of accessing
global memory, it will access the shared memory to get its neighbors.
○ And so on.
4. Store results: Finally, the result is stored back into global memory.
cpp
Copy
support[threadIdx.x] = input[index];
if (threadIdx.x < 2) {
support[THREADS_PER_BLK + threadIdx.x] = input[index +
THREADS_PER_BLK];
Host Code:
cpp
Copy
int N = 6;
Key Differences:
● Version 1: Each thread directly accesses global memory for its data.
● Version 2: Threads first load data into shared memory (which is faster), then use it for the
convolution. Threads synchronize to ensure all data is loaded before computation starts.
CUDA Abstractions:
● Execution: Thread Hierarchy:
○ CUDA allows you to run many threads at once, which are grouped into blocks. This setup
lets you break down tasks into smaller pieces for more efficient parallel execution.
○ The memory is divided between different address spaces. CUDA provides functions
(memcpy) to move data between the host (CPU) and the device (GPU). You can work
with memory at three levels: per thread, per block (shared), and per program (global).
● Barrier Synchronization:
○ Synchronization ensures that threads within a block coordinate their actions. This is
done using a barrier, which ensures that all threads in the block complete their task
before moving on to the next.
● Atomic Operations:
○ These are operations that ensure data consistency when multiple threads try to modify
the same variable. It ensures that the operation completes without interference from
other threads.
CUDA Semantics:
● Thread Definitions and Memory:
○ Each CUDA thread is a unit of execution. It has its own local memory, but it can also
share memory with other threads within the same block.
● Kernel Launch:
○ CUDA kernels (functions that run on the GPU) are launched with a specific configuration.
For example, blockDim.x and threadIdx.x define how the threads are organized
within blocks.
● Host vs. Device Code:
○ The "host" code runs on the CPU, while the "device" (CUDA) code runs on the GPU.
CUDA separates the code that runs on the CPU and GPU.
CUDA Compilation:
● CUDA Compilation Steps:
○ The kernel (CUDA function) is compiled into machine code that can run on the GPU.
During this, the kernel text and resource requirements are defined. Each thread requires
a specific amount of memory.
● Kernel Launch:
○ When launching a kernel, the number of blocks and threads per block is defined. For
example, launch<<<blocks, threads>>> is how you tell CUDA to run the kernel
with a certain number of threads.
○ A thread block is a collection of threads that can cooperate and share data. These blocks
are scheduled by the GPU scheduler. Each block is assigned to a GPU core, and the
blocks can run in parallel.
○ Step 1: The host sends the command to the GPU to run a kernel. The GPU scheduler
starts the process.
○ Step 2: The scheduler maps thread blocks to cores on the GPU. Each block gets allocated
execution contexts and shared memory.
○ Step 3: The scheduler continues mapping remaining blocks across the cores available.
○ Step 4: Once a block completes, the next block is scheduled on the next available core.
○ Step 5: Once all blocks are completed, the results are written back to the global
memory.
NVIDIA GTX 980 (2014) Architecture:
● GPU Cores:
○ The GTX 980 has 16 cores (called SMM cores), each capable of executing multiple CUDA
threads. The cores manage memory (shared and global) and process threads in parallel.
● Warp:
○ A warp is a set of 32 threads that execute the same instruction at the same time. A warp
is managed by the core to ensure efficient execution.
○ The GPU memory is split into different types: global memory, shared memory, and local
memory. CUDA uses shared memory within a block, which is faster and allows threads
within a block to collaborate.
○ CUDA threads are grouped into warps. Warps execute in a single instruction stream, and
this parallel execution helps improve performance.
● Persistent Threads:
○ In some advanced cases, persistent threads are used to keep threads running
continuously, making use of multiple cores to handle tasks dynamically.
● When you want to run a kernel, you have to launch it. Launching a kernel means starting the
task or job on the GPU.
● threadIdx.x: This tells you the index of a thread within its block. Every thread in a block gets its
own ID (index), and this ID helps the thread know what part of the work it is responsible for.
Now, you launch the kernel to tell the GPU to start working on this task. CUDA takes care of distributing
these 10 threads across the available GPU cores. The threads work together within the block and
complete the task.
In summary:
● blockDim.x and threadIdx.x control how many threads are there and how they are arranged to
share the workload.
What is a Warp?
In simple terms, a warp is a group of 32 threads that are treated as a unit and execute the same
instruction at the same time. Imagine you have 32 people working on the same task together at the
same moment. Instead of having each person do their task separately, they all work together in sync to
complete that task.
● Warp: A group of 32 threads working together at the same time on the same task. This is like
having 32 workers in one team, each doing a specific part of a job simultaneously.
● Core: The GPU's core (like a brain of a team) manages the warp. It makes sure that all 32 threads
work together properly to complete the job.
What are Persistent Threads?
● Normal Threads: In typical programs, threads are created to perform a specific task, and once
the task is completed, the thread stops and waits for a new task.
● Persistent Threads: These threads, on the other hand, keep running continuously, even after
finishing their current task. They are always "alive" and ready to take on new tasks whenever
they are assigned one.
North Bridge:
● Role: The North Bridge connects the CPU to the memory (RAM) and also to the video card. It
handles high-speed communication. It is often referred to as the “memory controller hub”
because it manages the data that is sent between the memory and the CPU.
Memory (RAM):
● Role: This is the temporary storage where data that is currently being used by the CPU is kept.
The more memory you have, the more programs you can run at once.
South Bridge:
● Role: The South Bridge is responsible for managing the input and output devices. It controls
communication with the LAN (network), USB devices, hard drives, and other peripherals. It's
called "South" because it connects to the "North Bridge" via the PCI bus.
VGA Display:
● Role: This is the monitor or screen where the computer displays the graphical output. It shows
what you see as images, text, and videos.
Framebuffer Memory:
● Role: This is the memory used by the video card to store the current image or video frame that
is being displayed. It holds the data that represents what is shown on the screen at any given
moment.
○ PCI-Express Link (x16): This is the connection that links the GPU to the North Bridge. It
is the primary interface between the GPU and the rest of the computer system. The
"x16" denotes the speed of the link, which is capable of handling large amounts of data.
○ GPU: The Graphics Processing Unit is responsible for rendering graphics and performing
parallel computations. It has its own memory (GPU Memory) to store data and
instructions used for processing.
○ North Bridge: The North Bridge connects the CPU to high-speed devices like memory
(DDR2) and GPU. It helps manage data traffic between the CPU and the rest of the
system.
○ South Bridge: The South Bridge connects lower-speed peripherals like hard drives, USB
ports, and input/output devices. It also links to the North Bridge.
○ Front Side Bus: It connects the CPU to the North Bridge and transfers data between
them.
○ CPU core: The central processor of the AMD CPU. It handles all processing tasks in the
system.
○ Internal Bus: This connects the CPU core to other components like the North Bridge and
is used for internal data communication within the CPU.
○ PCI-Express Link (x16): Just like in the Intel configuration, the GPU is connected to the
North Bridge using this link.
○ Chipset:
● The Chipset helps control data flow between the North Bridge and the GPU.
3. GPU Memory: Both configurations feature GPU Memory, where the GPU stores its data and
instructions for rendering or parallel computations.
4. Display: The output of the GPU is sent to the display (monitor), where the processed graphics
are shown.
Explanation of Code Components:
1. #include <stdio.h>:
○ This line includes the Standard Input Output (stdio) library, which allows the program to
use functions like printf for printing output.
○ __global__ tells the compiler that this function will run on the GPU.
○ Inside this function, the printf function is used to print "Hello World from GPU!".
○ This line prints the text "Hello World from GPU!" followed by a newline (\n) to the
console.
○ The printf inside the helloFromGPU kernel will run on the GPU, not the CPU.
4. int main(void):
○ This is the main function of the program. This is where the program starts running on
the CPU.
○ The main function includes a printf to print "Hello World from CPU!" and calls the
CUDA kernel to print "Hello World from GPU!" using the GPU.
○ This line prints "Hello World from CPU!" to the console. This is done by the CPU part of
the program before the GPU is invoked.
6. helloFromGPU<<<1, 10>>>();:
○ Each of the 10 threads will run the helloFromGPU kernel and print "Hello World from
GPU!" to the console.
7. cudaDeviceReset();:
○ This function call is used to reset the GPU device and clean up after the kernel
execution.
○ It's good practice to call this function at the end of a CUDA program.
● "Hello World from GPU!" will be printed 10 times by the GPU, as 10 threads are launched to
execute the kernel.
Standard C Functions:
1. malloc:
2. memcpy:
○ Copies data from one memory location to another.
3. memset:
4. free:
2. cudaMemcpy:
○ Copies data between host (CPU) memory and device (GPU) memory.
3. cudaMemset:
4. cudaFree:
Key Components:
1. Host (CPU):
○ The CPU (or host) is the main processor that handles all major computations and control
in your system.
○ The Host side uses the CUDA functions to allocate and transfer memory:
2. Device (GPU):
○ The GPU (device) performs parallel processing tasks, handling computations faster than
the CPU.
■ Shared Memory: Memory shared within the same thread block. It’s fast, but
limited in size.
■ Global Memory: Larger memory, accessible by all threads in the grid, but slower
to access than shared memory.
3. Grid:
○ A grid is a collection of thread blocks. In CUDA, work is divided into blocks and threads.
■ For example, the diagram shows Thread (0,0), Thread (1,0), and Thread
(2,0) within a block.
4. Memory Transfer:
○ cudaMemcpy is the function used to transfer data between Host and GPU. It allows for
data to be copied from the Host’s System Memory to the Global Memory of the GPU or
vice versa.
○ The shared memory within the blocks is used for quick, cooperative computation
among threads in the same block.
Workflow:
1. The Host (CPU) uses cudaMalloc to allocate memory on the GPU.
2. The Host sends data to the GPU using cudaMemcpy, moving it from the Host’s System Memory
to the GPU’s Global Memory.
3. The GPU uses shared memory and global memory to execute tasks in parallel using threads
inside blocks.
4. After computation, the Host can retrieve the results from the GPU’s Global Memory back to the
Host’s System Memory via cudaMemcpy.
Breakdown of the Code for Data Transfer Between CPU and GPU
This example demonstrates the process of transferring data between the CPU (Host) and GPU (Device)
in CUDA. Let's walk through the code and explain each part concisely:
Step-by-Step Explanation
Declaring Pointers:
cpp
Copy
float *a_h, *b_h; // host data
1.
○ a_h and b_h: These pointers hold data on the CPU (Host) side.
○ a_d and b_d: These pointers hold data on the GPU (Device) side.
cpp
Copy
nBytes = N * sizeof(float);
2.
○ nBytes: This calculates the amount of memory required to store N floating-point
numbers (sizeof(float) gives the size of one float in bytes).
○ a_h and b_h are allocated memory on the Host using the standard malloc function.
○ a_d and b_d are allocated memory on the GPU using cudaMalloc, which is the CUDA-
specific function for allocating memory on the GPU.
cpp
Copy
for (i = 0; i < N; i++) a_h[i] = 100.f + i;
3.
○ This loop initializes the host data a_h with values starting from 100 and increments by
1 for each element.
cpp
Copy
cudaMemcpy(a_d, a_h, nBytes, cudaMemcpyHostToDevice);
4.
○ cudaMemcpy(a_d, a_h, nBytes, cudaMemcpyHostToDevice): This function copies data
from the Host (a_h) to the Device (a_d).
cpp
Copy
cudaMemcpy(b_h, b_d, nBytes, cudaMemcpyDeviceToHost);
5.
○ cudaMemcpy(b_h, b_d, nBytes, cudaMemcpyDeviceToHost): This function copies data
from the Device (b_d) to the Host (b_h).
Verification:
cpp
Copy
for (i = 0; i < N; i++) assert(a_h[i] == b_h[i]);
6.
○ This loop checks that the data transferred between the Host and Device is correct. The
assert function ensures that each element in a_h is equal to the corresponding element
in b_h. If any value doesn't match, the program will stop (as the condition fails).
Cleaning Up:
cpp
Copy
free(a_h); free(b_h);
cudaFree(a_d); cudaFree(b_d);
7.
○ free(a_h), free(b_h): Frees the memory allocated for Host data (a_h and b_h).
○ cudaFree(a_d), cudaFree(b_d): Frees the memory allocated for Device data (a_d and
b_d).
Explanation of CUDA Threads (SIMT)
1. Threads: Individual units of execution on the GPU, each working on a small part of the problem.
2. Thread Blocks: Groups of threads that execute together on the same GPU core. They can
synchronize and share data via shared memory.
3. Grid: A collection of thread blocks, representing the entire parallel task to be executed on the
GPU.
4. Shared Memory: Memory shared by all threads in a block for fast data exchange.
Workflow:
● Each Thread Block has threads that work together, sharing data via Shared Memory and
synchronizing their execution.
1. __global__:
○ Callable: It can be called from the host (CPU) and, for devices with compute capability 3
or higher, it can also be called from the device.
2. __device__:
○ Callable: It can only be called from the device, not from the host.
3. __host__:
○ Execution: This function is executed on the host (CPU).
● Must have void return type: A CUDA kernel function cannot return any value, so its return
type must always be void.
● No support for a variable number of arguments: CUDA kernel functions do not support
functions that take a dynamic or variable number of arguments.
● No support for static variables: Static variables, which persist between function calls, are not
allowed inside CUDA kernel functions.
● No support for function pointers: You cannot pass function pointers as arguments to a CUDA
kernel.
● Exhibit asynchronous behavior: CUDA kernels are designed to run asynchronously, meaning the
host does not wait for the device to complete the kernel before continuing its own execution.
Copy code
This is how you launch a kernel (a function that runs on the GPU).
● dB (blockDim) → how many threads in each block (can be 1D, 2D, or 3D).
This <<< >>> syntax is not regular C/C++; it’s a special CUDA extension to tell the GPU how much
parallel work to do.
🔹 Important Note:
If you don’t set a value for one of the dim3 fields, it automatically becomes 1.
🔹 1D (One-Dimensional)
cpp
Copy code
🔹 2D (Two-Dimensional)
cpp
Copy code
“If you don’t set a value for one of the dim3 fields, it automatically becomes 1.”
✅ Simple Example:
cpp
Copy code
kernel<<<grid, block>>>(...);
✅ What it means:
Copy code
grid.x = 2; grid.y = 4;
kernel<<<grid, block>>>(...);
Copy code
dim3 grid(2, 4), block(8, 16);
kernel<<<grid, block>>>(...);
This does the same as above, just a cleaner way to write it.
Copy code
kernel<<<32, 512>>>(...);
● 32 blocks in 1D grid
🔹 gridDim
🔹 blockDim
🔹 blockIdx
🔹 threadIdx
markdown
CopyEdit
Y ↑
—————————————→ X
0 1 2 3
cpp
CopyEdit
scss
You said:
ChatGPT said:
Great question!
No, grid(4, 4) and block(4, 4) are not built-in CUDA commands — they are just variable
initializations using the CUDA-provided dim3 type.
CopyEdit
cpp
CopyEdit
Then you use these variables when launching the CUDA kernel:
cpp
CopyEdit
myKernel<<<grid, block>>>();
🧠 What's dim3?
● dim3 is a special CUDA-defined data type used to specify dimensions (X, Y, Z).
cpp
CopyEdit
struct dim3 {
unsigned int x, y, z;
};
So this is not a command like printf() or malloc() — it’s a constructor call creating a 2D or 3D
size for blocks and grids.
1. First Kernel
cpp
Copy code
a[idx] = 7;
● What's happening: Every thread calculates its unique index and stores the number 7 at that
index.
2. Second Kernel
cpp
Copy code
a[idx] = blockIdx.x;
● What's happening: Each thread writes the block ID to its array location.
● Output: 0 0 0 0 1 1 1 1 2 2 2 2
● Why: All threads in the same block share the same blockIdx.x.
3. Third Kernel
cpp
Copy code
a[idx] = threadIdx.x;
● What's happening: Each thread writes its thread ID within the block.
● Output: 0 1 2 3 0 1 2 3 0 1 2 3
● Why: threadIdx.x starts at 0 for each block and increases per thread.
Copy code
a[idx] = a[idx] + 1;
Copy code
if (idx < N)
a_d[idx] = a_d[idx] + 1;
Copy code
dim3 dimBlock(blocksize);
Quick Example
Let’s say:
● N = 10
● blocksize = 4
Then:
● Threads: 3 * 4 = 12, but only the first 10 are used due to the if (idx < N) check.
z
Tab 2
🔍 Why does it seem there's no difference between atomic and critical?
In your specific example (a simple counter++), both #pragma omp atomic and #pragma omp
critical behave the same — they both ensure thread-safe access and produce correct results.
Copy code
x++;
x += y;
Copy code
#include <stdio.h>
#include <omp.h>
int counter = 0;
int main() {
✅ Slide Title: Unique Thread IDs
This slide explains how CUDA gives each thread a unique global ID so it knows exactly what part of the
data to work on (like an index in an array).
CUDA provides special built-in variables automatically for each thread and block. These are:
● But to access global data (e.g., array index), you need a global thread ID.
ini
CopyEdit
📦 Diagram Explanation:
● So blockDim.x = 5
0 0, 1, 2, 3, 4 0, 1, 2, 3, 4
1 0, 1, 2, 3, 4 5, 6, 7, 8, 9
You will:
reduction Threads work on local copies and combine the results automatically — safe and
efficient.
atomic Only the update to the shared variable is atomic — avoids full locking, low overhead.
critical Only one thread executes the protected section at a time — simple, but slow.
locks Manual locking mechanism — gives full control but expensive and complex.
no sync No protection at all — fast but causes incorrect results due to data races.
Copy code
#include <iostream>
cpp
Array Initialization
cpp
Copy code
int n = 1000000;
cpp
Copy code
std::vector<int> arr(n);
cpp
Copy code
int sum = 0;
● A shared variable used to store the final sum result in each method.
Copy code
srand(time(0));
● Seeds the random number generator with the current system time.
cpp
Copy code
for (int i = 0; i < n; ++i) {
Copy code
cpp
Copy code
int serial_sum = 0;
serial_sum += arr[i];
cpp
Copy code
● Ends the timer and prints the sum and time taken.
🧵 Parallel Versions
1. 🔁 Reduction
cpp
Copy code
sum = 0;
start_time = omp_get_wtime();
sum += arr[i];
end_time = omp_get_wtime();
● What it does: Each thread computes a local sum; OpenMP adds them together at the end.
● Safe and efficient because OpenMP handles data separation and combining.
2. ⚛️Atomic Operation
cpp
Copy code
sum = 0;
start_time = omp_get_wtime();
#pragma omp parallel for
sum += arr[i];
end_time = omp_get_wtime();
● What it does: The #pragma omp atomic makes sure sum += arr[i] happens
atomically (one at a time).
● Faster than critical, but still slower than reduction due to overhead from many atomic
operations.
3. 🚧 Critical Section
cpp
Copy code
sum = 0;
start_time = omp_get_wtime();
sum += arr[i];
end_time = omp_get_wtime();
● What it does: Only one thread can enter the critical block at a time.
Copy code
sum = 0;
omp_lock_t lock;
omp_init_lock(&lock);
start_time = omp_get_wtime();
omp_set_lock(&lock);
sum += arr[i];
omp_unset_lock(&lock);
end_time = omp_get_wtime();
omp_destroy_lock(&lock);
● What it does: Like critical, but you manually lock and unlock around the shared variable.
● More flexible but also higher overhead and risk of forgetting to unlock (leading to deadlocks).
Copy code
sum = 0;
start_time = omp_get_wtime();
end_time = omp_get_wtime();
📤 Output Statements
cpp
Copy code
std::cout << "Parallel Sum with <Technique>: " << sum << std::endl;
Atomic ✅ Moderate ⚠️ Safe; less overhead than critical/locks, but slower than
reduction.
✅ 1. Reduction
Description:
OpenMP automatically creates private copies of the variable for each thread, performs the operation
locally, and combines the results at the end of the parallel region.
✔️Advantages:
❌ Drawbacks:
● Limited to scalar variables, not applicable to arrays or complex logic inside loops.
✅ 2. Atomic
Description:
#pragma omp atomic ensures a single memory operation (like sum += arr[i]) is done
without interference.
✔️Advantages:
❌ Drawbacks:
● Only works for simple, one-liner operations (e.g., increment, add, subtract).
● Not suitable for compound operations (e.g., modifying multiple variables atomically).
✅ 3. Critical
Description:
A critical section ensures only one thread at a time can execute the protected code block.
✔️Advantages:
● Very general-purpose — can protect complex logic, multiple operations, or large blocks.
❌ Drawbacks:
● Slower than atomic and reduction — threads must wait to enter the critical section.
✅ 4. Locks
Description:
Locks (omp_lock_t) provide manual control over synchronization by explicitly locking and unlocking
access to a critical section.
✔️Advantages:
● Highly flexible — can protect any region of code.
❌ Drawbacks:
● Slower than all other methods for simple tasks like summing.
❌ 5. No Synchronization
Description:
Threads update the shared variable without any protection — leads to data races.
✔️Advantages:
❌ Drawbacks:
Code Line:
cpp
Copy code
srand(time(0));
🔍 What does it do?
This line initializes the random number generator in C++ so that the numbers you get from rand() are
different each time you run the program.
READING ASSIGNMENT:
🧩 Part 1: Task-Level Parallelism with Dependencies in OpenMP
Task-level parallelism in OpenMP allows the programmer to define independent or dependent tasks that
are executed in parallel. OpenMP schedules these tasks among the available threads, and you can
control dependencies between them using the depend clause.
Copy
Edit
#include <stdio.h>
#include <omp.h>
printf("Task 1 started\n");
*result = 10;
printf("Task 1 finished\n");
*result = 20;
printf("Task 2 finished\n");
printf("Task 3 started\n");
int main() {
// Task 1 creation
task1(&result1);
// Task 2 creation
task2(&result2);
// Task 3 depends on both Task 1 and Task 2
task3(result1, result2);
return 0;
🧠 Line-by-Line Explanation:
omp_set_num_threads(4);
→ Ensures that only one thread creates the tasks inside the block.
→ Declares Task 1, which produces result1. This task must complete before any task that uses result1 as
input can begin.
→ Declares Task 3, which depends on both result1 and result2. It will wait until Task 1 and Task 2 finish.
The loop inside each task is just a dummy operation to simulate time-consuming work.
📌 Important Concepts:
Feature Explanation
depend(in: var) This task reads from var and must wait for any previous out on var to finish
#pragma omp single Only one thread generates the tasks; tasks themselves can run in parallel
✅ Definition: Barrier
A barrier is a point in the program where all threads must stop and wait until every thread has reached
the same point. It's useful to ensure one phase of work is complete before the next starts.
#include <stdio.h>
#include <omp.h>
void task1() {
void task2() {
}
int main() {
return 0;
🧠 Line-by-Line Explanation:
omp_set_num_threads(4);
task1();
→ Barrier point. All threads must finish Task 1 and reach this point before any can proceed.
task2();
Example: Each thread writes to a section of an array; we want to sort the array after all threads are done
writing.
✅ Output Behavior:
But in OpenMP task-based parallelism, you want only one thread to create the tasks, not all threads.
That’s where #pragma omp single is used.
✅ Simple Explanation:
c
CopyEdit
{
#pragma omp single
● ✔️One thread (any one of the available threads) will enter the single block.
● ❌ The other threads will wait (unless you say not to) or do nothing there.
● ✅ Inside this single block, you can define tasks using #pragma omp task.
If all threads create the same tasks, you'll get duplicate tasks — that's wrong.
CopyEdit
do_something();
Then OpenMP can run the task in any available thread (not just the one that created it).
CopyEdit
#include <stdio.h>
#include <omp.h>
int main() {
return 0;
✅ Output Example:
arduino
CopyEdit
Task B by thread 3
● "Thread 2 is creating tasks" → Only one thread printed this (due to single).
● Task A and Task B ran on different threads — this is parallel task execution.
● Cluster Computing:
○ They are connected through LAN using MPI (Message Passing Interface).
○ Two types:
2. Distributed Memory (Multicomputer): Each processor has its own local memory.
○ These computers communicate over a network but operate independently (i.e., different
functions).
🧠 Example: A university lab where each computer does different tasks but shares results over a network.
Every computer in cluster needs to have a software in order to communicate with each other and that software
is called LAM(Local AArea multicomputers ) MPI .
● Grid: A more complex setup that connects various clusters over the internet.
○ Proper configuration.
○ Ability to talk with others (via LAM - Local Area Multicomputer).
○ Example: mpirun [Link] -np 10 means running the [Link] program on 10 processors.
● TCP/UDP: These are networking protocols, but as developers, you don’t have to worry about their
technical setup here.
● SPMD: A programming model where the same program runs on multiple computers, but each handles
different data.
🧠 Example: Sorting a huge list - each processor sorts a part of the list.
🔹 MPI Basics
🧠 Example: Master processor gives each student a question to solve, collects answers at the end.
🔹 Broadcast in MPI
○ Whole group
○ Sub-groups
○ Individual processes
● Reduce Operation: Combines values from all processes into one (e.g., sum, max).
🧠 Example:
● Scan: Each student knows the total marks of all students before them.