TP - Optimization of the energy consumption of a Matrix Multiplication

Overview

Optimization de la consommation: Multiplication de matrices

Nous allons travailler sur la multiplication de matrices denses. C’est un algorithme classique qui est utilisé dans de nombreux domaines comme l’apprentissage automatique, l’analyse de données, les moteurs 3D, les simulations physiques, etc.

Ici nous travaillerons avec des matrices carrées de taille \(n \times n\). Le produit de matrices \(C = A \times B\) est défini par

\(c_{ij} = \sum^{n}_{k=1} a_{ik}b_{kj}\)

avec i est l’indice de ligne et j l’indice de colonne de la matrice C.

Représentation des matrices en Rust

Pour représenter les matrices en Rust nous allons utiliser la structure suivante:

type Element = f64;

#[derive(Debug, PartialEq)]
pub struct Matrix {
    n: usize,
    values: Vec<Element>,
}
  • Element est le type des valeurs à l’intérieur de la matrice, ici des nombres à virgule flottante stockés sur 64 bits (l’équivalent des double en C).

  • n représente la taille d’un côté de la matrice

  • values est un vecteur qui contient les éléments de la matrice

    1. Implémentez pour la structure Matrix un constructeur fn new(n: usize, values: Vec<Element>) → Self. Attention à bien vérifier grace à une assertion que le vecteur values contient bien \(n^2\) valeurs.

Tip
N’hésitez pas à implémenter des tests automatiques pour vous aider à valider l’implémentation de chaque méthode tout au long du TP.

Implémentation des traits Index et IndexMut

Une matrice est une structure à deux dimensions, mais nous allons la stocker sur le vecteur values à une seule dimension. Il y a plusieurs ordres de stockage possibles. Ici, nous allons stocker les matrices dans l’ordre des lignes (row-major order).

Ordre des lignes pour une matrice 3x3

Nous souhaitons cependant pouvoir accéder aux éléments en utilisant les indices ligne i et colonne j.

  1. Implémentez les traits std::ops::Index<(usize, usize)> et std::ops::IndexMut<(usize, usize)> pour la structure Matrix de manière à ce que l’on puisse accéder aux éléments de la matrice avec la syntaxe suivante,

#[test]
fn indexes() {
    let mut m = Matrix::new(2, vec![1.0, 2.0, 3.0, 4.0]);
    assert_eq!(m[(0, 0)], 1.0);
    assert_eq!(m[(1, 0)], 3.0);

    m[(1,0)] = 5.0;
    assert_eq!(m[(1, 0)], 5.0);
}

Génération de matrices remarquables

  1. Implémentez un deuxième constructeur fn id(n:usize) → Self qui retourne la matrice identité de dimension \(n\).

  2. Implémentez un troisième constructeur fn random(n:usize) → Self qui retourne une matrice de dimension \(n\) dont les éléments sont tirés aléatoirement d’une distribution uniforme sur \([-1.0, 1.0\)].

Installation de l’outil perf

Dans la suite du TP nous allons utiliser l’outil perf pour faire diverses mesurer de performance. Pour installer l’outil perf sous Ubuntu ou Debian vous pouvez exécuter les commandes suivantes:

sudo apt-get install --reinstall linux-tools-common linux-tools-generic linux-tools-`uname -r`
echo "-1" | sudo tee /proc/sys/kernel/perf_event_paranoid
Warning
Sur certaines machines il n’est pas possible d’installer l’outil perf. Le cas échéant, utilisez la commande time et au lieu de mesurer l’énergie consommée / le nombre de cache-misses, vous mesurerez le temps d’exécution.

Multiplication matricielle naïve

  1. Implémentez une méthode fn multiply(a: &Matrix, b: &Matrix) → Matrix qui retourne le résultat de la multiplication matricielle de a par b. Écrivez un code simple sans essayer à ce stade d’optimiser la fonction. Rajoutez un ou plusieurs tests pour tester que le résultat est correct.

  2. Modifiez le fichier src/main.rs de manière à pouvoir réaliser le produit de deux matrices aléatoires dont la taille est passée en ligne de commande.

  3. Utilisez l’outil perf pour estimer grâce aux compteurs RAPL la consommation énérgétique en Joules de votre programme pour une matrice de dimension 256x256.

