<jupyter_start><jupyter_text>Tutorial 2: Inside CrypTensorsNote: This tutorial is optional, and can be skipped without any loss of continuity to the following tutorials.In this tutorial, we will take a brief look at the internals of ```CrypTensors```. Using the `mpc` backend, a `CrypTensor` is a tensor encrypted using secure MPC protocols, called an `MPCTensor`. In order to support the mathematical operations required by the `MPCTensor`, CrypTen implements two kinds of secret-sharing protocols: arithmetic secret-sharing and binary secret-sharing. Arithmetic secret sharing forms the basis for most of the mathematical operations implemented by `MPCTensor`. Similarly, binary secret-sharing allows for the evaluation of logical expressions.In this tutorial, we'll first introduce the concept of a `CrypTensor` ptype (i.e. private-type), and show how to use it to obtain `MPCTensors` that use arithmetic and binary secret shares. We will also describe how each of these ptypes is used, and how they can be combined to implement desired functionality.<jupyter_code>#import the libraries
import crypten
import torch

#initialize crypten
crypten.init()
#Disables OpenMP threads -- needed by @mpc.run_multiprocess which uses fork
torch.set_num_threads(1)<jupyter_output>WARNING:root:module 'torchvision.models.mobilenet' has no attribute 'ConvBNReLU'<jupyter_text>ptype in CrypTenCrypTen defines the `ptype` (for private-type) attribute of an `MPCTensor` to denote the kind of secret-sharing protocol used in the `CrypTensor`. The `ptype` is, in many ways, analogous to the `dtype` of PyTorch. The `ptype` may have two values: - `crypten.mpc.arithmetic` for `ArithmeticSharedTensors`- `crypten.mpc.binary` for  `BinarySharedTensors`We can use the `ptype` attribute to create a `CrypTensor` with the appropriate secret-sharing protocol. For example:<jupyter_code>#Constructing CrypTensors with ptype attribute

#arithmetic secret-shared tensors
x_enc = crypten.cryptensor([1.0, 2.0, 3.0], ptype=crypten.mpc.arithmetic)
print("x_enc internal type:", x_enc.ptype)

#binary secret-shared tensors
y = torch.tensor([1, 2, 1], dtype=torch.int32)
y_enc = crypten.cryptensor(y, ptype=crypten.mpc.binary)
print("y_enc internal type:", y_enc.ptype)<jupyter_output>x_enc internal type: ptype.arithmetic
y_enc internal type: ptype.binary<jupyter_text>Arithmetic secret-sharingLet's look more closely at the `crypten.mpc.arithmetic` ptype. Most of the mathematical operations implemented by `CrypTensors` are implemented using arithmetic secret sharing. As such, `crypten.mpc.arithmetic` is the default ptype for newly generated `CrypTensors`. Let's begin by creating a new `CrypTensor` using `ptype=crypten.mpc.arithmetic` to enforce that the encryption is done via arithmetic secret sharing. We can print values of each share to confirm that values are being encrypted properly. To do so, we will need to create multiple parties to hold each share. We do this here using the `@mpc.run_multiprocess` function decorator, which we developed to execute crypten code from a single script (as we have in a Jupyter notebook). CrypTen follows the standard MPI programming model: it runs a separate process for each party, but each process runs an identical (complete) program. Each process has a `rank` variable to identify itself.Note that the sum of the two `_tensor` attributes below is equal to a scaled representation of the input. (Because MPC requires values to be integers, we scale input floats to a fixed-point encoding before encryption.)<jupyter_code>import crypten.mpc as mpc
import crypten.communicator as comm 

@mpc.run_multiprocess(world_size=2)
def examine_arithmetic_shares():
    x_enc = crypten.cryptensor([1, 2, 3], ptype=crypten.mpc.arithmetic)
    
    rank = comm.get().get_rank()
    crypten.print(f"\nRank {rank}:\n {x_enc}\n", in_order=True)
        
x = examine_arithmetic_shares()<jupyter_output>Rank 0:
 MPCTensor(
	_tensor=tensor([ 4977840465844292698,   234311463858409737, -8644375101282040029])
	plain_text=HIDDEN
	ptype=ptype.arithmetic
)


