Παράλληλη Επεξεργασία — Ενδεικτικά θέματα (ΛΠΣΥΕ, Εαρινό 2025-26)¶
Μεταγραφή από το αρχείο eClass «ΛΠΣΥΕ Ενδεικτικά Θέματα.pdf» (σελίδες 1–4). Το φύλλο δεν έχει ημερομηνία εξέτασης.
Ερωτήσεις 1–5¶
1. Η μέγιστη ονομαστική απόδοση ενός υπολογιστικού συστήματος μπορεί να διπλασιαστεί όταν:
- [ ] a. διπλασιάσουμε το μέγεθος της κρυφής μνήμης
- [ ] b. υποδιπλασιάσουμε τον αριθμό των πυρήνων
- [ ] c. υποδιπλασιάσουμε την ακρίβεια των πράξεων κινητής υποδιαστολής (double σε float)
- [ ] d. τίποτα από τα υπόλοιπα
2. Εάν το 20% ενός κώδικα δεν μπορεί να παραλληλοποιηθεί τότε η μέγιστη δυνατή χρονοβελτίωση είναι:
- [ ] a. 2
- [ ] b. 5
- [ ] c. 10
- [ ] d. τίποτα από τα υπόλοιπα
3. Ένα υπολογιστικό σύστημα διαθέτει 4 πυρήνες που λειτουργούν σε συχνότητα 2.1 GHz. Κάθε πυρήνας υποστηρίζει διανυσματικές εντολές (SIMD) μεγέθους 8. Στις εντολές αυτές περιλαμβάνεται και η εντολή “fused-multiply-add” (FMA) (δηλ. μία εντολή που εκτελεί την πράξη a := b*c + d). Επίσης, κάθε εντολή (απλή ή SIMD) ολοκληρώνεται σε ένα κύκλο ρολογιού. Ποια είναι η μέγιστη ονομαστική απόδοση (σε GFLOP/s) του συστήματος;
4. Ποια θα είναι η τιμή της μεταβλητής test_var μετά την εκτέλεση της παράλληλης περιοχής στον παρακάτω OpenMP κώδικα;
int test_var = 1;
#pragma omp parallel num_threads(4) reduction(*:test_var)
{
test_var = omp_get_thread_num() - omp_get_num_threads();
}
- [ ] a. τίποτα από τα παρακάτω
- [ ] b. 1
- [ ] c. -10
- [ ] d. 12
5. Στον παρακάτω κώδικα OpenMP και συγκεκριμένα στο σημείο αναφοράς εντός του task, η μεταβλητή b είναι:
int main(int argc, char *argv[]) {
int b,c ;
#pragma omp parallel shared(c) private(b)
{
int d ;
#pragma omp task
{
int e ;
// σημείο αναφοράς εντός του task
}
}
// more code here
return 0;
}
- [ ] a. shared
- [ ] b. private
- [ ] c. threadprivate
- [ ] d. firstprivate
Ερωτήσεις 6–10¶
6. Ποιες θα είναι οι τελικές τιμές του διανύσματος Α μετά την εκτέλεση του παρακάτω OpenMP κώδικα; Εξηγήστε.
int Α[12];
omp_set_num_threads(4);
#pragma omp parallel for schedule(static,3)
for (int i = 0; i < 12; i++) {
Α[i] = omp_get_thread_num();
}
7. Ποια τιμή θα εκτυπωθεί όταν ο παρακάτω κώδικας MPI εκτελεστεί με 4 διεργασίες (mpirun -n 4
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
double var1 = rank+1.0;
double var2 = 0.0, var3 = 0.0;
MPI_Reduce(&var1,&var2,1, MPI_DOUBLE, MPI_SUM,0, MPI_COMM_WORLD);
MPI_Bcast(&var2, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
MPI_Allreduce(&var2, &var3, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
if (rank == 1) printf("%f\n", var3);
8. Ποιο μήνυμα θα εκτυπώσει η διεργασία με rank = 2 όταν ο παρακάτω κώδικας MPI εκτελεστεί με 4 διεργασίες;
#include <mpi.h>
#include <stdio.h>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
int input = rank + 2; // Each process has a value: rank + 2
int output;
MPI_Scan(&input, &output, 1, MPI_INT, MPI_PROD, MPI_COMM_WORLD);
printf("Process %d: output = %d\n", rank, output);
MPI_Finalize();
return 0;
}
- [ ] a. 6
- [ ] b. 9
- [ ] c. 24
- [ ] d. 41
9. Ποια από τα παρακάτω είναι αληθές για τις συλλογικές (collective) λειτουργίες Ι/Ο του MPI;
- [ ] a. Όλες οι διεργασίες απαιτείται να συμμετέχουν στην λειτουργία I/O.
- [ ] b. Είναι πιο αποδοτικές από τις ανεξάρτητες λειτουργίες Ι/Ο.
- [ ] c. Εκτελούνται με συναρτήσεις όπως οι MPI_File_read_all() και MPI_File_write_all().
- [ ] d. Όλα τα παραπάνω.
10. Ποια από τις παρακάτω προτάσεις είναι σωστή σχετικά με το παράλληλο I/O με MPI;
- [ ] a. Το MPI υποστηρίζει μόνο συλλογικές λειτουργίες I/O, όπου όλες οι διεργασίες πρέπει να συμμετέχουν στην ίδια λειτουργία ανάγνωσης ή εγγραφής.
- [ ] b. Το MPI υποστηρίζει τόσο συλλογικές όσο και ατομικές λειτουργίες I/O, επιτρέποντας μεγαλύτερη ευελιξία στη διαχείριση αρχείων.
- [ ] c. Το MPI δεν υποστηρίζει καθόλου λειτουργίες I/O, και οι προγραμματιστές πρέπει να χρησιμοποιούν εξωτερικές βιβλιοθήκες για ανάγνωση και εγγραφή αρχείων.
- [ ] d. Το MPI υποστηρίζει μόνο ατομικές λειτουργίες I/O, όπου κάθε διεργασία διαχειρίζεται τα δικά της αρχεία ανεξάρτητα από τις άλλες διεργασίες.
Ερωτήσεις 11–17¶
11. Ποιο από τα παρακάτω είναι αληθές σχετικά με τα SSE και AVX;
- [ ] a. To SSE λειτουργεί με καταχωρητές 128-bits, ενώ το AVX λειτουργεί με καταχωρητές 256-bits.
- [ ] b. To AVX είναι υποσύνολο του SSE.
- [ ] c. Το SSE υποστηρίζει πράξεις κινητής υποδιαστολής ενώ το AVX όχι.
- [ ] d. Το AVX δεν είναι διαθέσιμο σε επεξεργαστές της Intel.
12. Ποια συνάρτηση θα χρησιμοποιούσατε για να φορτώσετε 4 αριθμούς κινητής υποδιαστολής απλής ακρίβειας σε έναν καταχωρητή SSE;
- [ ] a. _mm_load_ps()
- [ ] b. _mm_load_pd()
- [ ] c. _mm256_load_ps()
- [ ] d. _mm256_load_pd()
13. Τι κάνει το παρακάτω τμήμα κώδικα με τις εντολές AVX;
__m256 a = _mm256_load_ps(array1);
__m256 b = _mm256_load_ps(array2);
__m256 c = _mm256_add_ps(a, b);
- [ ] a. Προσθέτει τα στοιχεία δύο διανυσμάτων με 4 floats το καθένα
- [ ] b. Προσθέτει τα στοιχεία δύο διανυσμάτων με 8 floats το καθένα
- [ ] c. Προσθέτει τα στοιχεία δύο διανυσμάτων με 8 doubles το καθένα
- [ ] d. Προσθέτει τα στοιχεία δύο διανυσμάτων με 8 integers το καθένα
14. Ποιο από τα παρακάτω είναι αληθές σχετικά με τις απαιτήσεις στοίχισης για εντολές SIMD;
- [ ] a. Το SSE απαιτεί στοίχιση σε 16 bytes για τη συνάρτηση _mm_load_ps().
- [ ] b. Το AVX απαιτεί στοίχιση σε 32 bytes για τη συνάρτηση _mm256_load_ps().
- [ ] c. Η πρόσβαση σε μη στοιχισμένη μνήμη μπορεί να οδηγήσει σε σφάλματα ή χαμηλή απόδοση.
- [ ] d. Όλα τα παραπάνω.
15. Αναφέρετε και διορθώστε πιθανά σφάλματα τον παρακάτω πολυνηματικό κώδικα με εντολές SSE.
int nThreads = 16;
float*data;
posix_memalign((void **)&data, 16, 32*sizeof(float)); // allocate 16-Byte aligned memory for 32 float values
for (int i=0; i<32; ++i) data[i] = drand48();
omp_set_num_threads(nThreads);
#pragma omp parallel
{
float * const myData = data + 2*omp_get_thread_num();
__m128 c4 = _mm_set1_ps(M_PI); // = 3.141592…
__m128 r4 = _mm_load_ps(myData);
__m128 a4 = _mm_mul_ps(c4, _mm_mul_ps(r4, r4));
_mm_store_ps(myData, a4);
}
16. Ποια είναι η σωστή σύνταξη για να εκτελεστεί μια συνάρτηση CUDA με 256 νήματα ανά μπλοκ και 10 μπλοκς;
- [ ] a. kernel<<<10, 256>>>();
- [ ] b. kernel<<<256, 10>>>();
- [ ] c. kernel<<<10, 256, 0>>>();
- [ ] d. kernel<<<256, 10, 0>>>();
17. Ποιος είναι ο σκοπός της χρήση του __global__ στην CUDA;
- [ ] a. Καθορίζει μια συνάρτηση που τρέχει στον κύριο σύστημα (host) αλλά μπορεί να κληθεί από την κάρτα γραφικών.
- [ ] b. Καθορίζει μια συνάρτηση που τρέχει στην κάρτα γραφικών και μπορεί να κληθεί από το κύριο σύστημα (host).
- [ ] c. Καθορίζει μια συνάρτηση που τρέχεις στην κάρτα γραφικών και μπορεί να κληθεί μόνο από την κάρτα γραφικών.
- [ ] d. Καθορίζει μια συνάρτηση που τρέχει στον επεξεργαστή (host) και μπορεί να κληθεί μόνο από τον επεξεργαστή (host).
Ερωτήσεις 18–21¶
18. Ποια συνάρτηση CUDA χρησιμοποιείται για να αντιγράψει δεδομένα από το κύριο σύστημα (host) στην κάρτα γραφικών;
- [ ] a. cudaMemcpyToHost()
- [ ] b. cudaMemcpyToDevice()
- [ ] c. cudaMemcpy() με cudaMemcpyHostToDevice
- [ ] d. cudaMemcpy() με cudaMemcpyDeviceToHost
19. Ποιος είναι ο σκοπός χρήσης της συνάρτησης __syncthreads() σε έναν υπολογιστικό πυρήνα CUDA;
- [ ] a. Συγχρονίζει όλα τα νήματα σε ένα πλέγμα (grid)
- [ ] b. Συγχρονίζει όλα τα νήματα σε ένα μπλοκ
- [ ] c. Συγχρονίζει όλα τα νήματα σε ένα warp
- [ ] d. Συγχρονίζει το κύριο σύστημα και την κάρτα γραφικών
20. Ποια είναι η έξοδος από την εκτέλεση του παρακάτω κώδικα CUDA;
__global__ void kernel(int *a) {
int tid = threadIdx.x;
a[tid] = tid * 2;
}
int main() {
int h_a[4], *d_a;
cudaMalloc(&d_a, 4 * sizeof(int));
kernel<<<1, 4>>>(d_a);
cudaMemcpy(h_a, d_a, 4*sizeof(int), cudaMemcpyDeviceToHost);
printf("%d %d %d %d", h_a[0], h_a[1], h_a[2], h_a[3]);
cudaFree(d_a);
return 0;
}
- [ ] a. 0 1 2 3
- [ ] b. 0 2 4 6
- [ ] c. 0 0 0 0
- [ ] d. 1 2 3 4
21. Αναφέρετε τα σφάλματα στον παρακάτω κώδικα και προτείνετε μια λύση.
__global__ void kernel(int *array)
{
// do something
}
int main()
{
int * b;
cudaMalloc(&b, 512*sizeof(int));
for (int i=0; i<512; i++) b[i] = i;
kernel<<< 16, 32 >>>(b);
cudaFree(b);
return 0;
}
Λύσεις (⚪ AI, μη επαληθευμένο)
Κατάσταση: ⚪ AI, μη επαληθευμένο Πηγή: λύσεις AI (deepseek-flash), ανεξάρτητη επανάληψη και αντιπαραβολή· καμία δεν έχει ελεγχθεί από άνθρωπο
Προσοχή
Λύση από AI — ελέγξτε την· δεν έχει επαληθευτεί από άνθρωπο. Η ένδειξη «δύο λύσεις συμφωνούν» δεν σημαίνει ότι η απάντηση είναι σωστή.
AI λύσεις — 2025-26-endeiktika¶
Οι λύσεις γράφτηκαν από μοντέλα AI και δεν έχουν ελεγχθεί από άνθρωπο. Η αρίθμηση ακολουθεί το αρχείο θεμάτων.
Θέμα Ενδεικτικά θέματα (ΛΠΣΥΕ, Εαρινό 2025-26)¶
Αξιοπιστία: μία λύση AI, δεν έχει αντιπαραβληθεί με δεύτερη · εμπιστοσύνη λύτη: υψηλή
-
c. υποδιπλασιάσουμε την ακρίβεια των πράξεων κινητής υποδιαστολής (double σε float).
Με float χωρούν διπλάσια στοιχεία ανά διανυσματικό καταχωρητή σε σχέση με double, άρα μπορεί να διπλασιαστεί η ονομαστική κορυφαία απόδοση. -
b. 5.
Από τον νόμο Amdahl: \(S_{\max}=\frac{1}{0.20}=5\), αφού το 20\% είναι μη παραλληλοποιήσιμο. -
Η μέγιστη ονομαστική απόδοση είναι
[ 4 \times 2.1\times 10^9 \times 8 \times 2 = 134.4\ \text{GFLOP/s}. ]
Εδώ το FMA μετράει ως 2 πράξεις κινητής υποδιαστολής ανά στοιχείο. Άρα 134.4 GFLOP/s. -
a. τίποτα από τα παρακάτω.
Με 4 νήματα οι ιδιωτικές τιμές είναι \(-4,-3,-2,-1\). Το γινόμενο είναι
[ (-4)(-3)(-2)(-1)=24, ] το οποίο δεν υπάρχει στις επιλογές. -
d. firstprivate.
Ηbείναι private στην παράλληλη περιοχή, αλλά μέσα στο task λαμβάνεται ως firstprivate εξ ορισμού, δηλαδή αντιγράφεται η τιμή της κατά τη δημιουργία του task. -
Με
schedule(static,3)και 12 επαναλήψεις δημιουργούνται 4 chunks των 3 επαναλήψεων, ένα για κάθε νήμα: - νήμα 0: \(i=0,1,2\)
- νήμα 1: \(i=3,4,5\)
- νήμα 2: \(i=6,7,8\)
- νήμα 3: \(i=9,10,11\)
Άρα: [ A={0,0,0,\;1,1,1,\;2,2,2,\;3,3,3}. ]
-
Αρχικά
var1 = rank+1, άρα για ranks \(0,1,2,3\) οι τιμές είναι \(1,2,3,4\).
ΤοMPI_ReduceμεMPI_SUMστη ρίζα 0 δίνειvar2 = 10στη rank 0.
ΤοMPI_Bcastμεταδίδει τοvar2 = 10σε όλες τις διεργασίες.
ΤοMPI_AllreduceμεMPI_SUMαθροίζει τέσσερα 10:
[ var3 = 10+10+10+10 = 40. ]
Η rank 1 εκτυπώνει:40.000000. -
c. 24.
ΤοMPI_ScanμεMPI_PRODείναι inclusive prefix γινόμενο. Για rank 2: [ (0+2)(1+2)(2+2)=2\cdot 3\cdot 4=24. ] -
d. Όλα τα παραπάνω.
Οι συλλογικές I/O απαιτούν συμμετοχή όλων των διεργασιών, μπορούν να είναι πιο αποδοτικές και υλοποιούνται με συναρτήσεις όπωςMPI_File_read_all()καιMPI_File_write_all(). -
b. Το MPI υποστηρίζει τόσο συλλογικές όσο και ατομικές/ανεξάρτητες λειτουργίες I/O.
-
a. Το SSE χρησιμοποιεί καταχωρητές 128 bits, ενώ το AVX 256 bits.
-
a.
_mm_load_ps().
Φορτώνει 4 αριθμούςfloatαπλής ακρίβειας σε καταχωρητή SSE 128 bits. -
b. Προσθέτει τα στοιχεία δύο διανυσμάτων με 8 floats το καθένα.
Το_mm256_load_psκαι το_mm256_add_psδουλεύουν με 8 single-precision floats. -
d. Όλα τα παραπάνω.
Το_mm_load_ps()απαιτεί στοίχιση 16 bytes, το_mm256_load_ps()32 bytes, και η μη στοιχισμένη πρόσβαση μπορεί να προκαλέσει σφάλμα ή επιβράδυνση. -
Σφάλματα:
- Το
_mm_load_ps()/_mm_store_ps()απαιτούν διεύθυνση στοιχισμένη στα 16 bytes. Μεdata + 2*tid, για περιττάtidη διεύθυνση δεν είναι 16-byte aligned. - Κάθε νήμα γράφει 4 floats, άρα με 16 νήματα και βήμα 2 δημιουργούνται επικαλύψεις και data races.
- Το τελευταίο νήμα (
tid=15) προσπελαύνει τα στοιχεία 30..33, δηλαδή εκτός ορίων του πίνακα των 32 floats. - Δεν ελέγχεται η επιστροφή της
posix_memalign, και δεν αποδεσμεύεται η μνήμη.
Διόρθωση: να χρησιμοποιηθούν 8 νήματα, ώστε κάθε νήμα να επεξεργάζεται ένα διανυσματικό chunk των 4 floats:
int nThreads = 8; float *data; if (posix_memalign((void **)&data, 16, 32*sizeof(float)) != 0) return 1; for (int i = 0; i < 32; ++i) data[i] = drand48(); omp_set_num_threads(nThreads); #pragma omp parallel { int tid = omp_get_thread_num(); float * const myData = data + 4*tid; __m128 c4 = _mm_set1_ps((float)M_PI); __m128 r4 = _mm_load_ps(myData); __m128 a4 = _mm_mul_ps(c4, _mm_mul_ps(r4, r4)); _mm_store_ps(myData, a4); } free(data); - Το
-
a.
kernel<<<10, 256>>>();
Η σύνταξη είναιkernel<<<αριθμός_μπλοκ, νήματα_ανά_μπλοκ>>>(...).
Τυπικά είναι επίσης συντακτικά έγκυρη και η c.kernel<<<10, 256, 0>>>();, αφού το τρίτο όρισμα 0 δηλώνει 0 bytes dynamic shared memory· η αναμενόμενη απάντηση όμως είναι η a. -
b. Το
__global__ορίζει kernel που εκτελείται στη GPU και καλείται από τον host. -
c.
cudaMemcpy()μεcudaMemcpyHostToDevice.
Αντιγράφει δεδομένα από host σε device. -
b. Συγχρονίζει όλα τα νήματα σε ένα μπλοκ.
Δεν συγχρονίζει ολόκληρο grid ούτε host με device. -
b.
0 2 4 6.
ΜεthreadIdx.x = 0,1,2,3, γράφεταιa[tid] = tid*2. -
Σφάλματα:
- Το
bείναι δείκτης σε device memory μετά τοcudaMalloc, αλλά ο κώδικας γράφειb[i] = iαπό τον host. Αυτό είναι μη έγκυρη προσπέλαση device μνήμης από host. - Δεν γίνεται
cudaMemcpyαπό host σε device πριν τον kernel. - Δεν ελέγχονται σφάλματα των CUDA κλήσεων.
- Αν τα αποτελέσματα του kernel χρειάζονται στον host, λείπει συγχρονισμός/αντιγραφή πίσω.
- Αν ο kernel χρησιμοποιεί μόνο
threadIdx.x, με 16 μπλοκ θα επαναλαμβάνονται τα ίδια indices· χρειάζεται global indexblockIdx.x*blockDim.x + threadIdx.x.
Ενδεικτική διόρθωση:
__global__ void kernel(int *array) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx < 512) { // do something, π.χ. array[idx] = idx; } } int main() { int h_b[512]; int *d_b; for (int i = 0; i < 512; i++) h_b[i] = i; cudaMalloc((void **)&d_b, 512*sizeof(int)); cudaMemcpy(d_b, h_b, 512*sizeof(int), cudaMemcpyHostToDevice); kernel<<<16, 32>>>(d_b); cudaDeviceSynchronize(); // Αν χρειάζονται τα αποτελέσματα πίσω στον host: cudaMemcpy(h_b, d_b, 512*sizeof(int), cudaMemcpyDeviceToHost); cudaFree(d_b); return 0; } - Το