From 31c00d809d4fc0ace0ab47372d39311a25fe8679 Mon Sep 17 00:00:00 2001 From: Daniel Nata Nugraha <52073299+danielnugraha@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:23:41 +0200 Subject: [PATCH] Add and structure Swift SDK API reference (#1943) Co-authored-by: danielnugraha Co-authored-by: Taner Topal Co-authored-by: Daniel J. Beutel --- src/swift/flwr/Package.swift | 1 + .../flwr/Sources/Flower/Client/Client.swift | 23 ++++- .../Sources/Flower/Common/Exception.swift | 8 ++ .../Sources/Flower/Common/Parameter.swift | 25 ++++++ .../flwr/Sources/Flower/Common/Typing.swift | 84 +++++++++++++++++++ .../Sources/Flower/CoreML/MLFlwrClient.swift | 40 +++++++-- .../Flower/CoreML/MLFlwrClientModel.swift | 3 + .../flwr/Sources/Flower/GRPC/FlwrGRPC.swift | 50 ++++++++++- .../Flower/GRPC/InterceptorExtension.swift | 3 +- .../flwr/Sources/Flower/flwr.docc/flwr.md | 24 ++++++ 10 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 src/swift/flwr/Sources/Flower/flwr.docc/flwr.md diff --git a/src/swift/flwr/Package.swift b/src/swift/flwr/Package.swift index 85c1557a4489..9ebef2d89870 100644 --- a/src/swift/flwr/Package.swift +++ b/src/swift/flwr/Package.swift @@ -20,6 +20,7 @@ let package = Package( .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), .package(url: "https://github.com/kewlbear/NumPy-iOS.git", branch: "main"), .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/src/swift/flwr/Sources/Flower/Client/Client.swift b/src/swift/flwr/Sources/Flower/Client/Client.swift index 4ccdcdef39f6..8efb90d4694b 100644 --- a/src/swift/flwr/Sources/Flower/Client/Client.swift +++ b/src/swift/flwr/Sources/Flower/Client/Client.swift @@ -7,18 +7,33 @@ import Foundation -/// The protocol class for the client implementation. -/// It contains abstract functions required for processing the server statements. -/// The expected return types are derived from the defined return structure. +/// Protocol for Flower clients. +/// +/// ## Topics +/// +/// ### Functionalities +/// +/// - ``getParameters()`` +/// - ``getProperties(ins:)-4u0tf`` +/// - ``fit(ins:)`` +/// - ``evaluate(ins:)`` public protocol Client { + + /// Return the current local model parameters. func getParameters() -> GetParametersRes + + /// Return set of client properties. func getProperties(ins: GetPropertiesIns) -> GetPropertiesRes + + /// Refine the provided parameters using the locally held dataset. func fit(ins: FitIns) -> FitRes + + /// Evaluate the provided parameters using the locally held dataset. func evaluate(ins: EvaluateIns) -> EvaluateRes } -/// Extension to Client since per default GetPropertiesIns is not implemented. public extension Client { + /// Extension to Client since per default GetPropertiesIns is not implemented. func getProperties(ins: GetPropertiesIns) -> GetPropertiesRes { return GetPropertiesRes(properties: [:], status: Status(code: .getPropertiesNotImplemented, message: String())) } diff --git a/src/swift/flwr/Sources/Flower/Common/Exception.swift b/src/swift/flwr/Sources/Flower/Common/Exception.swift index 885ab68dcf52..cb35c1f52601 100644 --- a/src/swift/flwr/Sources/Flower/Common/Exception.swift +++ b/src/swift/flwr/Sources/Flower/Common/Exception.swift @@ -7,6 +7,14 @@ import Foundation +/// Set of Flower client exceptions. +/// +/// ## Topics +/// +/// ### Exceptions +/// +/// - ``TypeException(_:)`` +/// - ``UnknownServerMessage`` public enum FlowerException: Error { case TypeException(String) case UnknownServerMessage diff --git a/src/swift/flwr/Sources/Flower/Common/Parameter.swift b/src/swift/flwr/Sources/Flower/Common/Parameter.swift index 68af09f33d66..96e47ad1c286 100644 --- a/src/swift/flwr/Sources/Flower/Common/Parameter.swift +++ b/src/swift/flwr/Sources/Flower/Common/Parameter.swift @@ -16,6 +16,24 @@ import os let appDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! +/// A class responsible for (de)serializing model parameters. +/// +/// ## Topics +/// +/// ### Usage +/// +/// - ``shared`` +/// - ``finalize()`` +/// +/// ### Serialization +/// +/// - ``multiArrayToData(multiArray:)`` +/// - ``arrayToData(array:shape:)`` +/// +/// ### Deserialization +/// +/// - ``dataToMultiArray(data:)`` +/// - ``dataToArray(data:)`` @available(iOS 14.0, *) public class ParameterConverter { private var np: PythonObject? @@ -26,6 +44,8 @@ public class ParameterConverter { private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "flwr.Flower", category: String(describing: ParameterConverter.self)) + + /// ParameterConverter singleton object. public static let shared = ParameterConverter() private init() { @@ -126,6 +146,7 @@ public class ParameterConverter { return nil } + /// Deserialize bytes to MLMultiArray. public func dataToMultiArray(data: Data) -> MLMultiArray? { initGroup() let future = group?.next().submit { @@ -145,6 +166,7 @@ public class ParameterConverter { } + /// Serialize MLMultiArray to bytes. public func multiArrayToData(multiArray: MLMultiArray) -> Data? { initGroup() let future = group?.next().submit { @@ -164,6 +186,7 @@ public class ParameterConverter { } + /// Deserialize bytes into float array. public func dataToArray(data: Data) -> [Float]? { initGroup() let future = group?.next().submit { @@ -183,6 +206,7 @@ public class ParameterConverter { } + /// Serialize float array to bytes. public func arrayToData(array: [Float], shape: [Int16]) -> Data? { initGroup() let future = group?.next().submit { @@ -199,6 +223,7 @@ public class ParameterConverter { } } + /// Shutdown EventLoopGroup gracefully. public func finalize() { initGroup() let future = group?.next().submit { diff --git a/src/swift/flwr/Sources/Flower/Common/Typing.swift b/src/swift/flwr/Sources/Flower/Common/Typing.swift index f70b20575e0d..ea4e34944f44 100644 --- a/src/swift/flwr/Sources/Flower/Common/Typing.swift +++ b/src/swift/flwr/Sources/Flower/Common/Typing.swift @@ -7,13 +7,37 @@ import Foundation +/// Client status codes. +/// +/// ## Topics +/// +/// ### Status Codes +/// +/// - ``ok`` +/// - ``getParametersNotImplemented`` +/// - ``getPropertiesNotImplemented`` +/// - ``fitNotImplemented`` +/// - ``evaluateNotImplemented`` +/// - ``UNRECOGNIZED(_:)`` public enum Code: Equatable { typealias RawValue = Int + + /// Everything is okay status code. case ok + + /// No client implementation for getProperties status code. case getPropertiesNotImplemented + + /// No client implementation for getParameters status code. case getParametersNotImplemented + + /// No client implementation for fit status code. case fitNotImplemented + + /// No client implementation for evaluate status code. case evaluateNotImplemented + + /// Unrecognized client status code. case UNRECOGNIZED(Int) init(rawValue: Int) { @@ -39,13 +63,37 @@ public enum Code: Equatable { } } +/// Set of disconnect reasons for client. +/// +/// ## Topics +/// +/// ### Disconnect Reasons +/// +/// - ``unknown`` +/// - ``reconnect`` +/// - ``powerDisconnected`` +/// - ``wifiUnavailable`` +/// - ``ack`` +/// - ``UNRECOGNIZED(_:)`` public enum ReasonDisconnect { typealias RawValue = Int + + /// Unknown disconnect reason. case unknown // = 0 + + /// Reconnect disconnect reason. case reconnect // = 1 + + /// Power disconnected disconnect reason. case powerDisconnected // = 2 + + /// WiFi unavailable disconnect reason. case wifiUnavailable // = 3 + + /// Acknowledge disconnect reason. case ack // = 4 + + /// Unrecognized disconnect reason. case UNRECOGNIZED(Int) var rawValue: Int { @@ -60,17 +108,43 @@ public enum ReasonDisconnect { } } +/// Container for a set of recognised single quantity values. +/// +/// ## Topics +/// +/// ### Scalar Values +/// +/// - ``bool`` +/// - ``bytes`` +/// - ``float`` +/// - ``int`` +/// - ``str`` public struct Scalar: Equatable { + + /// Boolean scalar value. public var bool: Bool? + + /// Raw bytes scalar value. public var bytes: Data? + + /// Float scalar value. public var float: Float? + + /// Integer scalar value. public var int: Int? + + /// String scalar value. public var str: String? } +/// Typealias for a dictionary containing String and Scalar key-value pairs. public typealias Metrics = [String: Scalar] + +/// Typealias for a dictionary containing String and Scalar key-value pairs. public typealias Properties = [String: Scalar] + +/// Client status. public struct Status: Equatable { public static func == (lhs: Status, rhs: Status) -> Bool { if lhs.code == rhs.code && lhs.message == rhs.message { @@ -88,6 +162,7 @@ public struct Status: Equatable { } } +/// Parameters message. public struct Parameters: Equatable { public var tensors: [Data] public var tensorType: String @@ -98,6 +173,7 @@ public struct Parameters: Equatable { } } +/// Response when asked to return parameters. public struct GetParametersRes: Equatable { public var parameters: Parameters public var status: Status @@ -108,11 +184,13 @@ public struct GetParametersRes: Equatable { } } +/// Fit instructions for a client. public struct FitIns: Equatable { public var parameters: Parameters public var config: [String: Scalar] } +/// Fit response from a client. public struct FitRes: Equatable { public var parameters: Parameters public var numExamples: Int @@ -127,11 +205,13 @@ public struct FitRes: Equatable { } } +/// Evaluate instructions for a client. public struct EvaluateIns: Equatable { public var parameters: Parameters public var config: [String: Scalar] } +/// Evaluate response from a client. public struct EvaluateRes: Equatable { public static func == (lhs: EvaluateRes, rhs: EvaluateRes) -> Bool { if lhs.loss == rhs.loss && lhs.numExamples == rhs.numExamples && lhs.metrics == rhs.metrics && lhs.status == rhs.status { @@ -153,10 +233,12 @@ public struct EvaluateRes: Equatable { } } +/// Properties request for a client. public struct GetPropertiesIns: Equatable { public var config: Properties } +/// Properties response from a client. public struct GetPropertiesRes: Equatable { public var properties: Properties public var status: Status @@ -166,10 +248,12 @@ public struct GetPropertiesRes: Equatable { } } +/// Reconnect message from server to client. public struct Reconnect: Equatable { public var seconds: Int? } +/// Disconnect message from client to server. public struct Disconnect: Equatable { public var reason: String } diff --git a/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClient.swift b/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClient.swift index 69a0326b269f..4686a06be9c0 100644 --- a/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClient.swift +++ b/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClient.swift @@ -11,11 +11,41 @@ import NIOPosix import CoreML import os +/// Set of CoreML machine learning task options. public enum MLTask { case train case test } +/// Default Flower client implementation using CoreML as local training pipeline. +/// +/// ## Topics +/// +/// ### Usage +/// +/// - ``init(layerWrappers:dataLoader:compiledModelUrl:)`` +/// - ``getParameters()`` +/// - ``getProperties(ins:)`` +/// - ``fit(ins:)`` +/// - ``evaluate(ins:)`` +/// - ``closeEventLoopGroup()`` +/// +/// ### Data Loader +/// +/// - ``MLDataLoader`` +/// +/// ### Layer Wrapper +/// +/// - ``MLLayerWrapper`` +/// +/// ### Model Parameters +/// +/// - ``MLParameter`` +/// - ``ParameterConverter`` +/// +/// ### Task +/// +/// - ``MLTask`` @available(iOS 14.0, *) public class MLFlwrClient: Client { private var eventLoopGroup: EventLoopGroup? @@ -29,7 +59,7 @@ public class MLFlwrClient: Client { private let log = Logger(subsystem: Bundle.main.bundleIdentifier ?? "flwr.Flower", category: String(describing: MLFlwrClient.self)) - /// Inits the implementation of the Client protocol. + /// Creates an MLFlwrClient instance that conforms to Client protocol. /// /// - Parameters: /// - layerWrappers: A MLLayerWrapper struct that contains layer information. @@ -51,7 +81,7 @@ public class MLFlwrClient: Client { } } - /// Parses the parameters from the local model and returns them as GetParametersRes struct + /// Parses the parameters from the local model and returns them as GetParametersRes struct. /// /// - Returns: Parameters from the local model public func getParameters() -> GetParametersRes { @@ -61,7 +91,7 @@ public class MLFlwrClient: Client { return GetParametersRes(parameters: parameters, status: status) } - /// Calls the routine to fit the local model + /// Calls the routine to fit the local model. /// /// - Returns: The result from the local training, e.g., updated parameters public func fit(ins: FitIns) -> FitRes { @@ -72,7 +102,7 @@ public class MLFlwrClient: Client { return FitRes(parameters: parameters, numExamples: result.numSamples, status: status) } - /// Calls the routine to evaluate the local model + /// Calls the routine to evaluate the local model. /// /// - Returns: The result from the evaluation, e.g., loss public func evaluate(ins: EvaluateIns) -> EvaluateRes { @@ -140,7 +170,7 @@ public class MLFlwrClient: Client { return result ?? MLResult(loss: 1, numSamples: 0, accuracy: 0) } - /// Closes the initiated group of event-loop + /// Closes the initiated group of event-loop. public func closeEventLoopGroup() { do { try self.eventLoopGroup?.syncShutdownGracefully() diff --git a/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClientModel.swift b/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClientModel.swift index 6b50d59fdf5d..e9b76672f8e9 100644 --- a/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClientModel.swift +++ b/src/swift/flwr/Sources/Flower/CoreML/MLFlwrClientModel.swift @@ -9,6 +9,7 @@ import Foundation import CoreML import os +/// Container for train and test dataset. public struct MLDataLoader { public let trainBatchProvider: MLBatchProvider public let testBatchProvider: MLBatchProvider @@ -19,6 +20,7 @@ public struct MLDataLoader { } } +/// Container for neural network layer information. public struct MLLayerWrapper { let shape: [Int16] let name: String @@ -39,6 +41,7 @@ struct MLResult { let accuracy: Double } +/// A class responsible for loading and retrieving model parameters to and from the CoreML model. @available(iOS 14.0, *) public class MLParameter { private var parameterConverter = ParameterConverter.shared diff --git a/src/swift/flwr/Sources/Flower/GRPC/FlwrGRPC.swift b/src/swift/flwr/Sources/Flower/GRPC/FlwrGRPC.swift index 775a4c23360b..715b791bfad0 100644 --- a/src/swift/flwr/Sources/Flower/GRPC/FlwrGRPC.swift +++ b/src/swift/flwr/Sources/Flower/GRPC/FlwrGRPC.swift @@ -10,6 +10,52 @@ import GRPC import NIOCore import os +/// A class that manages gRPC connection from client to the server. +/// +/// ## Topics +/// +/// ### Usage +/// +/// - ``init(serverHost:serverPort:extendedInterceptor:)`` +/// - ``startFlwrGRPC(client:)`` +/// - ``startFlwrGRPC(client:completion:)`` +/// - ``abortGRPCConnection(reasonDisconnect:completion:)`` +/// - ``InterceptorExtension`` +/// - ``GRPCPartWrapper`` +/// +/// ### GetParameters +/// +/// - ``Parameters`` +/// - ``GetParametersRes`` +/// +/// ### GetProperties +/// +/// - ``GetPropertiesIns`` +/// - ``GetPropertiesRes`` +/// +/// ### Fit +/// +/// - ``FitIns`` +/// - ``FitRes`` +/// +/// ### Evaluate +/// +/// - ``EvaluateIns`` +/// - ``EvaluateRes`` +/// +/// ### Reconnect +/// +/// - ``Reconnect`` +/// - ``Disconnect`` +/// - ``ReasonDisconnect`` +/// +/// ### Supporting Messages +/// - ``Scalar`` +/// - ``Status`` +/// - ``Code`` +/// +/// ### Exceptions +/// - ``FlowerException`` @available(iOS 14.0, *) public class FlwrGRPC { typealias GRPCResponse = (Flwr_Proto_ClientMessage, Int, Bool) @@ -54,7 +100,7 @@ public class FlwrGRPC { } } - /// Opens the bidirectional stream with the server and starts sending messages. + /// Start a Flower client node which connects to a Flower server. /// /// - Parameters: /// - client: The implementation of the Client which includes the machine learning routines and results. @@ -62,7 +108,7 @@ public class FlwrGRPC { startFlwrGRPC(client: client) {} } - /// Opens the bidirectional stream with the server and starts sending messages. + /// Start a Flower client node which connects to a Flower server. /// /// - Parameters: /// - client: The implementation of the Client which includes the machine learning routines and results. diff --git a/src/swift/flwr/Sources/Flower/GRPC/InterceptorExtension.swift b/src/swift/flwr/Sources/Flower/GRPC/InterceptorExtension.swift index d2ccc584f86d..744edb5f003b 100644 --- a/src/swift/flwr/Sources/Flower/GRPC/InterceptorExtension.swift +++ b/src/swift/flwr/Sources/Flower/GRPC/InterceptorExtension.swift @@ -8,14 +8,15 @@ import Foundation import GRPC +/// Extension for gRPC Interceptor in a stream. public protocol InterceptorExtension { func receive(part: GRPCPartWrapper) func send(part: GRPCPartWrapper) } +/// Represents different parts of a gRPC message public enum GRPCPartWrapper { case metadata(header: String) case message(content: String) case end(status: GRPCStatus?, trailers: String?) - } diff --git a/src/swift/flwr/Sources/Flower/flwr.docc/flwr.md b/src/swift/flwr/Sources/Flower/flwr.docc/flwr.md new file mode 100644 index 000000000000..3f293f89e231 --- /dev/null +++ b/src/swift/flwr/Sources/Flower/flwr.docc/flwr.md @@ -0,0 +1,24 @@ +# ``flwr`` + +Seamlessly integrate Flower federated learning framework into your existing machine learning project. + +## Overview + +Flower Swift client SDK provides tools and functionalities for federating your machine learning project easily using Flower. The framework provides protocol to create a Flower ``Client``, and a default ``MLFlwrClient`` implementation that uses CoreML as its local training pipeline. You can create your own custom Flower clients by conforming to the provided protocol. + +You can connect to a Flower server using GRPC by instantiating ``FlwrGRPC`` and providing a correct hostname and port. To start a gRPC connection, you can call the function ``FlwrGRPC/startFlwrGRPC(client:)`` and provide a ``Client`` as its argument. + + +## Topics + +### Client + +- ``Client`` + +### GRPC + +- ``FlwrGRPC`` + +### CoreML Client + +- ``MLFlwrClient``