Non-sequential Computing
doc. Ing. Petr Sosík, Dr.
Non-sequential Computing
Info
Období
zima 2023

The course introduces elementary concepts of parallel programming, the parallel computing model PRAM and a basic set of platform-independent parallel programming techniques. The second part of the course is devoted to practical multi-thread programming, including individual projects.


Kapitola obsahuje:
11
Odevzdávárna
1
Web

Introduction to the course

Grading policy

A series of theoretical and programming assignments (mini-projects - MP's): Each MP must be passed with at least 50% points.

Final grading: 50% - 59% E, 60% - 69% D, 70% - 79% C, 80% - 89% B, 90% -100% A.

Monographs and web resources
  • ATTIYA, H., WELCH, J., Distributed Computing: Fundamentals, Simulations and Advanced Topics, J. Wiley & Sons, 2004.
  • CORMEN, T.H., LEISERSON, C.A., RIVEST, R.L., STEIN, C. Introduction to Algorithms, 3rd Edition, MIT Press, Cambridge, MA, 2009.
  • GRAHAM, R.L., KNUTH, D.E., PATASHNIK, O.: Concrete mathematics. Addison-Wesley, 1992.
  • JA'JA, J., An Introduction to Parallel Algorithms, Addison-Wesley, Reading, Mass., 1992.
  • Microsoft Docs: Threading (C#) (online)

Part 1 - parallel programming techniques

Syllabus
  1. Review of formula manipulation: sequences, sums, logarithms
  2. Elements of the complexity theory, the O notation. 
  3. Paradigm of parallel computing. The model PRAM. Performance of parallel algorithms. 
  4. Brent's Lemma, the WT Scheduling Principle. The Parallel Computation Thesis. 
  5. Parallelization: balanced trees. Algorithm analysis - the master method.
  6. Parallelization: divide and conquer. 
  7. Parallelization: partitioning. 
  8. Parallelization: accelerated cascading.
  9. Parallelization: pipelining.



Assignment: logarithms and asymptotics
Exercises will be distributed during our lessons or by email.
Assignment: Vector-matrix product
Ex. 2 at p. 11 of the coursebook


Assignment: Master method
Exercise 4.5-1 in Cormen
Solutions to Exercise 4.5-1
Assignment: Prefix Sums in C#
Implement the parallel recursive algorithm 3.1 for prefix sum in our textbook. Submit a zipped source code + executable code for .net 7.0 or higher. The application should allow to enter input data and then print out the result.
Algorithm structure for the Array packing Problem


Assignment: Prefix sum by divide-and-conquer
Exercise 3 in Section 4.3 of our coursebook
Assignment: Sample Sort
Implement the sorting method SampleSort described in Exercise 3 in Section 5.2 of our coursebook. Elements of the input array must be separated into the buckets in parallel. In C#, use the Parallel.For method, and the recommended class to implement buckets is the ConcurrentBag, https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1?view=net-7.0, which allows concurrent addition of elements from multiple threads. Then you can sort all buckets in parallel by the method ToImmutableSortedSet, Finally, use the method Concat to catenate the sorted buckets.
Assignment: Accelerated Cascading
Ex.1 in Section 7.4 of our coursebook. Start with an input array of size n=1024, fill it with random integers in range 0-1000 and print it. Then run the binary tree algorithm for ⌈log log log n⌉=2 steps (implement this formula to calculate the number of steps). Obtaining an array reduced to 256 elements, construct the doubly-logarithmic tree and apply the algorithm finding maximum in it. Note that you need also to implement the algorithm 7.1 as a method which computes constant-time maximum in each node of the tree with more than two children.
Assignment: Graphical example of simple parallel insertion into 2-3 tree
Insert, in parallel, sequence (5, 15, 25, 35, 45, 55, 65, 75, 85) into the tree at Fig. 6.4 (d) in our coursebook following the algorithm in Sec. 6.2. Draw on paper parallel balancing steps of the tree similarly as Fig. 6.4 (a) - (d). Upload the scan or photo.
Assignment: Simplified array insertion into 2-3 tree
Naprogramujte paralelní vkládání sekvence klíčů B do 2-3 stromu za zjednodušeného předpokladu, že mezi dva stávající listy stromu nikdy nepadne více než jeden z nich. K dispozici máte vzorový kód už s hotovými některými metodami. nestačil jsem ho odladit, tak se nedivte, kdyby něco nefungovalo :-). Odevzdává se (v zazipovaném archivu) zdrojový kód + výpis ukázky funkce: * výpis stromu po ukončení fáze 1 (v textové podobě - pomocí ToString) * vkládané pole B * výpis stromu po ukončení fáze 2 a celkovém vyvážení
Assignment: General array insertion into 2-3 tree
Odevzdává se zdrojový kód + výpis ukázky funkce: výpis původního stromu (v textové podobě), vkládané pole B, a výpis jednotlivých fází přidávání uzlů a postupného vyvažování. Pro vytváření/rušení vláken doporučuji použít Parallel.ForEach anebo thread pool.
Part 1 - parallel programming techniques Solutions to Exercise 4.5-1

Solutions to Exercise 4.5-1

Obsah není zveřejněný.
Part 1 - parallel programming techniques Algorithm structure for the Array packing Problem

Algorithm structure for the Array packing Problem

Obsah není zveřejněný.

Template code for simplified insertion into 2-3 tree

 Vaším úkolem je:

  • Doplnit do metody Insert zamykání, viz komentář v kódu. Pokud se více vláken pokouší přidat syna k témuž rodiči, vyhrává to, které jej nejdřív zamkne. Ostatní musejí počkat, než je rodič opět odemčen. V tento okamžik ale musejí znovu ověřit, zda se nezměnil (díky vyvážení) rodič uzlu, za který (= vpravo od kterého) připojují nového syna. Proto je nutno Monitor.TryEnter volat v cyklu.
  • Napsat metodu Balance, která vyváží rodiče po přidání nového potomka. Rodič se musí zamknout. Pokud nemá víc než 3 potomky, stačí aktualizovat jeho člen <key>, protože se mohl přidáním potomka změnit. Pokud má více než 3 potomky, tak se musí vyvážit - rozdělí na dva rodiče (druhého ihned po vytvoření zamknout) a přerozdělit mezi ně potomky. Pak oba rodiče odemknout. A pokračovat (například rekurzívně) s týmiž operacemi  na dalších uzlech na cestě vzhůru ke kořeni. Uvědomte si, že i když nedošlo k vyvažování, můžou uzly na cestě ke kořeni potřebovat aktualizaci <key>.

Až to budete mít hotové tak to otestujte následovně:

  1. Nejméně 10x zavolejte metodu Insert s různými hodnotami klíče, tím vytvoříte základní strom.
  2. Pak vytvořte pole B s nejméně 6 prvky splňující podmínku, že mezi dva stávající listy stromu nikdy nepadne více než jeden z nich, a v cyklu Parallel.ForEach zavolejte metodu Insert pro všechny prvky pole.

Unlike the example of simple parallel insertion in our textbook, here we do not need to add all new leaves to an existing node of the 2-3 tree at once, but each thread may compete for lock of the parent node to add its leaf and eventually balance the parent if needed. See the example of three new leaves B1, B2, B3 being added to the tree:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace _23treeNew
{
    // Třída Node obsahuje základní metody pro práci s uzelm 2-3 stromu
    class Node
    {
        List children;                        // privátní seznam potomků 
        public Node parent { get; private set; }    // rodičovský uzel
        public int key { get; private set; }        // max. klíč v podstromu tohoto uzlu

        /* 
        Neudržujeme hodnoty l,m,r přímo v uzlu.
        Získáme je jako children[0].key, children[1].key, children[2].key
        */

        public Node(int value)
        {
            key = value;
            children = new List();
        }

        public int NoOfChildren => children.Count;

        // Vrátí potomka na pozici idx, počítáno od nuly
        // Případnou výjimku neřešíme
        public Node Child(int idx) => children[idx];

        // Přidá nového potomka a zatřídí ho do seznamu podle jeho klíče
        public void AddChild(Node node)
        {
            node.parent = this;
            var position = children.FindIndex(child => node.key <= child.key);
            position = position < 0 ? children.Count : position;
            children.Insert(position, node);
            UpdateKey();
        }

        // Odstraní potomka na pozici idx, počítáno od nuly
        // Případnou výjimku neřešíme
        public void RemoveChild(int idx)
        {
            children.RemoveAt(idx);
            UpdateKey();
        }


        // Přepočítá klíč, nutno volat vždy, když se něco změnilo u potomků 
        public void UpdateKey()
        {
            if (children.Any())
                key = children.Max(child => child.key);
        }

        // True pokud uzel je listem
        public bool isLeaf()
        {
            return !children.Any();
        }
    }
    // Tato třída bude obsahovat veškeré potřebné metody ke vkládání uzlů do stromu
    class Tree23
    {
        Node root;

        // Konstruktor vytváří strom sestávající z kořene
        public Tree23()
        {
            root = new Node(int.MaxValue);
        }

        // vrací list s nejmenším klíčem, který je aspoň tak velký jako 
        // pokud takový klíč ve stromu není, vrací nejpravější list
        public Node Search(int key)
        {
            return Search(root, key);
        }

        // vrací list podstromu  s nejmenším klíčem, který je aspoň tak velký jako 
        // pokud takový klíč v podstromu není, vrací nejpravější list
        Node Search(Node node, int key)
        {
            if (node.isLeaf())
                return node;

            int i = 0;
            while (i < node.NoOfChildren - 1 && key > node.Child(i).key)
                i++;
            return Search(node.Child(i), key);
        }


        // vloží do stromu nový list s klíčem key a případně strom vyváží
        // vrací false, pokud klíč už ve stromě je
        public bool Insert(int key)
        {
            var leaf = Search(key);
            if (leaf.key == key)
                return false;

             var newLeaf = new Node(key);
            
            // Nový uzel se má přidat k rodiči nalezeného listu
            // Pokud nalezený list nemá rodiče, znamená to, že strom sestává pouze z kořene 
            // Pak tedy přidáváme nový uzel jako potomka kořene
            var parent = leaf.parent ?? leaf;

            // Bude-li tuto metodu volat více vláken, musíte nyní zamknout . 
            // Pokud se to nepovede, čekáte na odemčení.
            // Pak ale musíte znovu získat , protože se mezitím mohl změnit!
            // Jiné vlákno mohlo vyvažovat  a přepojit  na jiného rodiče. 
            // Pokus o zamčení se tedy musí dělat v cyklu while - DOPLŇTE ZDE

             parent.AddChild(newLeaf);
            
            // Metoda Balance postupuje stromem vzhůru a vyvažuje postupně vyšší uzly.
            // Vyvažovaný uzel vždy zamkne (zámky jsou reentrantní) a pak odemkne. 
            Balance(parent);
            return true;
        }


        // Metoda vrací textový výpis celého stromu v jednotlivých řádcích pod sebou
        public override string ToString()
        {
            var level = new List { root };
            var result = "";
            while (level.Any())
            {
                var lowerLevel = new List();
                // Nodes of different parents will be separated by |
                var parent = level[0].parent;
                
                foreach (var node in level)
                {
                    result += (node.parent == parent ? "" : "|  ")
                      + (node.key == int.MaxValue ? "+N" : node.key.ToString()) 
                      + " (" + node.NoOfChildren + ")  ";
                    parent = node.parent;
                    for (int i = 0; i < node.NoOfChildren; i++)
                        lowerLevel.Add(node.Child(i));
                }
                result += "\n";
                level = lowerLevel;
            }
            return result;
        }

    }
}

Hints for general array insertion

1. fáze: každý prvek bi zařazovaného pole B[1..k] bude mít své vlákno, které ho zatřídí do stromu metodou Search (viz vzorový kód). Výsledkem bude pomocné pole C[1..k], obsahující pro každý prvek bi  odkaz na list stromu, vlevo od něhož se má bi  připojit. 

2. fáze: potřebujeme nyní identifikovat všechny neprázdné řetězce B1, ..., Bm (jejich počet m předem neznáme), které se budou vkládat mezi listy stávajícího stromu. Na obr. 6.6 ve skriptech to vypadá jednoduše, ale uvědomte si, že algoritmus ty řetězce "nevidí." Musíme mu sdělit, kde v poli B začínají a končí. Začátek každého řetězce zjistíme jednoduše v poli C -  je to pozice, jejíž obsah je odlišný od pozice předchozí. Sekvenčně by to byla hračka, ale my to musíme paralelizovat. 

Proto vytvoříme pomocné binární pole D[1..k] s jedničkami na počátečních pozicích řetězců:

D[1]: = 1

for 2 ≤ ≤ k pardo

   if C[-1] ≠ C[ithen D[i] := 1

   else D[i] := 0

Následně spočteme prefixovou sumu pole D a výsledek uložíme do pole E. V prvcích pole E nyní máme očíslování jednotlivých řetězců B1, ..., Bm. Všimněte si, že jejich počet je m = E[k]. Konečně vytvoříme pole F[1..m+1], které bude obsahovat indexy začátků řetězců Bi v poli B:

F[1] := 1

for 2 ≤ ≤ k pardo

   if E[-1] ≠ E[ithen F[E[i]] := i

F[m+1] := k+1 

Každý řetězec Bi  začíná na pozici F[i] a končí na pozici F[i+1]-1. Příklad pro k = 8, m = 4:

C

a1a2a2a2a5a5a9a9


D

11001010


E

12223344


F

12579


3. fáze: pro každý řetězec Bi  se spustí samostatné vlákno, které ho bude zařazovat. Zamkne rodičovský uzel p listu ai , k němuž se Bi  připojuje, a přidá k němu prostřední prvek Bi  jako nový list. Pro každé ze dvou zbývajících neprázdných půlek Bi  spustí samostatné vlákno, které se bude chovat analogicky, viz obr. 6.6 ve skriptech. Nyní se spustí metoda Balance, kterou lze myslím použít beze změny z jednoduché verze algoritmu. Stejně tak zamykání a odemykání rodičovského uzlu.  

Oproti skriptům je implementace zjednodušená, není nutno synchronizovat jednotlivé vlny vyvážení stoupající stromem, stačí vždy zamknout a vyvážit jen uzel, ke kterému se navěšuje nový potomek.

Part 2 - Multithreaded programming


Study resources