Skip to content

Commit

Permalink
Merge pull request #29 from lsd-ucsc/feat/mlv
Browse files Browse the repository at this point in the history
Multiply Located Values / Census Polymorphism
  • Loading branch information
shumbo authored Dec 2, 2024
2 parents 1dfce23 + 187e914 commit 9d21a58
Show file tree
Hide file tree
Showing 25 changed files with 3,207 additions and 460 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
[workspace]

edition = "2021"
members = ["chorus_lib", "chorus_derive"]
resolver = "2"

[workspace.package]
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Shun Kashiwa <[email protected]>"]
homepage = "https://lsd-ucsc.github.io/ChoRus/"
Expand Down
3 changes: 3 additions & 0 deletions chorus_book/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ multilingual = false
src = "src"
title = "ChoRus"

[rust]
edition = "2021"

[output.html]
git-repository-url = "https://github.com/lsd-ucsc/Chorus"
git-repository-icon = "fa-github"
Expand Down
4 changes: 2 additions & 2 deletions chorus_book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
- [Transport](./guide-transport.md)
- [Projector](./guide-projector.md)
- [Input and Output](./guide-input-and-output.md)
- [Runner](./guide-runner.md)
- [Higher-order Choreography](./guide-higher-order-choreography.md)
- [Location Polymorphism](./guide-location-polymorphism.md)
- [Choreographic Enclave and Efficient Conditional](./guide-enclave.md)
- [Runner](./guide-runner.md)
- [Efficient Conditionals with Enclaves and MLVs](./guide-efficient-conditionals.md)
- [Links](./links.md)
37 changes: 37 additions & 0 deletions chorus_book/src/guide-choreography.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,43 @@ if num == 42 {
}
```

### Multicast

The `multicast` operator is similar to the `broadcast` operator, but it allows you to manually specify the recipients instead of sending the value to all locations. The operator returns a `MultiplyLocated` value that is available at the all recipient locations.

```rust
{{#include ./header.txt}}
#
# struct HelloWorldChoreography;
# impl Choreography for HelloWorldChoreography {
# type L = LocationSet!(Alice, Bob, Carol);
# fn run(self, op: &impl ChoreoOp<Self::L>) {
// This value is only available at Alice
let num_at_alice: Located<i32, Alice> = op.locally(Alice, |_| {
42
});

// Send the value from Alice to Bob and Carol
let num_at_bob_and_carol: MultiplyLocated<i32, LocationSet!(Bob, Carol)> =
op.multicast(Alice, <LocationSet!(Bob, Carol)>::new(), &num_at_alice);

// Bob and Carol can now access the value
op.locally(Bob, |un| {
let num_at_bob: &i32 = un.unwrap(&num_at_bob_and_carol);
println!("The number at Bob is {}", num_at_bob);
});
op.locally(Carol, |un| {
let num_at_carol: &i32 = un.unwrap(&num_at_bob_and_carol);
println!("The number at Carol is {}", num_at_carol);
});
# }
# }
```

The second parameter of the `multicast` is a value of the `LocationSet` type. You can use the `new()` method of the `LocationSet` type to obtain a value representation of the location set.

Both `Bob` and `Carol` can access the value sent from `Alice` inside their local computation using the same `unwrap` method.

### Note on invalid values for Choreography::L

You'll get a compile error if you try to work with a `ChoreographyLocation` that is not a member of `L`.
Expand Down
302 changes: 302 additions & 0 deletions chorus_book/src/guide-efficient-conditionals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
# Efficient Conditionals with Enclaves and MLVs

## `broadcast` incurs unnecessary communication

In [the previous section](./guide-choreography.html#broadcast), we discussed how the `broadcast` operator can be used to implement a conditional behavior in a choreography. In short, the `broadcast` operator sends a located value from a source location to all other locations, making the value available at all locations. The resulting value is a normal (not `Located`) value and it can be used to make a branch.

However, the `broadcast` operator can incur unnecessary communication when not all locations need to receive the value. Consider a simple key-value store where a *client* sends either a `Get` or `Put` request to a *primary* server, and the primary server forwards the request to a *backup* server if the request is a `Put`. The backup server does not need to receive the request if the request is a `Get`.

Using the `broadcast` operator, this protocol can be implemented as follows:

```rust
{{#include ./header.txt}}
#
# fn read_request() -> Request {
# Request::Put("key".to_string(), "value".to_string())
# }
# fn get_value(key: &Key) -> Option<Value> {
# Some("value".to_string())
# }
# fn set_value(key: &Key, value: &Value) {
# println!("Saved key: {} and value: {}", key, value);
# }
#
#[derive(ChoreographyLocation)]
struct Client;

#[derive(ChoreographyLocation)]
struct Primary;

#[derive(ChoreographyLocation)]
struct Backup;

type Key = String;
type Value = String;

#[derive(Serialize, Deserialize)]
enum Request {
Get(Key),
Put(Key, Value),
}

#[derive(Serialize, Deserialize)]
enum Response {
GetOk(Option<Value>),
PutOk,
}

struct KeyValueStoreChoreography;

impl Choreography<Located<Response, Client>> for KeyValueStoreChoreography {
type L = LocationSet!(Client, Primary, Backup);
fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<Response, Client> {
// Read the request from the client
let request_at_client: Located<Request, Client> = op.locally(Client, |_| read_request());
// Send the request to the primary server
let request_at_primary: Located<Request, Primary> =
op.comm(Client, Primary, &request_at_client);
// Check if the request is a `Put`
let is_put_at_primary: Located<bool, Primary> = op.locally(Primary, |un| {
matches!(un.unwrap(&request_at_primary), Request::Put(_, _))
});
// Broadcast the `is_put_at_primary` to all locations so it can be used for branching
let is_put: bool = op.broadcast(Primary, is_put_at_primary); // <-- Incurs unnecessary communication
// Depending on the request, set or get the value
let response_at_primary = if is_put {
let request_at_backup: Located<Request, Backup> =
op.comm(Primary, Backup, &request_at_primary);
op.locally(Backup, |un| match un.unwrap(&request_at_backup) {
Request::Put(key, value) => set_value(key, value),
_ => (),
});
op.locally(Primary, |_| Response::PutOk)
} else {
op.locally(Primary, |un| {
let key = match un.unwrap(&request_at_primary) {
Request::Get(key) => key,
_ => &"".to_string(),
};
Response::GetOk(get_value(key))
})
};
// Send the response from the primary to the client
let response_at_client = op.comm(Primary, Client, &response_at_primary);
response_at_client
}
}
```

While this implementation works, it incurs unnecessary communication. When we branch on `is_put`, we broadcast the value to all locations. This is necessary to make sure that the value is available at all locations so it can be used as a normal, non-located value. However, notice that the client does not need to receive the value. Regardless of whether the request is a `Put` or `Get`, the client should wait for the response from the primary server.

## Changing the census with `enclave`

To avoid unnecessary communication, we can use the `enclave` operator. The `enclave` operator is similar to [the `call` operator](./guide-higher-order-choreography.html) but executes a sub-choreography only at locations that are included in its location set. Inside the sub-choreography, `broadcast` only sends the value to the locations that are included in the location set. This allows us to avoid unnecessary communication.

Let's refactor the previous example using the `enclave` operator. We define a sub-choreography `HandleRequestChoreography` that describes how the primary and backup servers (but not the client) handle the request and use the `enclave` operator to execute the sub-choreography.

```rust
{{#include ./header.txt}}
#
# fn read_request() -> Request {
# Request::Put("key".to_string(), "value".to_string())
# }
# fn get_value(key: &Key) -> Option<Value> {
# Some("value".to_string())
# }
# fn set_value(key: &Key, value: &Value) {
# println!("Saved key: {} and value: {}", key, value);
# }
#
# #[derive(ChoreographyLocation)]
# struct Client;
#
# #[derive(ChoreographyLocation)]
# struct Primary;
#
# #[derive(ChoreographyLocation)]
# struct Backup;
#
# type Key = String;
# type Value = String;
#
# #[derive(Serialize, Deserialize)]
# enum Request {
# Get(Key),
# Put(Key, Value),
# }
#
# #[derive(Serialize, Deserialize)]
# enum Response {
# GetOk(Option<Value>),
# PutOk,
# }
#
struct HandleRequestChoreography {
request: Located<Request, Primary>,
}

// This sub-choreography describes how the primary and backup servers handle the request
impl Choreography<Located<Response, Primary>> for HandleRequestChoreography {
type L = LocationSet!(Primary, Backup);
fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<Response, Primary> {
let is_put_request: Located<bool, Primary> = op.locally(Primary, |un| {
matches!(un.unwrap(&self.request), Request::Put(_, _))
});
let is_put: bool = op.broadcast(Primary, is_put_request);
let response_at_primary = if is_put {
let request_at_backup: Located<Request, Backup> =
op.comm(Primary, Backup, &self.request);
op.locally(Backup, |un| match un.unwrap(&request_at_backup) {
Request::Put(key, value) => set_value(key, value),
_ => (),
});
op.locally(Primary, |_| Response::PutOk)
} else {
op.locally(Primary, |un| {
let key = match un.unwrap(&self.request) {
Request::Get(key) => key,
_ => &"".to_string(),
};
Response::GetOk(get_value(key))
})
};
response_at_primary
}
}

struct KeyValueStoreChoreography;

impl Choreography<Located<Response, Client>> for KeyValueStoreChoreography {
type L = LocationSet!(Client, Primary, Backup);
fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<Response, Client> {
let request_at_client: Located<Request, Client> = op.locally(Client, |_| read_request());
let request_at_primary: Located<Request, Primary> =
op.comm(Client, Primary, &request_at_client);
// Execute the sub-choreography only at the primary and backup servers
let response: MultiplyLocated<Located<Response, Primary>, LocationSet!(Primary, Backup)> =
op.enclave(HandleRequestChoreography {
request: request_at_primary,
});
let response_at_primary: Located<Response, Primary> = response.flatten();
let response_at_client = op.comm(Primary, Client, &response_at_primary);
response_at_client
}
}
```

In this refactored version, the `HandleRequestChoreography` sub-choreography describes how the primary and backup servers handle the request. The `enclave` operator executes the sub-choreography only at the primary and backup servers. The `broadcast` operator inside the sub-choreography sends the value only to the primary and backup servers, avoiding unnecessary communication.

The `enclave` operator returns a return value of the sub-choreography wrapped as a `MultiplyLocated` value. Since `HandleRequestChoreography` returns a `Located<Response, Primary>`, the return value of the `enclave` operator is a `MultiplyLocated<Located<Response, Primary>, LocationSet!(Primary, Backup)>`. To get the located value at the primary server, we can use the `locally` operator to unwrap the `MultiplyLocated` value on the primary. Since this is a common pattern, we provide the `flatten` method on `MultiplyLocated` to simplify this operation.

With the `enclave` operator, we can avoid unnecessary communication and improve the efficiency of the choreography.

## Reusing Knowledge of Choice in Enclaves

The key idea behind the `enclave` operator is that a normal value inside a choreography is equivalent to a (multiply) located value at all locations executing the choreography. This is why a normal value in a sub-choreography becomes a multiply located value at all locations executing the sub-choreography when returned from the `enclave` operator.

It is possible to perform this conversion in the opposite direction as well. If we have a multiply located value at some locations, and those are the only locations executing the choreography, then we can obtain a normal value out of the multiply located value. This is useful when we want to reuse the already known information about a choice in an enclave.

Inside a choreography, we can use the `naked` operator to convert a multiply located value at locations `S` to a normal value if the census of the choreography is a subset of `S`.

For example, the above choreography can be written as follows:

```rust
{{#include ./header.txt}}
#
# fn read_request() -> Request {
# Request::Put("key".to_string(), "value".to_string())
# }
# fn get_value(key: &Key) -> Option<Value> {
# Some("value".to_string())
# }
# fn set_value(key: &Key, value: &Value) {
# println!("Saved key: {} and value: {}", key, value);
# }
#
# #[derive(ChoreographyLocation)]
# struct Client;
#
# #[derive(ChoreographyLocation)]
# struct Primary;
#
# #[derive(ChoreographyLocation)]
# struct Backup;
#
# type Key = String;
# type Value = String;
#
# #[derive(Serialize, Deserialize)]
# enum Request {
# Get(Key),
# Put(Key, Value),
# }
#
# #[derive(Serialize, Deserialize)]
# enum Response {
# GetOk(Option<Value>),
# PutOk,
# }
#
struct HandleRequestChoreography {
request: Located<Request, Primary>,
is_put: MultiplyLocated<bool, LocationSet!(Primary, Backup)>,
}

impl Choreography<Located<Response, Primary>> for HandleRequestChoreography {
type L = LocationSet!(Primary, Backup);
fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<Response, Primary> {
// obtain a normal boolean because {Primary, Backup} is the census of the choreography
let is_put: bool = op.naked(self.is_put);
let response_at_primary = if is_put {
// ...
# let request_at_backup: Located<Request, Backup> =
# op.comm(Primary, Backup, &self.request);
# op.locally(Backup, |un| match un.unwrap(&request_at_backup) {
# Request::Put(key, value) => set_value(key, value),
# _ => (),
# });
# op.locally(Primary, |_| Response::PutOk)
} else {
// ...
# op.locally(Primary, |un| {
# let key = match un.unwrap(&self.request) {
# Request::Get(key) => key,
# _ => &"".to_string(),
# };
# Response::GetOk(get_value(key))
# })
};
response_at_primary
}
}

struct KeyValueStoreChoreography;

impl Choreography<Located<Response, Client>> for KeyValueStoreChoreography {
type L = LocationSet!(Client, Primary, Backup);
fn run(self, op: &impl ChoreoOp<Self::L>) -> Located<Response, Client> {
let request_at_client: Located<Request, Client> = op.locally(Client, |_| read_request());
let request_at_primary: Located<Request, Primary> =
op.comm(Client, Primary, &request_at_client);
let is_put_at_primary: Located<bool, Primary> = op.locally(Primary, |un| {
matches!(un.unwrap(&request_at_primary), Request::Put(_, _))
});
// get a MLV by multicasting the boolean to the census of the sub-choreography
let is_put: MultiplyLocated<bool, LocationSet!(Primary, Backup)> = op.multicast(
Primary,
<LocationSet!(Primary, Backup)>::new(),
&is_put_at_primary,
);
let response: MultiplyLocated<Located<Response, Primary>, LocationSet!(Primary, Backup)> =
op.enclave(HandleRequestChoreography {
is_put,
request: request_at_primary,
});
let response_at_primary: Located<Response, Primary> = response.flatten();
let response_at_client = op.comm(Primary, Client, &response_at_primary);
response_at_client
}
}
```

In this version, we first `multicast` the boolean value to the census of the sub-choreography (`Primary` and `Client`) and we pass the MLV to the sub-choreography. Inside the sub-choreography, we use the `naked` operator to obtain a normal boolean value. This allows us to reuse the already known information about the choice in the sub-choreography.
Loading

0 comments on commit 9d21a58

Please sign in to comment.