$ cargo build
$ perf stat -e power/energy-pkg/,power/energy-ram/ ./target/debug/matrixmult 256
Tip
Lorsque vous faites des mesures, assurez vous que les valeurs mesurées sont significatives et reproductibles. Pour cela vous pouvez répéter la mesure plusieurs fois et vérifier que l’écart type est petit par rapport à la valeur mesurée.
  1. Compilez maintenant le programme en utilisant les optimisations du compilateur et mesurez l’énergie consommée à nouveau.

$ cargo build --release
$ perf stat -e power/energy-pkg/,power/energy-ram/ ./target/debug/matrixmult naive 256

Qu’observez vous ? Comment l’expliquez vous ?

  1. Testez la version optimisée avec des dimensions plus grandes: 512, 768, 1024, 1280, 1536, 1792 ? Tracez la courbe de temps d’exécution et de consommation en fonction de n.

Comparez les avec les courbes ci-dessous obtenues sur un processeur à six coeurs i7-9850H CPU @ 2.60GHz, avec 12Mb de cache L3.

Multiplication naïve sur un core-i7 2.60GHz avec un L3 de 12Mb

  1. Quelles sont les caractéristiques de votre machine ? Vous pouvez utiliser les outils suivants:

    • lstopo pour examiner la hiérarchie mémoire

    • cat /proc/cpuinfo pour avoir les caractéristiques du processeur

Cache blocking

Comme nous avons vu en cours, la multiplication de matrices peut présenter des problèmes d’accès au cache et à la mémoire. En effet, l’accès à la deuxième matrice n’est pas dans l’ordre des lignes mais dans l’ordre des colonnes. Les éléments d’une colonne ne sont donc pas contigus en mémoire.

Pour vérifier si notre implémentation souffre de problèmes de localité mémoire nous allons à nouveau utiliser l’outil perf.

  1. Mesurons tout d’abord le nombre d’accès mémoire au dernier niveau de cache pour la version naïve avec la commande suivante:

$ perf stat -e LLC-loads,LLC-stores ./target/release/matrixmult 1280
  • LLC-loads mesure le nombre de lectures depuis le dernier niveau de cache

  • LLC-stores mesure le nombre d’écritures sur le dernier niveau de cache

  1. Implementez maintenant une version bloquée de la multiplication matricielle, fn multiply_blocked(a: &Matrix, b: &Matrix) → Matrix. Définissez une constante BLOCK dans la classe Matrix pour stocker la taille du block que vous pouvez fixer à 64.

Tip
Vérifiez bien que la dimension de vos matrices est un multiple de la taille du block.
  1. Mesurez maintenant le nombre d’accès mémoires au dernier niveau de cache pour la version bloquée ?

  2. Mesurez la consommation énergétique et le temps d’exécution pour la version bloquée et comparez les aux mesures de la version naïve ?

  3. Que concluez vous ? Pourquoi la version bloquée se révèle plus efficace ?

Parallélisation

Si notre processeur possède plusieurs cœurs de calcul, nous pouvons paralléliser l’algorithme de manière à le rendre encore plus efficace. Pour cela nous allons nous appuyer sur la bibliothèque Rayon.

  1. Implémentez une version parallèle du produit de matrices, fn multiply_rayon(a: &Matrix, b: &Matrix) → Matrix.

    • Nous vous conseillons de paralléliser en découpant selon les lignes de a et c (la matrice résultat).

    • Rayon ajoute les méthodes par_chunks(&self, chunk_size: usize) et par_chunks_mut(&mut self, chunk_size: usize) aux itérateurs. Ces deux méthodes retournent des itérateurs parallèles. Les opérations sur les itérateurs seront distribuées sur différents threads. Vous pouvez donc utiliser ces deux méthodes pour répartir un calcul sur les lignes de c et les lignes de a.

    • Pour itérer de manière synchronisé sur les lignes des deux itérateurs vous pouvez utiliser la méthode zip.

  2. Mesurez l’énergie consommée par l’implémentation parallèle. Comparez les mesures aux expériences précédentes. Que concluez vous ?

  3. Rajoutez du cache-blocking dans l’implémentation parallèle et mesurez l’effet sur la consommation.

Pour aller plus loin…​

