0

I would like to send a list of float list via nodejs and receive it in python using protobuff's repeated bytes type.

The graph helps to understand the problem:

The graph helps to understand the problem

I tried with this configuration and what I get on the python side is not really what I expect: tensors=[b'-TWW', b'-TWW', b'-TWW', b'-TWW']

Here is my test in node.

Client :

const PROTO_PATH = __dirname + '/route_guide.proto';
const async = require('async');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {
      keepCase: true,
      longs: String,
      enums: String,
      defaults: true,
      oneofs: true
    });
const routeguide = grpc.loadPackageDefinition(packageDefinition).routeguide;
const client = new routeguide.RouteGuide('localhost:50051',
    grpc.credentials.createInsecure());

function runJoin(callback) {
  const call = client.join();
  call.on('data', function(receivedMessage) {
    console.log('Got message "' + JSON.stringify(receivedMessage));
  });

  call.on('end', callback);

  messageToSend = {
    msg: 'parameters_res',
    parameters_res: {
      parameters: {
        tensors: [
          new Buffer.from(new Float64Array([45.1]).buffer),
          new Buffer.from(new Float64Array([45.1, 84.5, 87.9, 87.1]).buffer),
          new Buffer.from(new Float64Array([45.1, 84.5, 87.9, 87.1]).buffer),
          new Buffer.from(new Float64Array([45.1, 84.5, 87.9, 87.1]).buffer)
        ],
        tensor_type: 'numpy.ndarray'
      }
    }
  }
  console.log(messageToSend);
  console.log(messageToSend.parameters_res.parameters.tensors)

  call.write(messageToSend);
  call.end();
}

function main() {
  async.series([
      runJoin
  ]);
}

if (require.main === module) {
  main();
}

exports.runJoin = runJoin;

route_guide.proto:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
option objc_class_prefix = "RTG";

package routeguide;

service RouteGuide {
  rpc Join(stream ClientMessage) returns (stream ClientMessage) {}
}

message RouteNote {
  repeated bytes model = 1;
}

message ClientMessage {
  message Disconnect { Reason reason = 1; }
  message ParametersRes { Parameters parameters = 1; }
  oneof msg {
    Disconnect disconnect = 1;
    ParametersRes parameters_res = 2;
  }
}

message Parameters {
  repeated bytes tensors = 1;
  string tensor_type = 2;
}

enum Reason {
  UNKNOWN = 0;
  RECONNECT = 1;
  POWER_DISCONNECTED = 2;
  WIFI_UNAVAILABLE = 3;
  ACK = 4;
}

Server:

const PROTO_PATH = __dirname + '/route_guide.proto';
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
const routeguide = grpc.loadPackageDefinition(packageDefinition).routeguide;

function join(call) {
    call.on('data', function(receivedMessage) {
        console.log("SERVER RECEIVE:");
        console.log(receivedMessage);
        console.log(receivedMessage.parameters_res.parameters.tensors)
        for (const element of receivedMessage.parameters_res.parameters.tensors) {
            console.log(element)

        }
        call.write(receivedMessage);
    });
    call.on('end', function() {
        call.end();
    });
}

function getServer() {
  var server = new grpc.Server();
  server.addService(routeguide.RouteGuide.service, {
      join: join
  });
  return server;
}

if (require.main === module) {
  var routeServer = getServer();
  routeServer.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    routeServer.start()
  });
}

exports.getServer = getServer;

MyStartegy.py:

from logging import WARNING
from typing import Callable, Dict, List, Optional, Tuple, cast

import numpy as np
import flwr as fl

from flwr.common import (
    EvaluateIns,
    EvaluateRes,
    FitIns,
    FitRes,
    Parameters,
    Scalar,
    Weights,
)
from flwr.common.logger import log
from flwr.server.client_manager import ClientManager
from flwr.server.client_proxy import ClientProxy

from flwr.server.strategy.aggregate import aggregate, weighted_loss_avg
from flwr.server.strategy import Strategy
from tensorflow import Tensor

DEPRECATION_WARNING = """
DEPRECATION WARNING: deprecated `eval_fn` return format

    loss, accuracy

move to

    loss, {"accuracy": accuracy}

instead. Note that compatibility with the deprecated return format will be
removed in a future release.
"""

DEPRECATION_WARNING_INITIAL_PARAMETERS = """
DEPRECATION WARNING: deprecated initial parameter type

    flwr.common.Weights (i.e., List[np.ndarray])

will be removed in a future update, move to

    flwr.common.Parameters

instead. Use

    parameters = flwr.common.weights_to_parameters(weights)

to easily transform `Weights` to `Parameters`.
"""


