From 3bea82846e533a93b979d5a745581882d7aa320e Mon Sep 17 00:00:00 2001 From: Yagiz Senal Date: Thu, 11 May 2023 22:12:19 +0300 Subject: [PATCH 1/3] docs(tfhe): add dark market tutorial --- tfhe/Cargo.toml | 4 + tfhe/docs/tutorial/dark_market.md | 479 ++++++++++++++++++++++++++++++ tfhe/examples/dark_market.rs | 412 +++++++++++++++++++++++++ 3 files changed, 895 insertions(+) create mode 100644 tfhe/docs/tutorial/dark_market.md create mode 100644 tfhe/examples/dark_market.rs diff --git a/tfhe/Cargo.toml b/tfhe/Cargo.toml index 8a180004b7..550e0ddc50 100644 --- a/tfhe/Cargo.toml +++ b/tfhe/Cargo.toml @@ -189,6 +189,10 @@ required-features = ["shortint", "internal-keycache"] name = "boolean_key_sizes" required-features = ["boolean", "internal-keycache"] +[[example]] +name = "dark_market" +required-features = ["integer", "internal-keycache"] + [[example]] name = "shortint_key_sizes" required-features = ["shortint", "internal-keycache"] diff --git a/tfhe/docs/tutorial/dark_market.md b/tfhe/docs/tutorial/dark_market.md new file mode 100644 index 0000000000..ef0967aefb --- /dev/null +++ b/tfhe/docs/tutorial/dark_market.md @@ -0,0 +1,479 @@ +# Dark Market Tutorial + +In this tutorial, we are going to build a dark market application using TFHE-rs. A dark market is a marketplace where +buy and sell orders are not visible to the public before they are filled. Different algorithms aim to +solve this problem, we are going to implement the algorithm defined [in this paper](https://eprint.iacr.org/2022/923.pdf) with TFHE-rs. + +We will first implement the algorithm in plain Rust and then we will see how to use TFHE-rs to +implement the same algorithm with FHE. + +In addition, we will also implement a modified version of the algorithm that allows for more concurrent operations which +improves the performance in hardware where there are multiple cores. + +## Specifications + +#### Inputs: + +* A list of sell orders where each sell order is only defined in volume terms, it is assumed that the price is fetched + from a different source. +* A list of buy orders where each buy order is only defined in volume terms, it is assumed that the price is fetched + from a different source. + +#### Input constraints: + +* The sell and buy orders are within the range [1,100]. +* The maximum number of sell and buy orders is 500, respectively. + +#### Outputs: + +There is no output returned at the end of the algorithm. Instead, the algorithm makes changes on the given input lists. +The number of filled orders is written over the original order count in the respective lists. If it is not possible to +fill the orders, the order count is set to zero. + +#### Example input and output: + +##### Example 1: + +| | Sell | Buy | +|--------|--------------------|-----------| +| Input | [ 5, 12, 7, 4, 3 ] | [ 19, 2 ] | +| Output | [ 5, 12, 4, 0, 0 ] | [ 19, 2 ] | + +Last three indices of the filled sell orders are zero because there is no buy orders to match them. + +##### Example 2: + +| | Sell | Buy | +|--------|-------------------|----------------------| +| Input | [ 3, 1, 1, 4, 2 ] | [ 5, 3, 3, 2, 4, 1 ] | +| Output | [ 3, 1, 1, 4, 2 ] | [ 5, 3, 3, 0, 0, 0 ] | + +Last three indices of the filled buy orders are zero because there is no sell orders to match them. + +## Plain Implementation + +1. Calculate the total sell volume and the total buy volume. + +```rust +let total_sell_volume: u16 = sell_orders.iter().sum(); +let total_buy_volume: u16 = buy_orders.iter().sum(); +``` + +2. Find the total volume that will be transacted. In the paper, this amount is calculated with the formula: + +``` +(total_sell_volume > total_buy_volume) * (total_buy_volume − total_sell_volume) + total_sell_volume +``` + +When closely observed, we can see that this formula can be replaced with the `min` function. Therefore, we calculate this +value by taking the minimum of the total sell volume and the total buy volume. + +```rust +let total_volume = std::cmp::min(total_buy_volume, total_sell_volume); +``` + +3. Beginning with the first item, start filling the sell orders one by one. We apply the `min` function replacement also + here. + +```rust +let mut volume_left_to_transact = total_volume; +for sell_order in sell_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *sell_order); + *sell_order = filled_amount; + volume_left_to_transact -= filled_amount; +} +``` + +The number of orders that are filled is indicated by modifying the input list. For example, if the first sell order is +1000 and the total volume is 500, then the first sell order will be modified to 500 and the second sell order will be +modified to 0. + +4. Do the fill operation also for the buy orders. + +```rust +let mut volume_left_to_transact = total_volume; +for buy_order in buy_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *buy_order); + *buy_order = filled_amount; + volume_left_to_transact -= filled_amount; +} +``` + +#### The complete algorithm in plain Rust: + +```rust +fn volume_match_plain(sell_orders: &mut Vec, buy_orders: &mut Vec) { + let total_sell_volume: u16 = sell_orders.iter().sum(); + let total_buy_volume: u16 = buy_orders.iter().sum(); + + let total_volume = std::cmp::min(total_buy_volume, total_sell_volume); + + let mut volume_left_to_transact = total_volume; + for sell_order in sell_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *sell_order); + *sell_order = filled_amount; + volume_left_to_transact -= filled_amount; + } + + let mut volume_left_to_transact = total_volume; + for buy_order in buy_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *buy_order); + *buy_order = filled_amount; + volume_left_to_transact -= filled_amount; + } +} +``` + +## FHE Implementation + +For the FHE implementation, we first start with finding the right bit size for our algorithm to work without +overflows. + +The variables that are declared in the algorithm and their maximum values are described in the table below: + +| Variable | Maximum Value | Bit Size | +|-------------------------|---------------|----------| +| total_sell_volume | 50000 | 16 | +| total_buy_volume | 50000 | 16 | +| total_volume | 50000 | 16 | +| volume_left_to_transact | 50000 | 16 | +| sell_order | 100 | 7 | +| buy_order | 100 | 7 | + +As we can observe from the table, we need **16 bits of message space** to be able to run the algorithm without +overflows. TFHE-rs provides different presets for the different bit sizes. Since we need 16 bits of message, we are +going to use the `integer` module to implement the algorithm. + +Here are the input types of our algorithm: + +* `sell_orders` is of type `Vec` +* `buy_orders` is of type `Vec` +* `server_key` is of type `tfhe::integer::ServerKey` + +Now, we can start implementing the algorithm with FHE: + +1. Calculate the total sell volume and the total buy volume. + +```rust +let mut total_sell_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); +for sell_order in sell_orders.iter_mut() { + server_key.smart_add_assign(&mut total_sell_volume, sell_order); +} + +let mut total_buy_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); +for buy_order in buy_orders.iter_mut() { + server_key.smart_add_assign(&mut total_buy_volume, buy_order); +} +``` + +2. Find the total volume that will be transacted by taking the minimum of the total sell volume and the total buy + volume. + +```rust +let total_volume = server_key.smart_min(&mut total_sell_volume, &mut total_buy_volume); +``` + +3. Beginning with the first item, start filling the sell and buy orders one by one. We can create `fill_orders` closure to +reduce code duplication since the code for filling buy orders and sell orders are the same. + +```rust +let fill_orders = |orders: &mut [RadixCiphertextBig]| { + let mut volume_left_to_transact = total_volume.clone(); + for mut order in orders.iter_mut() { + let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, &mut order); + server_key.smart_sub_assign(&mut volume_left_to_transact, &mut filled_amount); + *order = filled_amount; + } +}; + +fill_orders(sell_orders); +fill_orders(buy_orders); +``` + +#### The complete algorithm in TFHE-rs: + +```rust +const NUMBER_OF_BLOCKS: usize = 8; + +fn volume_match_fhe( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + let mut total_sell_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for sell_order in sell_orders.iter_mut() { + server_key.smart_add_assign(&mut total_sell_volume, sell_order); + } + + let mut total_buy_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for buy_order in buy_orders.iter_mut() { + server_key.smart_add_assign(&mut total_buy_volume, buy_order); + } + + let total_volume = server_key.smart_min(&mut total_sell_volume, &mut total_buy_volume); + + let fill_orders = |orders: &mut [RadixCiphertextBig]| { + let mut volume_left_to_transact = total_volume.clone(); + for mut order in orders.iter_mut() { + let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, &mut order); + server_key.smart_sub_assign(&mut volume_left_to_transact, &mut filled_amount); + *order = filled_amount; + } + }; + + fill_orders(sell_orders); + fill_orders(buy_orders); +} + +``` + +### Optimizing the implementation + +* TFHE-rs provides parallelized implementations of the operations. We can use these parallelized + implementations to speed up the algorithm. For example, we can use `smart_add_assign_parallelized` instead of + `smart_add_assign`. + +* We can parallelize vector sum with Rayon and `reduce` operation. +```rust +let parallel_vector_sum = |vec: &mut [RadixCiphertextBig]| { + vec.to_vec().into_par_iter().reduce( + || server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS), + |mut acc: RadixCiphertextBig, mut ele: RadixCiphertextBig| { + server_key.smart_add_parallelized(&mut acc, &mut ele) + }, + ) +}; +``` + +* We can run vector summation on `buy_orders` and `sell_orders` in parallel since these operations do not depend on each other. +```rust +let (mut total_sell_volume, mut total_buy_volume) = + rayon::join(|| vector_sum(sell_orders), || vector_sum(buy_orders)); +``` + +* We can match sell and buy orders in parallel since the matching does not depend on each other. +```rust +rayon::join(|| fill_orders(sell_orders), || fill_orders(buy_orders)); +``` + +#### Optimized algorithm +```rust +fn volume_match_fhe_parallelized( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + let parallel_vector_sum = |vec: &mut [RadixCiphertextBig]| { + vec.to_vec().into_par_iter().reduce( + || server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS), + |mut acc: RadixCiphertextBig, mut ele: RadixCiphertextBig| { + server_key.smart_add_parallelized(&mut acc, &mut ele) + }, + ) + }; + + let (mut total_sell_volume, mut total_buy_volume) = rayon::join( + || parallel_vector_sum(sell_orders), + || parallel_vector_sum(buy_orders), + ); + + let total_volume = + server_key.smart_min_parallelized(&mut total_sell_volume, &mut total_buy_volume); + + let fill_orders = |orders: &mut [RadixCiphertextBig]| { + let mut volume_left_to_transact = total_volume.clone(); + for mut order in orders.iter_mut() { + let mut filled_amount = + server_key.smart_min_parallelized(&mut volume_left_to_transact, &mut order); + server_key + .smart_sub_assign_parallelized(&mut volume_left_to_transact, &mut filled_amount); + *order = filled_amount; + } + }; + + rayon::join(|| fill_orders(sell_orders), || fill_orders(buy_orders)); +} +``` + +## Modified Algorithm + +When observed closely, there is only a small amount of concurrency introduced in the `fill_orders` part of the algorithm. +The reason is that the `volume_left_to_transact` is shared between all the orders and should be modified sequentially. +This means that the orders cannot be filled in parallel. If we can somehow remove this dependency, we can fill the orders in parallel. + +In order to do so, we closely observe the function of `volume_left_to_transact` variable in the algorithm. We can see that it is being used to check whether we can fill the current order or not. +Instead of subtracting the current order value from `volume_left_to_transact` in each loop, we can add this value to the next order +index and check the availability by comparing the current order value with the total volume. If the current order value +(now representing the sum of values before this order plus this order) is smaller than the total number of matching orders, +we can safely fill all the orders and continue the loop. If not, we should partially fill the orders with what is left from +matching orders. + +We will call the new list the "prefix sum" of the array. + +The new version for the plain `fill_orders` is as follows: +```rust +let fill_orders = |orders: &mut [u64], prefix_sum: &[u64], total_orders: u64|{ + orders.iter().for_each(|order : &mut u64| { + if (total_orders >= prefix_sum[i]) { + continue; + } else if total_orders >= prefix_sum.get(i-1).unwrap_or(0) { + *order = total_orders - prefix_sum.get(i-1).unwrap_or(0); + } else { + *order = 0; + } + }); +}; +``` + +To write this new function we need transform the conditional code into a mathematical expression since FHE does not support conditional operations. +```rust + +let fill_orders = |orders: &mut [u64], prefix_sum: &[u64], total_orders: u64| { + orders.iter().for_each(|order| : &mut){ + *order = *order + ((total_orders >= prefix_sum - std::cmp::min(total_orders, prefix_sum.get(i - 1).unwrap_or(&0).clone()) - *order); + } +}; +``` + +New `fill_order` function requires a prefix sum array. We are going to calculate this prefix sum array in parallel +with the algorithm described [here](https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-39-parallel-prefix-sum-scan-cuda). + +The sample code in the paper is written in CUDA. When we try to implement the algorithm in Rust we see that the compiler does not allow us to do so. +The reason for that is while the algorithm does not access the same array element in any of the threads(the index calculations using `d` and `k` values never overlap), +Rust compiler cannot understand this and does not let us share the same array between threads. +So we modify how the algorithm is implemented, but we don't change the algorithm itself. + +Here is the modified version of the algorithm in TFHE-rs: +```rust +fn volume_match_fhe_modified( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + let compute_prefix_sum = |arr: &[RadixCiphertextBig]| { + if arr.is_empty() { + return arr.to_vec(); + } + let mut prefix_sum: Vec = (0..arr.len().next_power_of_two()) + .into_par_iter() + .map(|i| { + if i < arr.len() { + arr[i].clone() + } else { + server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS) + } + }) + .collect(); + // Up sweep + for d in 0..(prefix_sum.len().ilog2() as u32) { + prefix_sum + .par_chunks_exact_mut(2_usize.pow(d + 1)) + .for_each(move |chunk| { + let length = chunk.len(); + let mut left = chunk.get((length - 1) / 2).unwrap().clone(); + server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut left) + }); + } + // Down sweep + let last = prefix_sum.last().unwrap().clone(); + *prefix_sum.last_mut().unwrap() = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for d in (0..(prefix_sum.len().ilog2() as u32)).rev() { + prefix_sum + .par_chunks_exact_mut(2_usize.pow(d + 1)) + .for_each(move |chunk| { + let length = chunk.len(); + let t = chunk.last().unwrap().clone(); + let mut left = chunk.get((length - 1) / 2).unwrap().clone(); + server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut left); + chunk[(length - 1) / 2] = t; + }); + } + prefix_sum.push(last); + prefix_sum[1..=arr.len()].to_vec() + }; + + println!("Creating prefix sum arrays..."); + let time = Instant::now(); + let (prefix_sum_sell_orders, prefix_sum_buy_orders) = rayon::join( + || compute_prefix_sum(sell_orders), + || compute_prefix_sum(buy_orders), + ); + println!("Created prefix sum arrays in {:?}", time.elapsed()); + + let fill_orders = |total_orders: &RadixCiphertextBig, + orders: &mut [RadixCiphertextBig], + prefix_sum_arr: &[RadixCiphertextBig]| { + orders + .into_par_iter() + .enumerate() + .for_each(move |(i, order)| { + server_key.smart_add_assign_parallelized( + order, + &mut server_key.smart_mul_parallelized( + &mut server_key + .smart_ge_parallelized(&mut order.clone(), &mut total_orders.clone()), + &mut server_key.smart_sub_parallelized( + &mut server_key.smart_sub_parallelized( + &mut total_orders.clone(), + &mut server_key.smart_min_parallelized( + &mut total_orders.clone(), + &mut prefix_sum_arr + .get(i - 1) + .unwrap_or( + &server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS), + ) + .clone(), + ), + ), + &mut order.clone(), + ), + ), + ); + }); + }; + + let total_buy_orders = &mut prefix_sum_buy_orders + .last() + .unwrap_or(&server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS)) + .clone(); + + let total_sell_orders = &mut prefix_sum_sell_orders + .last() + .unwrap_or(&server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS)) + .clone(); + + println!("Matching orders..."); + let time = Instant::now(); + rayon::join( + || fill_orders(total_sell_orders, buy_orders, &prefix_sum_buy_orders), + || fill_orders(total_buy_orders, sell_orders, &prefix_sum_sell_orders), + ); + println!("Matched orders in {:?}", time.elapsed()); +} +``` + +## Running the tutorial + +The plain, FHE and parallel FHE implementations can be run by providing respective arguments as described below. + +```bash +# Runs FHE implementation +cargo run --release --package tfhe --example dark_market --features="integer internal-keycache" -- fhe + +# Runs parallelized FHE implementation +cargo run --release --package tfhe --example dark_market --features="integer internal-keycache" -- fhe-parallel + +# Runs modified FHE implementation +cargo run --release --package tfhe --example dark_market --features="integer internal-keycache" -- fhe-modified + +# Runs plain implementation +cargo run --release --package tfhe --example dark_market --features="integer internal-keycache" -- plain + +# Multiple implementations can be run within same instance +cargo run --release --package tfhe --example dark_market --features="integer internal-keycache" -- plain fhe-parallel +``` + +## Conclusion + +In this tutorial, we've learned how to implement the volume matching algorithm described [in this paper](https://eprint.iacr.org/2022/923.pdf) in plain Rust and in TFHE-rs. +We've identified the right bit size for our problem at hand, used operations defined in `TFHE-rs`, and introduced concurrency to the algorithm to increase its performance. diff --git a/tfhe/examples/dark_market.rs b/tfhe/examples/dark_market.rs new file mode 100644 index 0000000000..04158e50d4 --- /dev/null +++ b/tfhe/examples/dark_market.rs @@ -0,0 +1,412 @@ +use std::time::Instant; + +use rayon::prelude::*; + +use tfhe::integer::ciphertext::RadixCiphertextBig; +use tfhe::integer::keycache::IntegerKeyCache; +use tfhe::integer::ServerKey; +use tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2; + +/// The number of blocks to be used in the Radix. +const NUMBER_OF_BLOCKS: usize = 8; + +/// Plain implementation of the volume matching algorithm. +/// +/// Matches the given [sell_orders] with [buy_orders]. +/// The amount of the orders that are successfully filled is written over the original order count. +fn volume_match_plain(sell_orders: &mut [u16], buy_orders: &mut [u16]) { + let total_sell_volume: u16 = sell_orders.iter().sum(); + let total_buy_volume: u16 = buy_orders.iter().sum(); + + let total_volume = std::cmp::min(total_buy_volume, total_sell_volume); + + let mut volume_left_to_transact = total_volume; + for sell_order in sell_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *sell_order); + *sell_order = filled_amount; + volume_left_to_transact -= filled_amount; + } + + let mut volume_left_to_transact = total_volume; + for buy_order in buy_orders.iter_mut() { + let filled_amount = std::cmp::min(volume_left_to_transact, *buy_order); + *buy_order = filled_amount; + volume_left_to_transact -= filled_amount; + } +} + +/// FHE implementation of the volume matching algorithm. +/// +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. +/// The amount of the orders that are successfully filled is written over the original order count. +fn volume_match_fhe( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + println!("Calculating total sell and buy volumes..."); + let time = Instant::now(); + let mut total_sell_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for sell_order in sell_orders.iter_mut() { + server_key.smart_add_assign(&mut total_sell_volume, sell_order); + } + + let mut total_buy_volume = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for buy_order in buy_orders.iter_mut() { + server_key.smart_add_assign(&mut total_buy_volume, buy_order); + } + println!( + "Total sell and buy volumes are calculated in {:?}", + time.elapsed() + ); + + println!("Calculating total volume to be matched..."); + let time = Instant::now(); + let total_volume = server_key.smart_min(&mut total_sell_volume, &mut total_buy_volume); + println!( + "Calculated total volume to be matched in {:?}", + time.elapsed() + ); + + let fill_orders = |orders: &mut [RadixCiphertextBig]| { + let mut volume_left_to_transact = total_volume.clone(); + for mut order in orders.iter_mut() { + let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, &mut order); + server_key.smart_sub_assign(&mut volume_left_to_transact, &mut filled_amount); + *order = filled_amount; + } + }; + + println!("Filling orders..."); + let time = Instant::now(); + fill_orders(sell_orders); + fill_orders(buy_orders); + println!("Filled orders in {:?}", time.elapsed()); +} + +/// FHE implementation of the volume matching algorithm. +/// +/// This version of the algorithm utilizes parallelization to speed up the computation. +/// +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. +/// The amount of the orders that are successfully filled is written over the original order count. +fn volume_match_fhe_parallelized( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + // Calculate the element sum of the given vector in parallel + let parallel_vector_sum = |vec: &mut [RadixCiphertextBig]| { + vec.to_vec().into_par_iter().reduce( + || server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS), + |mut acc: RadixCiphertextBig, mut ele: RadixCiphertextBig| { + server_key.smart_add_parallelized(&mut acc, &mut ele) + }, + ) + }; + + println!("Calculating total sell and buy volumes..."); + let time = Instant::now(); + // Total sell and buy volumes can be calculated in parallel because they have no dependency on each other. + let (mut total_sell_volume, mut total_buy_volume) = rayon::join( + || parallel_vector_sum(sell_orders), + || parallel_vector_sum(buy_orders), + ); + println!( + "Total sell and buy volumes are calculated in {:?}", + time.elapsed() + ); + + println!("Calculating total volume to be matched..."); + let time = Instant::now(); + let total_volume = + server_key.smart_min_parallelized(&mut total_sell_volume, &mut total_buy_volume); + println!( + "Calculated total volume to be matched in {:?}", + time.elapsed() + ); + + let fill_orders = |orders: &mut [RadixCiphertextBig]| { + let mut volume_left_to_transact = total_volume.clone(); + for mut order in orders.iter_mut() { + let mut filled_amount = + server_key.smart_min_parallelized(&mut volume_left_to_transact, &mut order); + server_key + .smart_sub_assign_parallelized(&mut volume_left_to_transact, &mut filled_amount); + *order = filled_amount; + } + }; + println!("Filling orders..."); + let time = Instant::now(); + rayon::join(|| fill_orders(sell_orders), || fill_orders(buy_orders)); + println!("Filled orders in {:?}", time.elapsed()); +} + +/// FHE implementation of the volume matching algorithm. +/// +/// In this function, the implemented algorithm is modified to utilize more concurrency. +/// +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. +/// The amount of the orders that are successfully filled is written over the original order count. +fn volume_match_fhe_modified( + sell_orders: &mut [RadixCiphertextBig], + buy_orders: &mut [RadixCiphertextBig], + server_key: &ServerKey, +) { + let compute_prefix_sum = |arr: &[RadixCiphertextBig]| { + if arr.is_empty() { + return arr.to_vec(); + } + let mut prefix_sum: Vec = (0..arr.len().next_power_of_two()) + .into_par_iter() + .map(|i| { + if i < arr.len() { + arr[i].clone() + } else { + server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS) + } + }) + .collect(); + for d in 0..(prefix_sum.len().ilog2() as u32) { + prefix_sum + .par_chunks_exact_mut(2_usize.pow(d + 1)) + .for_each(move |chunk| { + let length = chunk.len(); + let mut left = chunk.get((length - 1) / 2).unwrap().clone(); + server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut left) + }); + } + let last = prefix_sum.last().unwrap().clone(); + *prefix_sum.last_mut().unwrap() = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); + for d in (0..(prefix_sum.len().ilog2() as u32)).rev() { + prefix_sum + .par_chunks_exact_mut(2_usize.pow(d + 1)) + .for_each(move |chunk| { + let length = chunk.len(); + let temp = chunk.last().unwrap().clone(); + let mut mid = chunk.get((length - 1) / 2).unwrap().clone(); + server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut mid); + chunk[(length - 1) / 2] = temp; + }); + } + prefix_sum.push(last); + prefix_sum[1..=arr.len()].to_vec() + }; + + println!("Creating prefix sum arrays..."); + let time = Instant::now(); + let (prefix_sum_sell_orders, prefix_sum_buy_orders) = rayon::join( + || compute_prefix_sum(sell_orders), + || compute_prefix_sum(buy_orders), + ); + println!("Created prefix sum arrays in {:?}", time.elapsed()); + + let fill_orders = |total_orders: &RadixCiphertextBig, + orders: &mut [RadixCiphertextBig], + prefix_sum_arr: &[RadixCiphertextBig]| { + orders + .into_par_iter() + .enumerate() + .for_each(move |(i, order)| { + server_key.smart_add_assign_parallelized( + order, + &mut server_key.smart_mul_parallelized( + &mut server_key + .smart_ge_parallelized(&mut order.clone(), &mut total_orders.clone()), + &mut server_key.smart_sub_parallelized( + &mut server_key.smart_sub_parallelized( + &mut total_orders.clone(), + &mut server_key.smart_min_parallelized( + &mut total_orders.clone(), + &mut prefix_sum_arr + .get(i - 1) + .unwrap_or( + &server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS), + ) + .clone(), + ), + ), + &mut order.clone(), + ), + ), + ); + }); + }; + + let total_buy_orders = &mut prefix_sum_buy_orders + .last() + .unwrap_or(&server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS)) + .clone(); + + let total_sell_orders = &mut prefix_sum_sell_orders + .last() + .unwrap_or(&server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS)) + .clone(); + + println!("Matching orders..."); + let time = Instant::now(); + rayon::join( + || fill_orders(total_sell_orders, buy_orders, &prefix_sum_buy_orders), + || fill_orders(total_buy_orders, sell_orders, &prefix_sum_sell_orders), + ); + println!("Matched orders in {:?}", time.elapsed()); +} + +/// Runs the given [tester] function with the test cases for volume matching algorithm. +fn run_test_cases(tester: F) { + println!("Testing empty sell orders..."); + tester( + &vec![], + &(1..11).map(|i| i).collect::>(), + &vec![], + &(1..11).map(|_| 0).collect::>(), + ); + println!(); + + println!("Testing empty buy orders..."); + tester( + &(1..11).map(|i| i).collect::>(), + &vec![], + &(1..11).map(|_| 0).collect::>(), + &vec![], + ); + println!(); + + println!("Testing exact matching of sell and buy orders..."); + tester( + &(1..11).map(|i| i).collect::>(), + &(1..11).map(|i| i).collect::>(), + &(1..11).map(|i| i).collect::>(), + &(1..11).map(|i| i).collect::>(), + ); + println!(); + + println!("Testing the case where there are more buy orders than sell orders..."); + tester( + &(1..11).map(|_| 10).collect::>(), + &vec![200], + &(1..11).map(|_| 10).collect::>(), + &vec![100], + ); + println!(); + + println!("Testing the case where there are more sell orders than buy orders..."); + tester( + &vec![200], + &(1..11).map(|_| 10).collect::>(), + &vec![100], + &(1..11).map(|_| 10).collect::>(), + ); + println!(); + + println!("Testing maximum input size for sell and buy orders..."); + tester( + &(1..=500).map(|_| 100).collect::>(), + &(1..=500).map(|_| 100).collect::>(), + &(1..=500).map(|_| 100).collect::>(), + &(1..=500).map(|_| 100).collect::>(), + ); + println!(); +} + +/// Runs the test cases for the plain implementation of the volume matching algorithm. +fn test_volume_match_plain() { + let tester = |input_sell_orders: &[u16], + input_buy_orders: &[u16], + expected_filled_sells: &[u16], + expected_filled_buys: &[u16]| { + let mut sell_orders = input_sell_orders.to_vec(); + let mut buy_orders = input_buy_orders.to_vec(); + + println!("Running plain implementation..."); + let time = Instant::now(); + volume_match_plain(&mut sell_orders, &mut buy_orders); + println!("Ran plain implementation in {:?}", time.elapsed()); + + assert_eq!(sell_orders, expected_filled_sells); + assert_eq!(buy_orders, expected_filled_buys); + }; + + println!("Running test cases for the plain implementation"); + run_test_cases(tester); +} + +/// Runs the test cases for the fhe implementation of the volume matching algorithm. +/// +/// [parallelized] indicates whether the fhe implementation should be run in parallel. +fn test_volume_match_fhe( + fhe_function: fn(&mut [RadixCiphertextBig], &mut [RadixCiphertextBig], &ServerKey), +) { + let working_dir = std::env::current_dir().unwrap(); + std::env::set_current_dir(working_dir.join("tfhe")).unwrap(); + + println!("Generating keys..."); + let time = Instant::now(); + let (client_key, server_key) = IntegerKeyCache.get_from_params(PARAM_MESSAGE_2_CARRY_2); + println!("Keys generated in {:?}", time.elapsed()); + + let tester = |input_sell_orders: &[u16], + input_buy_orders: &[u16], + expected_filled_sells: &[u16], + expected_filled_buys: &[u16]| { + let mut encrypted_sell_orders = input_sell_orders + .iter() + .cloned() + .map(|pt| client_key.encrypt_radix(pt as u64, NUMBER_OF_BLOCKS)) + .collect::>(); + let mut encrypted_buy_orders = input_buy_orders + .iter() + .cloned() + .map(|pt| client_key.encrypt_radix(pt as u64, NUMBER_OF_BLOCKS)) + .collect::>(); + + println!("Running FHE implementation..."); + let time = Instant::now(); + fhe_function( + &mut encrypted_sell_orders, + &mut encrypted_buy_orders, + &server_key, + ); + println!("Ran FHE implementation in {:?}", time.elapsed()); + + let decrypted_filled_sells = encrypted_sell_orders + .iter() + .map(|ct| client_key.decrypt_radix::(ct) as u16) + .collect::>(); + let decrypted_filled_buys = encrypted_buy_orders + .iter() + .map(|ct| client_key.decrypt_radix::(ct) as u16) + .collect::>(); + + assert_eq!(decrypted_filled_sells, expected_filled_sells); + assert_eq!(decrypted_filled_buys, expected_filled_buys); + }; + + println!("Running test cases for the FHE implementation"); + run_test_cases(tester); +} + +fn main() { + for argument in std::env::args() { + if argument == "fhe-modified" { + println!("Running modified fhe version"); + test_volume_match_fhe(volume_match_fhe_modified); + println!(); + } + if argument == "fhe-parallel" { + println!("Running parallelized fhe version"); + test_volume_match_fhe(volume_match_fhe_parallelized); + println!(); + } + if argument == "plain" { + println!("Running plain version"); + test_volume_match_plain(); + println!(); + } + if argument == "fhe" { + println!("Running fhe version"); + test_volume_match_fhe(volume_match_fhe); + println!(); + } + } +} From 1fb3a134c60fe291c7d711b88edcefc924dbad86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Test=C3=A9?= Date: Wed, 24 May 2023 12:54:04 +0200 Subject: [PATCH 2/3] docs(tfhe): format dark market example add make recipe to run it --- Makefile | 7 +++++ tfhe/examples/dark_market.rs | 60 +++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index d972580e4e..77be7c5894 100644 --- a/Makefile +++ b/Makefile @@ -343,6 +343,13 @@ regex_engine: install_rs_check_toolchain --features=$(TARGET_ARCH_FEATURE),integer \ -- $(REGEX_STRING) $(REGEX_PATTERN) +.PHONY: dark_market # Run dark market example +dark_market: install_rs_check_toolchain + RUSTFLAGS="$(RUSTFLAGS)" cargo $(CARGO_RS_CHECK_TOOLCHAIN) run --profile $(CARGO_PROFILE) \ + --example dark_market \ + --features=$(TARGET_ARCH_FEATURE),integer,internal-keycache \ + -- fhe-modified fhe-parallel plain fhe + .PHONY: pcc # pcc stands for pre commit checks pcc: no_tfhe_typo check_fmt doc clippy_all check_compile_tests diff --git a/tfhe/examples/dark_market.rs b/tfhe/examples/dark_market.rs index 04158e50d4..214d1df1df 100644 --- a/tfhe/examples/dark_market.rs +++ b/tfhe/examples/dark_market.rs @@ -37,8 +37,9 @@ fn volume_match_plain(sell_orders: &mut [u16], buy_orders: &mut [u16]) { /// FHE implementation of the volume matching algorithm. /// -/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. -/// The amount of the orders that are successfully filled is written over the original order count. +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given +/// [server_key]. The amount of the orders that are successfully filled is written over the original +/// order count. fn volume_match_fhe( sell_orders: &mut [RadixCiphertextBig], buy_orders: &mut [RadixCiphertextBig], @@ -70,8 +71,8 @@ fn volume_match_fhe( let fill_orders = |orders: &mut [RadixCiphertextBig]| { let mut volume_left_to_transact = total_volume.clone(); - for mut order in orders.iter_mut() { - let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, &mut order); + for order in orders.iter_mut() { + let mut filled_amount = server_key.smart_min(&mut volume_left_to_transact, order); server_key.smart_sub_assign(&mut volume_left_to_transact, &mut filled_amount); *order = filled_amount; } @@ -88,8 +89,9 @@ fn volume_match_fhe( /// /// This version of the algorithm utilizes parallelization to speed up the computation. /// -/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. -/// The amount of the orders that are successfully filled is written over the original order count. +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given +/// [server_key]. The amount of the orders that are successfully filled is written over the original +/// order count. fn volume_match_fhe_parallelized( sell_orders: &mut [RadixCiphertextBig], buy_orders: &mut [RadixCiphertextBig], @@ -107,7 +109,8 @@ fn volume_match_fhe_parallelized( println!("Calculating total sell and buy volumes..."); let time = Instant::now(); - // Total sell and buy volumes can be calculated in parallel because they have no dependency on each other. + // Total sell and buy volumes can be calculated in parallel because they have no dependency on + // each other. let (mut total_sell_volume, mut total_buy_volume) = rayon::join( || parallel_vector_sum(sell_orders), || parallel_vector_sum(buy_orders), @@ -128,9 +131,9 @@ fn volume_match_fhe_parallelized( let fill_orders = |orders: &mut [RadixCiphertextBig]| { let mut volume_left_to_transact = total_volume.clone(); - for mut order in orders.iter_mut() { + for order in orders.iter_mut() { let mut filled_amount = - server_key.smart_min_parallelized(&mut volume_left_to_transact, &mut order); + server_key.smart_min_parallelized(&mut volume_left_to_transact, order); server_key .smart_sub_assign_parallelized(&mut volume_left_to_transact, &mut filled_amount); *order = filled_amount; @@ -146,8 +149,9 @@ fn volume_match_fhe_parallelized( /// /// In this function, the implemented algorithm is modified to utilize more concurrency. /// -/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given [server_key]. -/// The amount of the orders that are successfully filled is written over the original order count. +/// Matches the given encrypted [sell_orders] with encrypted [buy_orders] using the given +/// [server_key]. The amount of the orders that are successfully filled is written over the original +/// order count. fn volume_match_fhe_modified( sell_orders: &mut [RadixCiphertextBig], buy_orders: &mut [RadixCiphertextBig], @@ -167,18 +171,18 @@ fn volume_match_fhe_modified( } }) .collect(); - for d in 0..(prefix_sum.len().ilog2() as u32) { + for d in 0..prefix_sum.len().ilog2() { prefix_sum .par_chunks_exact_mut(2_usize.pow(d + 1)) .for_each(move |chunk| { let length = chunk.len(); let mut left = chunk.get((length - 1) / 2).unwrap().clone(); server_key.smart_add_assign_parallelized(chunk.last_mut().unwrap(), &mut left) - }); + }); } let last = prefix_sum.last().unwrap().clone(); *prefix_sum.last_mut().unwrap() = server_key.create_trivial_zero_radix(NUMBER_OF_BLOCKS); - for d in (0..(prefix_sum.len().ilog2() as u32)).rev() { + for d in (0..prefix_sum.len().ilog2()).rev() { prefix_sum .par_chunks_exact_mut(2_usize.pow(d + 1)) .for_each(move |chunk| { @@ -256,45 +260,45 @@ fn volume_match_fhe_modified( fn run_test_cases(tester: F) { println!("Testing empty sell orders..."); tester( - &vec![], - &(1..11).map(|i| i).collect::>(), - &vec![], + &[], + &(1..11).collect::>(), + &[], &(1..11).map(|_| 0).collect::>(), ); println!(); println!("Testing empty buy orders..."); tester( - &(1..11).map(|i| i).collect::>(), - &vec![], + &(1..11).collect::>(), + &[], &(1..11).map(|_| 0).collect::>(), - &vec![], + &[], ); println!(); println!("Testing exact matching of sell and buy orders..."); tester( - &(1..11).map(|i| i).collect::>(), - &(1..11).map(|i| i).collect::>(), - &(1..11).map(|i| i).collect::>(), - &(1..11).map(|i| i).collect::>(), + &(1..11).collect::>(), + &(1..11).collect::>(), + &(1..11).collect::>(), + &(1..11).collect::>(), ); println!(); println!("Testing the case where there are more buy orders than sell orders..."); tester( &(1..11).map(|_| 10).collect::>(), - &vec![200], + &[200], &(1..11).map(|_| 10).collect::>(), - &vec![100], + &[100], ); println!(); println!("Testing the case where there are more sell orders than buy orders..."); tester( - &vec![200], + &[200], &(1..11).map(|_| 10).collect::>(), - &vec![100], + &[100], &(1..11).map(|_| 10).collect::>(), ); println!(); From bb002e6d35d35034508a2057b1d4d1d7dbb98639 Mon Sep 17 00:00:00 2001 From: tmontaigu Date: Wed, 24 May 2023 21:56:52 +0200 Subject: [PATCH 3/3] fix(dark_market): fix change cwd logic --- tfhe/examples/dark_market.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tfhe/examples/dark_market.rs b/tfhe/examples/dark_market.rs index 214d1df1df..6984568b2d 100644 --- a/tfhe/examples/dark_market.rs +++ b/tfhe/examples/dark_market.rs @@ -342,7 +342,9 @@ fn test_volume_match_fhe( fhe_function: fn(&mut [RadixCiphertextBig], &mut [RadixCiphertextBig], &ServerKey), ) { let working_dir = std::env::current_dir().unwrap(); - std::env::set_current_dir(working_dir.join("tfhe")).unwrap(); + if working_dir.file_name().unwrap() != std::path::Path::new("tfhe") { + std::env::set_current_dir(working_dir.join("tfhe")).unwrap(); + } println!("Generating keys..."); let time = Instant::now();