Rank 1:
 MPCTensor(
	_tensor=tensor([-4977840465844227162,  -234311463858278665,  8644375101282236637])
	plain_text=HIDDEN
	ptype=ptype.arithmetic
)<jupyter_text>Binary secret-sharingThe second type of secret-sharing implemented in CrypTen is binary or XOR secret-sharing. This type of secret-sharing allows greater efficiency in evaluating logical expressions. Let's look more closely at the `crypten.mpc.binary` ptype. Most of the logical operations implemented by `CrypTensors` are implemented using arithmetic secret sharing. We typically use this type of secret-sharing when we want to evaluate binary operators (i.e. `^ & | >> <<`, etc.) or logical operations (like comparitors).Let's begin by creating a new `CrypTensor` using `ptype=crypten.mpc.binary` to enforce that the encryption is done via binary secret sharing. We can print values of each share to confirm that values are being encrypted properly, as we did for arithmetic secret-shares.(Note that an xor of the two `_tensor` attributes below is equal to an unscaled version of input.)<jupyter_code>@mpc.run_multiprocess(world_size=2)
def examine_binary_shares():
    x_enc = crypten.cryptensor([2, 3], ptype=crypten.mpc.binary)
    
    rank = comm.get().get_rank()
    crypten.print(f"\nRank {rank}:\n {x_enc}\n", in_order=True)
        
x = examine_binary_shares()<jupyter_output>Rank 0:
 MPCTensor(
	_tensor=tensor([-3617348383499570248,  -581960226550774565])
	plain_text=HIDDEN
	ptype=ptype.binary
)


Rank 1:
 MPCTensor(
	_tensor=tensor([-3617348383499570246,  -581960226550774568])
	plain_text=HIDDEN
	ptype=ptype.binary
)<jupyter_text>Using Both Secret-sharing ProtocolsQuite often a mathematical function may need to use both additive and XOR secret sharing for efficient evaluation.  Functions that require conversions between sharing types include comparators (`>, >=, <, <=, ==, !=`) as well as functions derived from them (`abs, sign, relu`, etc.). For a full list of supported functions, please see the CrypTen documentation.CrypTen provides functionality that allows for the conversion of between ptypes. Conversion between ptypes can be done using the `.to()` function with a `crypten.ptype` input, or by calling the `.arithmetic()` and `.binary()` conversion functions.<jupyter_code>from crypten.mpc import MPCTensor

@mpc.run_multiprocess(world_size=2)
def examine_conversion():
    x = torch.tensor([1, 2, 3])
    rank = comm.get().get_rank()

    # create an MPCTensor with arithmetic secret sharing
    x_enc_arithmetic = MPCTensor(x, ptype=crypten.mpc.arithmetic)
    
    # To binary
    x_enc_binary = x_enc_arithmetic.to(crypten.mpc.binary)
    x_from_binary = x_enc_binary.get_plain_text()
    
    # print only once
    crypten.print("to(crypten.binary):")
    crypten.print(f"  ptype: {x_enc_binary.ptype}\n  plaintext: {x_from_binary}\n")

        
    # To arithmetic
    x_enc_arithmetic = x_enc_arithmetic.to(crypten.mpc.arithmetic)
    x_from_arithmetic = x_enc_arithmetic.get_plain_text()
    
    # print only once
    crypten.print("to(crypten.arithmetic):")
    crypten.print(f"  ptype: {x_enc_arithmetic.ptype}\n  plaintext: {x_from_arithmetic}\n")

        
z = examine_conversion()<jupyter_output>to(crypten.binary):
  ptype: ptype.binary
  plaintext: tensor([1., 2., 3.])

to(crypten.arithmetic):
  ptype: ptype.arithmetic
  plaintext: tensor([1., 2., 3.])<jupyter_text>Data SourcesCrypTen follows the standard MPI programming model: it runs a separate process for each party, but each process runs an identical (complete) program. Each process has a `rank` variable to identify itself.If the process with rank `i` is the source of data `x`, then `x` gets encrypted with `i` as its source value (denoted as `src`). However, MPI protocols require that both processes to provide a tensor with the same size as their input. CrypTen ignores all data provided from non-source processes when encrypting.In the next example, we'll show how to use the `rank` and `src` values to encrypt tensors. Here, we will have each of 3 parties generate a value `x` which is equal to its own `rank` value. Within the loop, 3 encrypted tensors are created, each with a different source. When these tensors are decrypted, we can verify that the tensors are generated using the tensor provided by the source process.(Note that `crypten.cryptensor` uses rank 0 as the default source if none is provided.)<jupyter_code>@mpc.run_multiprocess(world_size=3)
def examine_sources():
    # Create a different tensor on each rank
    rank = comm.get().get_rank()
    x = torch.tensor(rank)
    crypten.print(f"Rank {rank}: {x}", in_order=True)
    
    # 
    world_size = comm.get().get_world_size()
    for i in range(world_size):
        x_enc = crypten.cryptensor(x, src=i)
        z = x_enc.get_plain_text()
        
        # Only print from one process to avoid duplicates
        crypten.print(f"Source {i}: {z}")
        
x = examine_sources()<jupyter_output>Rank 0: 0
Rank 1: 1
Rank 2: 2
Source 0: 0.0
Source 1: 1.0
Source 2: 2.0