Le produit de matrices que nous avons implémenté est efficace mais il est possible de pousser les optimisations encore plus loin. Voici quelques références et pistes, si ce travail vous intéresse:

  • Le compilateur actuel Rust ne réussit pas à vectoriser correctement le produit de matrices. Néanmoins il est possible d’utiliser des (appels intrinsèques pour vectoriser manuellement et tirer parti des instructions SIMD du processeur.

Tip
La vectorisation automatique est un des points faibles de Rust. En raison de vérifications plus poussées, comme les débordement de tableaux, Rust n’arrive pas toujours à bien vectoriser une boucle. Au contraire des langages comme le C ou le Fortran, offrent moins de garanties sur la correction mémoire, mais vectorisent généralement mieux le code.
  • Plutôt qu’utiliser des techniques de blocking, qui doivent être paramétrisées par une taille de bloc fixe; il est possible d’implementer la multiplication matricielle pour préserver la localité indépendamment de l’échelle. C’est ce qu’on appelle en anglais un algorithme cache-oblivious. Pour la multiplication de matrices, un tel algorithme peut être obtenu en réordonnant les éléments selon l’ordre donné par la courbe de Lebesgue. Cela permet d’obtenir des implémentations très efficaces pour des matrices dont la dimension est une puissance de deux.

  • La parallélisation que nous vous avons proposé se révèle très efficace. Néanmoins il est possible d’aller encore plus vite en utilisant une décomposition en blocs et la multiplication proposée par Strassen.

Crédits

  • Illustation d’ordre des lignes adapté de l’image de Cmglee, en CC BY-SA 4.0.

You might also like...
Cli tool for git productivity written in Rust and packaged for consumption via NPM
Cli tool for git productivity written in Rust and packaged for consumption via NPM

crust 🍞 cli tool for git productivity written in Rust and packaged for consumption via NPM This repo is identical with @skyneticist/golee in terms of

Sample and plot power consumption, average frequency and cpu die temperatures over time.
Sample and plot power consumption, average frequency and cpu die temperatures over time.

sense Sense is a small tool to gather data on cpu temperature, power usage and clock frequency and plot graphs during some load. Dependencies Sense is

Scaphandre is a metrology agent dedicated to electrical power consumption metrics.
Scaphandre is a metrology agent dedicated to electrical power consumption metrics.

Scaphandre [skafɑ̃dʁ] is a metrology agent dedicated to electrical power consumption metrics. The goal of the project is to permit to any company or individual to measure the power consumption of its tech services and get this data in a convenient form, sending it through any monitoring or data analysis toolchain.

An API and test-app that exposes zcash functionality for app consumption

Zingolib This repo provides both a library for zingoproxyclient and zingo-mobile, as well as an included cli application to interact with zcashd via l

Raspberry Pi's CPU and GPU temperature exporter for Prometheus consumption.
Raspberry Pi's CPU and GPU temperature exporter for Prometheus consumption.

Pi Temperature Exporter A simple application for collecting Raspberry Pi's CPU and GPU temperatures and exporting them for Prometheus consumption. Ins

BLEZ - Asynchronous Bluetooth Low Energy on Linux for Rust

BLEZ - Asynchronous Bluetooth Low Energy on Linux for Rust This library provides an asynchronous, fully featured interface to the Bluetooth Low Energy

A plugin for Jupyter Notebooks that shows you its energy use.

Jupyter Energy Jupyter Notebooks are a data science tool mostly used for statistics and machine learning, some of the most energy-intensive computing

Coppers is a custom test harnass for Rust that measures the energy usage of your test suite.
Coppers is a custom test harnass for Rust that measures the energy usage of your test suite.

Coppers Coppers is a test harness for Rust that can measure the evolution of power consumptions of a Rust program between different versions with the

The hacker's BLE (bluetooth low energy) browser terminal app
The hacker's BLE (bluetooth low energy) browser terminal app

Blendr is a terminal UI app for browsing BLE (Bluetooth Low Energy) devices. It allows you to inspect, search, connect, and analyze data coming from B

auto-epp-rs is a program that manages the energy performance preferences (EPP) of your AMD CPU using the AMD-Pstate driver.

auto-epp-rs is a program that manages the energy performance preferences (EPP) of your AMD CPU using the AMD-Pstate driver. It adjusts the EPP settings based on whether your system is running on AC power or battery power, helping optimize power consumption and performance. This project is a rewrite of the original Python version jothi-prasath/auto-epp in Rust, aiming to improve memory efficiency and overall performance.

Rust implementation of real-coded GA for solving optimization problems and training of neural networks
Rust implementation of real-coded GA for solving optimization problems and training of neural networks

revonet Rust implementation of real-coded genetic algorithm for solving optimization problems and training of neural networks. The latter is also know

Mathematical optimization in pure Rust

argmin A pure Rust optimization framework This crate offers a numerical optimization toolbox/framework written entirely in Rust. It is at the moment p

Collection of Optimization algorithm in Rust

rustimization A rust optimization library which includes L-BFGS-B and Conjugate Gradient algorithm. Documentation The simplest way to use these optimi

Linearly Constrained Separable Optimization

LCSO (Linearly Constrained Separable Optimization) Authors The original algorithms here were developed by Nicholas Moehle, and this project was author

Rust implementation of real-coded GA for solving optimization problems and training of neural networks
Rust implementation of real-coded GA for solving optimization problems and training of neural networks

revonet Rust implementation of real-coded genetic algorithm for solving optimization problems and training of neural networks. The latter is also know

A pure, low-level tensor program representation enabling tensor program optimization via program rewriting

Glenside is a pure, low-level tensor program representation which enables tensor program optimization via program rewriting, using rewriting frameworks such as the egg equality saturation library.

OptFrame is an optimization framework focused in metaheuristic techniques

optframe-rust Welcome to OptFrame project in Rust. What is OptFrame? OptFrame is an optimization framework focused in metaheuristic techniques, develo

A standalone Aleo prover build upon snarkOS and snarkVM, with multi-threading optimization

Aleo Light Prover Introduction A standalone Aleo prover build upon snarkOS and snarkVM, with multi-threading optimization. It's called "light" because

A neural network model that can approximate any non-linear function by using the random search algorithm for the optimization of the loss function.

random_search A neural network model that can approximate any non-linear function by using the random search algorithm for the optimization of the los

Owner
Bynawers
Bynawers
A plugin for Jupyter Notebooks that shows you its energy use.

Jupyter Energy Jupyter Notebooks are a data science tool mostly used for statistics and machine learning, some of the most energy-intensive computing

Marcel Garus 3 Jun 29, 2022
Rust implementation of real-coded GA for solving optimization problems and training of neural networks

revonet Rust implementation of real-coded genetic algorithm for solving optimization problems and training of neural networks. The latter is also know

Yury Tsoy 19 Aug 11, 2022
A pure, low-level tensor program representation enabling tensor program optimization via program rewriting

Glenside is a pure, low-level tensor program representation which enables tensor program optimization via program rewriting, using rewriting frameworks such as the egg equality saturation library.

Gus Smith 45 Dec 28, 2022
A neural network model that can approximate any non-linear function by using the random search algorithm for the optimization of the loss function.

random_search A neural network model that can approximate any non-linear function by using the random search algorithm for the optimization of the los

ph04 2 Apr 1, 2022
import sticker packs from telegram, to be used at the Maunium sticker picker for Matrix

mstickereditor Import sticker packs from telegram, to be used at the Maunium sticker picker for Matrix Requirements: a Stickerpickerserver msrd0/docke

null 11 Dec 27, 2022
Statically sized matrix using a definition with const generics

Statically sized matrix using a definition with const generics

DenTaku 3 Aug 2, 2022
🧮 alphatensor matrix breakthrough algorithms + simd + rust.

simd-alphatensor-rs tldr; alphatensor matrix breakthrough algorithims + simd + rust. This repo contains the cutting edge matrix multiplication algorit

drbh 50 Feb 11, 2023
General matrix multiplication with custom configuration in Rust. Supports no_std and no_alloc environments.

microgemm General matrix multiplication with custom configuration in Rust. Supports no_std and no_alloc environments. The implementation is based on t

null 4 Nov 6, 2023
Tool to monitor the statistics and the energy consumption of docker containers

Docker Activity Docker activity is a tool to monitor the statistics of your containers and output their energy consumption. Warning It's still in earl

Jérémie Drouet 39 Dec 6, 2022
fast rust implementation of online nonnegative matrix factorization as laid out in the paper "detect and track latent factors with online nonnegative matrix factorization"

ONMF status: early work in progress. still figuring this out. code still somewhat messy. api still in flux. fast rust implementation of online nonnega

null 2 Apr 10, 2020