class MyStrategy(Strategy):
    """Configurable FedAvg strategy implementation."""

    # pylint: disable=too-many-arguments,too-many-instance-attributes
    def __init__(
            self,
            fraction_fit: float = 0.1,
            fraction_eval: float = 0.1,
            min_fit_clients: int = 2,
            min_eval_clients: int = 2,
            min_available_clients: int = 2,
            eval_fn: Optional[
                Callable[[Weights], Optional[Tuple[float, Dict[str, Scalar]]]]
            ] = None,
            on_fit_config_fn: Optional[Callable[[int], Dict[str, Scalar]]] = None,
            on_evaluate_config_fn: Optional[Callable[[int], Dict[str, Scalar]]] = None,
            accept_failures: bool = True,
            initial_parameters: Optional[Parameters] = None,
    ) -> None:
        """Federated Averaging strategy.

        Implementation based on https://arxiv.org/abs/1602.05629

        Args:
            fraction_fit (float, optional): Fraction of clients used during
                training. Defaults to 0.1.
            fraction_eval (float, optional): Fraction of clients used during
                validation. Defaults to 0.1.
            min_fit_clients (int, optional): Minimum number of clients used
                during training. Defaults to 2.
            min_eval_clients (int, optional): Minimum number of clients used
                during validation. Defaults to 2.
            min_available_clients (int, optional): Minimum number of total
                clients in the system. Defaults to 2.
            eval_fn (Callable[[Weights], Optional[Tuple[float, float]]], optional):
                Function used for validation. Defaults to None.
            on_fit_config_fn (Callable[[int], Dict[str, Scalar]], optional):
                Function used to configure training. Defaults to None.
            on_evaluate_config_fn (Callable[[int], Dict[str, Scalar]], optional):
                Function used to configure validation. Defaults to None.
            accept_failures (bool, optional): Whether or not accept rounds
                containing failures. Defaults to True.
            initial_parameters (Parameters, optional): Initial global model parameters.
        """
        super().__init__()
        self.min_fit_clients = min_fit_clients
        self.min_eval_clients = min_eval_clients
        self.fraction_fit = fraction_fit
        self.fraction_eval = fraction_eval
        self.min_available_clients = min_available_clients
        self.eval_fn = eval_fn
        self.on_fit_config_fn = on_fit_config_fn
        self.on_evaluate_config_fn = on_evaluate_config_fn
        self.accept_failures = accept_failures
        self.initial_parameters = initial_parameters

    def __repr__(self) -> str:
        rep = f"FedAvg(accept_failures={self.accept_failures})"
        return rep

    def num_fit_clients(self, num_available_clients: int) -> Tuple[int, int]:
        """Return the sample size and the required number of available
        clients."""
        num_clients = int(num_available_clients * self.fraction_fit)
        return max(num_clients, self.min_fit_clients), self.min_available_clients

    def num_evaluation_clients(self, num_available_clients: int) -> Tuple[int, int]:
        """Use a fraction of available clients for evaluation."""
        num_clients = int(num_available_clients * self.fraction_eval)
        return max(num_clients, self.min_eval_clients), self.min_available_clients

    def initialize_parameters(
            self, client_manager: ClientManager
    ) -> Optional[Parameters]:
        """Initialize global model parameters."""
        initial_parameters = self.initial_parameters
        self.initial_parameters = None  # Don't keep initial parameters in memory
        if isinstance(initial_parameters, list):
            log(WARNING, DEPRECATION_WARNING_INITIAL_PARAMETERS)
            initial_parameters = self.weights_to_parameters(weights=initial_parameters)
        return initial_parameters

    def evaluate(
            self, parameters: Parameters
    ) -> Optional[Tuple[float, Dict[str, Scalar]]]:
        """Evaluate model parameters using an evaluation function."""
        if self.eval_fn is None:
            # No evaluation function provided
            return None
        weights = self.parameters_to_weights(parameters)
        eval_res = self.eval_fn(weights)
        if eval_res is None:
            return None
        loss, other = eval_res
        if isinstance(other, float):
            print(DEPRECATION_WARNING)
            metrics = {"accuracy": other}
        else:
            metrics = other
        return loss, metrics

    def configure_fit(
            self, rnd: int, parameters: Parameters, client_manager: ClientManager
    ) -> List[Tuple[ClientProxy, FitIns]]:
        """Configure the next round of training."""
        config = {}
        if self.on_fit_config_fn is not None:
            # Custom fit config function provided
            config = self.on_fit_config_fn(rnd)
        fit_ins = FitIns(parameters, config)

        # Sample clients
        sample_size, min_num_clients = self.num_fit_clients(
            client_manager.num_available()
        )
        clients = client_manager.sample(
            num_clients=sample_size, min_num_clients=min_num_clients
        )

        # Return client/config pairs
        return [(client, fit_ins) for client in clients]

    def configure_evaluate(
            self, rnd: int, parameters: Parameters, client_manager: ClientManager
    ) -> List[Tuple[ClientProxy, EvaluateIns]]:
        """Configure the next round of evaluation."""
        # Do not configure federated evaluation if fraction_eval is 0
        if self.fraction_eval == 0.0:
            return []

        # Parameters and config
        config = {}
        if self.on_evaluate_config_fn is not None:
            # Custom evaluation config function provided
            config = self.on_evaluate_config_fn(rnd)
        evaluate_ins = EvaluateIns(parameters, config)

        # Sample clients
        if rnd >= 0:
            sample_size, min_num_clients = self.num_evaluation_clients(
                client_manager.num_available()
            )
            clients = client_manager.sample(
                num_clients=sample_size, min_num_clients=min_num_clients
            )
        else:
            clients = list(client_manager.all().values())

        # Return client/config pairs
        return [(client, evaluate_ins) for client in clients]

    def aggregate_fit(
            self,
            rnd: int,
            results: List[Tuple[ClientProxy, FitRes]],
            failures: List[BaseException],
    ) -> Tuple[Optional[Parameters], Dict[str, Scalar]]:
        """Aggregate fit results using weighted average."""
        if not results:
            return None, {}
        # Do not aggregate if there are failures and failures are not accepted
        if not self.accept_failures and failures:
            return None, {}
        # Convert results
        print("\n\n aggregate_fit")
        print(results)
        weights_results = [
            (self.parameters_to_weights(fit_res.parameters), fit_res.num_examples)
            for client, fit_res in results
        ]
        print("weights_results")
        print(weights_results)
        return self.weights_to_parameters(aggregate(weights_results)), {}

    def aggregate_evaluate(
            self,
            rnd: int,
            results: List[Tuple[ClientProxy, EvaluateRes]],
            failures: List[BaseException],
    ) -> Tuple[Optional[float], Dict[str, Scalar]]:
        """Aggregate evaluation losses using weighted average."""
        if not results:
            return None, {}
        # Do not aggregate if there are failures and failures are not accepted
        if not self.accept_failures and failures:
            return None, {}
        loss_aggregated = weighted_loss_avg(
            [
                (evaluate_res.num_examples, evaluate_res.loss)
                for _, evaluate_res in results
            ]
        )
        return loss_aggregated, {}

    def weights_to_parameters(self, weights: Weights) -> Parameters:
        """Convert NumPy weights to parameters object."""
        print('weights_to_parameters')
        print(weights)
        tensors = [self.ndarray_to_bytes(ndarray) for ndarray in weights]
        return Parameters(tensors=tensors, tensor_type="numpy.nda")

    def parameters_to_weights(self, parameters: Parameters) -> Weights:
        """Convert parameters object to NumPy weights."""
        print('parameters_to_weights')
        print(parameters)
        return [self.bytes_to_ndarray(tensor) for tensor in parameters.tensors]

    # pylint: disable=R0201
    def ndarray_to_bytes(self, ndarray: np.ndarray) -> bytes:
        """Serialize NumPy array to bytes."""
        print('ndarray_to_bytes')
        print(ndarray)
        return None

    # pylint: disable=R0201
    def bytes_to_ndarray(self, tensor: bytes) -> np.ndarray:
        """Deserialize NumPy array from bytes."""
        print('bytes_to_ndarray')
        print(tensor)
        return None


# Start Flower server for three rounds of federated learning
fl.server.start_server(
    server_address='localhost:5006',
    config={"num_rounds": 2},
    strategy=MyStrategy()
)

Is Float64Array the right type?

What should I use on the python side to deserialize the data?

I specify that I cannot modify the proto.

Thank you in advance for your explanations.

  • Would be great if you can share a minimum reproducible code so that we can help better. – Clément Jean Mar 15 '22 at 00:40
  • I added my test code in node @ClémentJean – TrankilEmil Mar 15 '22 at 07:47
  • Could share the python code too? why both the client and the server are in JS? Don't you want to know how to deserialise in Python? I'm a bit confused – Clément Jean Mar 15 '22 at 09:26
  • Yes, sorry I forgot some information.... The problem seems complicated to me, I try to go step by step... The python server is in this project: https://github.com/adap/flower In my case, I can only redefine the "weights_to_parameters", "parameters_to_weights", "ndarray_to_bytes" and "bytes_to_ndarray" functions of the Strategy class which are the result of the grpc stream https://github.com/adap/flower/blob/main/src/py/flwr/server/strategy/strategy.py I add my strategy code in the question. @ClémentJean – TrankilEmil Mar 15 '22 at 09:53

0 Answers0