diff --git a/sdk/flutter/analysis_options.yaml b/sdk/flutter/analysis_options.yaml new file mode 100644 index 0000000..2d9eedd --- /dev/null +++ b/sdk/flutter/analysis_options.yaml @@ -0,0 +1,84 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Style rules + - always_declare_return_types + - avoid_empty_else + - avoid_relative_lib_imports + - avoid_returning_null_for_void + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - close_sinks + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - library_names + - library_prefixes + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - overridden_fields + - package_names + - package_prefixed_library_names + - prefer_adjacent_string_concatenation + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_declarations + - prefer_contains + - prefer_final_fields + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_generic_function_type_aliases + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - sort_child_properties_last + - test_types_in_equals + - throw_in_finally + - type_init_formals + - unawaited_futures + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_new + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - use_function_type_syntax_for_parameters + - use_rethrow_when_possible + - valid_regexps + - void_checks + +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + errors: + missing_required_param: error + missing_return: error + todo: ignore diff --git a/sdk/flutter/example/example.dart b/sdk/flutter/example/example.dart new file mode 100644 index 0000000..524e757 --- /dev/null +++ b/sdk/flutter/example/example.dart @@ -0,0 +1,178 @@ +import 'dart:io'; + +import 'package:synor_compute/synor_compute.dart'; + +/// Example usage of Synor Compute SDK for Flutter/Dart +void main() async { + // Initialize client with API key + final client = SynorCompute( + apiKey: Platform.environment['SYNOR_API_KEY'] ?? 'your-api-key', + // Optional: customize defaults + defaultProcessor: ProcessorType.auto, + defaultPrecision: Precision.fp32, + defaultPriority: Priority.normal, + ); + + try { + // Check service health + final isHealthy = await client.healthCheck(); + print('Service healthy: $isHealthy\n'); + + // Example 1: Matrix multiplication + await matrixMultiplicationExample(client); + + // Example 2: Tensor operations + await tensorOperationsExample(client); + + // Example 3: LLM inference + await llmInferenceExample(client); + + // Example 4: Streaming inference + await streamingInferenceExample(client); + + // Example 5: Pricing and usage + await pricingExample(client); + } finally { + // Always dispose client to release resources + client.dispose(); + } +} + +/// Matrix multiplication example +Future matrixMultiplicationExample(SynorCompute client) async { + print('=== Matrix Multiplication ==='); + + // Create random matrices + final a = Tensor.rand([256, 512]); + final b = Tensor.rand([512, 256]); + + print('A: ${a.shape}'); + print('B: ${b.shape}'); + + // Perform multiplication on GPU with FP16 precision + final result = await client.matmul( + a, + b, + options: MatMulOptions( + precision: Precision.fp16, + processor: ProcessorType.gpu, + priority: Priority.high, + ), + ); + + if (result.isSuccess) { + print('Result: ${result.result!.shape}'); + print('Execution time: ${result.executionTimeMs}ms'); + print('Cost: \$${result.cost?.toStringAsFixed(6)}'); + print('Processor: ${result.processor?.value}'); + } else { + print('Error: ${result.error}'); + } + print(''); +} + +/// Local tensor operations example +Future tensorOperationsExample(SynorCompute client) async { + print('=== Tensor Operations ==='); + + // Create tensors + final x = Tensor.randn([100], mean: 0.0, std: 1.0); + print('Random normal tensor: mean=${x.mean().toStringAsFixed(4)}, ' + 'std=${x.std().toStringAsFixed(4)}'); + + // Create identity matrix + final eye = Tensor.eye(4); + print('Identity matrix:\n${eye.toNestedList()}'); + + // Create linspace + final linspace = Tensor.linspace(0, 10, 5); + print('Linspace [0, 10, 5]: ${linspace.toNestedList()}'); + + // Reshape operations + final matrix = Tensor.arange(0, 12).reshape([3, 4]); + print('Reshaped [0..12] to [3,4]:\n${matrix.toNestedList()}'); + + // Transpose + final transposed = matrix.transpose(); + print('Transposed to ${transposed.shape}'); + + // Activations + final input = Tensor(shape: [5], data: [-2.0, -1.0, 0.0, 1.0, 2.0]); + print('ReLU of $input: ${input.relu().toNestedList()}'); + print('Sigmoid of $input: ${input.sigmoid().toNestedList()}'); + + // Softmax + final logits = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]); + print('Softmax of $logits: ${logits.softmax().toNestedList()}'); + + print(''); +} + +/// LLM inference example +Future llmInferenceExample(SynorCompute client) async { + print('=== LLM Inference ==='); + + final result = await client.inference( + 'llama-3-70b', + 'What is the capital of France? Answer in one word.', + options: InferenceOptions( + maxTokens: 10, + temperature: 0.1, + processor: ProcessorType.lpu, // Use LPU for LLM + ), + ); + + if (result.isSuccess) { + print('Response: ${result.result}'); + print('Time: ${result.executionTimeMs}ms'); + } else { + print('Error: ${result.error}'); + } + print(''); +} + +/// Streaming inference example +Future streamingInferenceExample(SynorCompute client) async { + print('=== Streaming Inference ==='); + print('Response: '); + + await for (final token in client.inferenceStream( + 'llama-3-70b', + 'Write a short poem about distributed computing.', + options: InferenceOptions( + maxTokens: 100, + temperature: 0.7, + ), + )) { + stdout.write(token); + } + + print('\n'); +} + +/// Pricing and usage example +Future pricingExample(SynorCompute client) async { + print('=== Pricing Information ==='); + + final pricing = await client.getPricing(); + + print('Current spot prices:'); + for (final p in pricing) { + print(' ${p.processor.value.toUpperCase().padRight(8)}: ' + '\$${p.pricePerSecond.toStringAsFixed(6)}/sec, ' + '${p.availableUnits} units available, ' + '${p.utilizationPercent.toStringAsFixed(1)}% utilized'); + } + + print(''); + + // Get usage stats + final usage = await client.getUsage(); + print('Usage Statistics:'); + print(' Total jobs: ${usage.totalJobs}'); + print(' Completed: ${usage.completedJobs}'); + print(' Failed: ${usage.failedJobs}'); + print(' Total compute time: ${usage.totalComputeSeconds.toStringAsFixed(2)}s'); + print(' Total cost: \$${usage.totalCost.toStringAsFixed(4)}'); + print(''); +} diff --git a/sdk/flutter/lib/src/client.dart b/sdk/flutter/lib/src/client.dart new file mode 100644 index 0000000..5e401a5 --- /dev/null +++ b/sdk/flutter/lib/src/client.dart @@ -0,0 +1,541 @@ +/// Main client for Synor Compute SDK +library synor_compute.client; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import 'job.dart'; +import 'tensor.dart'; +import 'types.dart'; + +/// Main client for interacting with Synor Compute +class SynorCompute { + final SynorConfig _config; + final http.Client _httpClient; + bool _isDisposed = false; + + /// Creates a new Synor Compute client + SynorCompute({ + required String apiKey, + String baseUrl = 'https://compute.synor.io', + Duration timeout = const Duration(seconds: 30), + int maxRetries = 3, + ProcessorType defaultProcessor = ProcessorType.auto, + Precision defaultPrecision = Precision.fp32, + Priority defaultPriority = Priority.normal, + http.Client? httpClient, + }) : _config = SynorConfig( + apiKey: apiKey, + baseUrl: baseUrl, + timeout: timeout, + maxRetries: maxRetries, + defaultProcessor: defaultProcessor, + defaultPrecision: defaultPrecision, + defaultPriority: defaultPriority, + ), + _httpClient = httpClient ?? http.Client(); + + /// Creates a client from configuration + SynorCompute.fromConfig(SynorConfig config, {http.Client? httpClient}) + : _config = config, + _httpClient = httpClient ?? http.Client(); + + Map get _headers => { + 'Authorization': 'Bearer ${_config.apiKey}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-SDK-Version': 'flutter/0.1.0', + }; + + void _checkDisposed() { + if (_isDisposed) { + throw StateError('Client has been disposed'); + } + } + + /// Perform matrix multiplication + Future> matmul( + Tensor a, + Tensor b, { + MatMulOptions? options, + }) async { + _checkDisposed(); + + final opts = options ?? const MatMulOptions(); + final body = { + 'operation': 'matmul', + 'inputs': { + 'a': a.toJson(), + 'b': b.toJson(), + }, + 'options': { + 'precision': (opts.precision ?? _config.defaultPrecision).value, + 'processor': (opts.processor ?? _config.defaultProcessor).value, + 'priority': (opts.priority ?? _config.defaultPriority).value, + 'transpose_a': opts.transposeA, + 'transpose_b': opts.transposeB, + }, + }; + + return _submitAndWait( + body, + (result) => Tensor.fromJson(result as Map), + ); + } + + /// Perform 2D convolution + Future> conv2d( + Tensor input, + Tensor kernel, { + Conv2dOptions? options, + }) async { + _checkDisposed(); + + final opts = options ?? const Conv2dOptions(); + final body = { + 'operation': 'conv2d', + 'inputs': { + 'input': input.toJson(), + 'kernel': kernel.toJson(), + }, + 'options': { + ...opts.toJson(), + 'precision': (opts.precision ?? _config.defaultPrecision).value, + 'processor': (opts.processor ?? _config.defaultProcessor).value, + 'priority': (opts.priority ?? _config.defaultPriority).value, + }, + }; + + return _submitAndWait( + body, + (result) => Tensor.fromJson(result as Map), + ); + } + + /// Perform flash attention + Future> attention( + Tensor query, + Tensor key, + Tensor value, { + required AttentionOptions options, + }) async { + _checkDisposed(); + + final body = { + 'operation': 'flash_attention', + 'inputs': { + 'query': query.toJson(), + 'key': key.toJson(), + 'value': value.toJson(), + }, + 'options': { + ...options.toJson(), + 'precision': (options.precision ?? _config.defaultPrecision).value, + 'processor': (options.processor ?? _config.defaultProcessor).value, + 'priority': (options.priority ?? _config.defaultPriority).value, + }, + }; + + return _submitAndWait( + body, + (result) => Tensor.fromJson(result as Map), + ); + } + + /// Run LLM inference + Future> inference( + String model, + String input, { + InferenceOptions? options, + }) async { + _checkDisposed(); + + final opts = options ?? const InferenceOptions(); + final body = { + 'operation': 'inference', + 'model': model, + 'input': input, + 'options': { + ...opts.toJson(), + 'processor': (opts.processor ?? _config.defaultProcessor).value, + 'priority': (opts.priority ?? _config.defaultPriority).value, + }, + }; + + return _submitAndWait( + body, + (result) => result['text'] as String, + ); + } + + /// Stream LLM inference with real-time token output + Stream inferenceStream( + String model, + String input, { + InferenceOptions? options, + }) async* { + _checkDisposed(); + + final opts = options ?? const InferenceOptions(); + final body = { + 'operation': 'inference', + 'model': model, + 'input': input, + 'options': { + ...opts.toJson(), + 'stream': true, + 'processor': (opts.processor ?? _config.defaultProcessor).value, + 'priority': (opts.priority ?? _config.defaultPriority).value, + }, + }; + + final request = http.Request('POST', Uri.parse('${_config.baseUrl}/stream')) + ..headers.addAll(_headers) + ..body = jsonEncode(body); + + final streamedResponse = await _httpClient.send(request); + + if (streamedResponse.statusCode != 200) { + final responseBody = await streamedResponse.stream.bytesToString(); + throw SynorException( + 'Streaming request failed', + statusCode: streamedResponse.statusCode, + details: {'response': responseBody}, + ); + } + + await for (final chunk in streamedResponse.stream.transform(utf8.decoder)) { + // Parse SSE format + for (final line in chunk.split('\n')) { + if (line.startsWith('data: ')) { + final data = line.substring(6); + if (data == '[DONE]') return; + try { + final json = jsonDecode(data) as Map; + if (json['token'] != null) { + yield json['token'] as String; + } + } catch (e) { + // Skip malformed JSON + } + } + } + } + } + + /// Apply element-wise operation + Future> elementwise( + String operation, + Tensor input, { + Tensor? other, + double? scalar, + Precision? precision, + ProcessorType? processor, + Priority? priority, + }) async { + _checkDisposed(); + + final body = { + 'operation': 'elementwise', + 'op': operation, + 'inputs': { + 'input': input.toJson(), + if (other != null) 'other': other.toJson(), + if (scalar != null) 'scalar': scalar, + }, + 'options': { + 'precision': (precision ?? _config.defaultPrecision).value, + 'processor': (processor ?? _config.defaultProcessor).value, + 'priority': (priority ?? _config.defaultPriority).value, + }, + }; + + return _submitAndWait( + body, + (result) => Tensor.fromJson(result as Map), + ); + } + + /// Reduce operation (sum, mean, max, min, etc.) + Future> reduce( + String operation, + Tensor input, { + List? axes, + bool keepDims = false, + Precision? precision, + ProcessorType? processor, + Priority? priority, + }) async { + _checkDisposed(); + + final body = { + 'operation': 'reduce', + 'op': operation, + 'inputs': { + 'input': input.toJson(), + }, + 'options': { + if (axes != null) 'axes': axes, + 'keep_dims': keepDims, + 'precision': (precision ?? _config.defaultPrecision).value, + 'processor': (processor ?? _config.defaultProcessor).value, + 'priority': (priority ?? _config.defaultPriority).value, + }, + }; + + return _submitAndWait( + body, + (result) => Tensor.fromJson(result as Map), + ); + } + + /// Submit a custom operation + Future> submit( + Map operation, + T Function(dynamic) resultParser, + ) async { + _checkDisposed(); + + final response = await _post('/jobs', operation); + final jobId = response['job_id'] as String; + + return Job( + jobId: jobId, + baseUrl: _config.baseUrl, + headers: _headers, + resultParser: resultParser, + ); + } + + /// Get job by ID + Future> getJob( + String jobId, { + T Function(dynamic)? resultParser, + }) async { + _checkDisposed(); + + final response = await _get('/jobs/$jobId'); + return JobResult.fromJson(response, resultParser); + } + + /// Cancel a job + Future cancelJob(String jobId) async { + _checkDisposed(); + + try { + await _post('/jobs/$jobId/cancel', {}); + return true; + } catch (e) { + return false; + } + } + + /// List active jobs + Future>> listJobs({ + JobStatus? status, + int limit = 20, + int offset = 0, + }) async { + _checkDisposed(); + + final params = { + 'limit': limit.toString(), + 'offset': offset.toString(), + if (status != null) 'status': status.value, + }; + + final response = await _get('/jobs', params); + final jobs = response['jobs'] as List; + return jobs + .map((j) => JobResult.fromJson(j as Map, null)) + .toList(); + } + + /// Get current pricing information + Future> getPricing() async { + _checkDisposed(); + + final response = await _get('/pricing'); + final pricing = response['pricing'] as List; + return pricing + .map((p) => PricingInfo.fromJson(p as Map)) + .toList(); + } + + /// Get pricing for specific processor + Future getPricingFor(ProcessorType processor) async { + final allPricing = await getPricing(); + return allPricing.firstWhere( + (p) => p.processor == processor, + orElse: () => throw SynorException( + 'No pricing available for processor ${processor.value}', + ), + ); + } + + /// Get account usage statistics + Future getUsage({DateTime? from, DateTime? to}) async { + _checkDisposed(); + + final params = { + if (from != null) 'from': from.toIso8601String(), + if (to != null) 'to': to.toIso8601String(), + }; + + final response = await _get('/usage', params); + return UsageStats.fromJson(response); + } + + /// Upload a tensor for reuse + Future uploadTensor(Tensor tensor, {String? name}) async { + _checkDisposed(); + + final body = { + 'tensor': tensor.toJson(), + if (name != null) 'name': name, + }; + + final response = await _post('/tensors', body); + return response['tensor_id'] as String; + } + + /// Download a tensor by ID + Future downloadTensor(String tensorId) async { + _checkDisposed(); + + final response = await _get('/tensors/$tensorId'); + return Tensor.fromJson(response); + } + + /// Delete a tensor by ID + Future deleteTensor(String tensorId) async { + _checkDisposed(); + + await _delete('/tensors/$tensorId'); + } + + /// Health check + Future healthCheck() async { + try { + final response = await _get('/health'); + return response['status'] == 'healthy'; + } catch (e) { + return false; + } + } + + // Internal HTTP methods + + Future> _submitAndWait( + Map body, + T Function(dynamic) resultParser, + ) async { + final response = await _post('/jobs', body); + final jobId = response['job_id'] as String; + + final job = Job( + jobId: jobId, + baseUrl: _config.baseUrl, + headers: _headers, + resultParser: resultParser, + ); + + try { + return await _pollJob(jobId, resultParser); + } finally { + job.dispose(); + } + } + + Future> _pollJob( + String jobId, + T Function(dynamic) resultParser, { + Duration interval = const Duration(milliseconds: 500), + Duration timeout = const Duration(minutes: 5), + }) async { + final endTime = DateTime.now().add(timeout); + + while (DateTime.now().isBefore(endTime)) { + final response = await _get('/jobs/$jobId'); + final result = JobResult.fromJson(response, resultParser); + + if (result.status.isTerminal) { + return result; + } + + await Future.delayed(interval); + } + + throw SynorException('Job polling timed out after $timeout'); + } + + Future> _get( + String path, [ + Map? queryParams, + ]) async { + var uri = Uri.parse('${_config.baseUrl}$path'); + if (queryParams != null && queryParams.isNotEmpty) { + uri = uri.replace(queryParameters: queryParams); + } + + final response = await _httpClient + .get(uri, headers: _headers) + .timeout(_config.timeout); + + return _handleResponse(response); + } + + Future> _post( + String path, + Map body, + ) async { + final uri = Uri.parse('${_config.baseUrl}$path'); + final response = await _httpClient + .post(uri, headers: _headers, body: jsonEncode(body)) + .timeout(_config.timeout); + + return _handleResponse(response); + } + + Future _delete(String path) async { + final uri = Uri.parse('${_config.baseUrl}$path'); + final response = await _httpClient + .delete(uri, headers: _headers) + .timeout(_config.timeout); + + if (response.statusCode != 200 && response.statusCode != 204) { + _handleResponse(response); + } + } + + Map _handleResponse(http.Response response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + if (response.body.isEmpty) { + return {}; + } + return jsonDecode(response.body) as Map; + } + + Map? errorBody; + try { + errorBody = jsonDecode(response.body) as Map; + } catch (e) { + // Body is not JSON + } + + throw SynorException( + errorBody?['message'] as String? ?? 'Request failed', + code: errorBody?['code'] as String?, + statusCode: response.statusCode, + details: errorBody, + ); + } + + /// Dispose the client and release resources + void dispose() { + _isDisposed = true; + _httpClient.close(); + } +} diff --git a/sdk/flutter/lib/src/job.dart b/sdk/flutter/lib/src/job.dart new file mode 100644 index 0000000..3ef0f76 --- /dev/null +++ b/sdk/flutter/lib/src/job.dart @@ -0,0 +1,394 @@ +/// Job tracking for Synor Compute SDK +library synor_compute.job; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:web_socket_channel/web_socket_channel.dart'; + +import 'tensor.dart'; +import 'types.dart'; + +/// Result of a compute job +class JobResult { + /// Unique job identifier + final String jobId; + + /// Current job status + final JobStatus status; + + /// Result data (if completed) + final T? result; + + /// Error message (if failed) + final String? error; + + /// Execution time in milliseconds + final int? executionTimeMs; + + /// Cost in credits + final double? cost; + + /// Processor that executed the job + final ProcessorType? processor; + + /// Metadata from execution + final Map? metadata; + + const JobResult({ + required this.jobId, + required this.status, + this.result, + this.error, + this.executionTimeMs, + this.cost, + this.processor, + this.metadata, + }); + + /// Whether the job completed successfully + bool get isSuccess => status == JobStatus.completed && result != null; + + /// Whether the job failed + bool get isFailed => status == JobStatus.failed; + + /// Whether the job is still running + bool get isRunning => !status.isTerminal; + + factory JobResult.fromJson( + Map json, + T Function(dynamic)? resultParser, + ) { + final status = JobStatus.fromString(json['status'] as String); + T? result; + + if (json['result'] != null && resultParser != null) { + result = resultParser(json['result']); + } + + return JobResult( + jobId: json['job_id'] as String, + status: status, + result: result, + error: json['error'] as String?, + executionTimeMs: json['execution_time_ms'] as int?, + cost: (json['cost'] as num?)?.toDouble(), + processor: json['processor'] != null + ? ProcessorType.fromString(json['processor'] as String) + : null, + metadata: json['metadata'] as Map?, + ); + } + + /// Transform the result to a different type + JobResult map(R Function(T) transform) { + return JobResult( + jobId: jobId, + status: status, + result: result != null ? transform(result as T) : null, + error: error, + executionTimeMs: executionTimeMs, + cost: cost, + processor: processor, + metadata: metadata, + ); + } + + @override + String toString() { + if (isSuccess) { + return 'JobResult(id: $jobId, status: ${status.value}, ' + 'time: ${executionTimeMs}ms, cost: \$${cost?.toStringAsFixed(6)})'; + } else if (isFailed) { + return 'JobResult(id: $jobId, status: ${status.value}, error: $error)'; + } + return 'JobResult(id: $jobId, status: ${status.value})'; + } +} + +/// Job status update event +class JobStatusUpdate { + final String jobId; + final JobStatus status; + final double? progress; + final String? message; + final DateTime timestamp; + + const JobStatusUpdate({ + required this.jobId, + required this.status, + this.progress, + this.message, + required this.timestamp, + }); + + factory JobStatusUpdate.fromJson(Map json) { + return JobStatusUpdate( + jobId: json['job_id'] as String, + status: JobStatus.fromString(json['status'] as String), + progress: (json['progress'] as num?)?.toDouble(), + message: json['message'] as String?, + timestamp: json['timestamp'] != null + ? DateTime.parse(json['timestamp'] as String) + : DateTime.now(), + ); + } +} + +/// Job handle for tracking and managing a submitted job +class Job { + final String jobId; + final String _baseUrl; + final Map _headers; + final T Function(dynamic)? _resultParser; + + WebSocketChannel? _wsChannel; + StreamController? _statusController; + JobResult? _cachedResult; + bool _isDisposed = false; + + Job({ + required this.jobId, + required String baseUrl, + required Map headers, + T Function(dynamic)? resultParser, + }) : _baseUrl = baseUrl, + _headers = headers, + _resultParser = resultParser; + + /// Stream of status updates for this job + Stream get statusUpdates { + _statusController ??= StreamController.broadcast( + onListen: _startWebSocket, + onCancel: _stopWebSocket, + ); + return _statusController!.stream; + } + + void _startWebSocket() { + if (_isDisposed) return; + + final wsUrl = _baseUrl + .replaceFirst('http://', 'ws://') + .replaceFirst('https://', 'wss://'); + + _wsChannel = WebSocketChannel.connect( + Uri.parse('$wsUrl/jobs/$jobId/stream'), + ); + + _wsChannel!.stream.listen( + (data) { + if (_isDisposed) return; + try { + final json = jsonDecode(data as String) as Map; + final update = JobStatusUpdate.fromJson(json); + _statusController?.add(update); + + if (update.status.isTerminal) { + _stopWebSocket(); + } + } catch (e) { + // Ignore parse errors + } + }, + onError: (error) { + if (!_isDisposed) { + _statusController?.addError(error); + } + }, + onDone: _stopWebSocket, + ); + } + + void _stopWebSocket() { + _wsChannel?.sink.close(); + _wsChannel = null; + } + + /// Poll for job result (for environments without WebSocket support) + Future> poll({ + Duration interval = const Duration(milliseconds: 500), + Duration timeout = const Duration(minutes: 5), + }) async { + if (_cachedResult?.status.isTerminal == true) { + return _cachedResult!; + } + + final endTime = DateTime.now().add(timeout); + final client = _createHttpClient(); + + try { + while (DateTime.now().isBefore(endTime)) { + final response = await client.get( + Uri.parse('$_baseUrl/jobs/$jobId'), + headers: _headers, + ); + + if (response.statusCode != 200) { + throw SynorException( + 'Failed to poll job status', + statusCode: response.statusCode, + ); + } + + final json = jsonDecode(response.body) as Map; + final result = JobResult.fromJson(json, _resultParser); + + if (result.status.isTerminal) { + _cachedResult = result; + return result; + } + + await Future.delayed(interval); + } + + throw SynorException('Job polling timed out after $timeout'); + } finally { + client.close(); + } + } + + /// Wait for job completion with automatic strategy selection + Future> wait({ + Duration timeout = const Duration(minutes: 5), + bool useWebSocket = true, + }) async { + if (_cachedResult?.status.isTerminal == true) { + return _cachedResult!; + } + + if (useWebSocket) { + try { + final completer = Completer>(); + late StreamSubscription subscription; + + subscription = statusUpdates.listen( + (update) async { + if (update.status.isTerminal && !completer.isCompleted) { + final result = await poll( + interval: Duration.zero, + timeout: const Duration(seconds: 10), + ); + completer.complete(result); + await subscription.cancel(); + } + }, + onError: (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + ); + + return await completer.future.timeout( + timeout, + onTimeout: () { + subscription.cancel(); + throw SynorException('Job wait timed out after $timeout'); + }, + ); + } catch (e) { + // Fall back to polling if WebSocket fails + return poll(timeout: timeout); + } + } + + return poll(timeout: timeout); + } + + /// Cancel the job + Future cancel() async { + final client = _createHttpClient(); + try { + final response = await client.post( + Uri.parse('$_baseUrl/jobs/$jobId/cancel'), + headers: _headers, + ); + + if (response.statusCode == 200) { + _cachedResult = JobResult( + jobId: jobId, + status: JobStatus.cancelled, + ); + return true; + } + return false; + } finally { + client.close(); + } + } + + /// Get current job status + Future getStatus() async { + final client = _createHttpClient(); + try { + final response = await client.get( + Uri.parse('$_baseUrl/jobs/$jobId/status'), + headers: _headers, + ); + + if (response.statusCode != 200) { + throw SynorException( + 'Failed to get job status', + statusCode: response.statusCode, + ); + } + + final json = jsonDecode(response.body) as Map; + return JobStatus.fromString(json['status'] as String); + } finally { + client.close(); + } + } + + /// Dispose resources + void dispose() { + _isDisposed = true; + _stopWebSocket(); + _statusController?.close(); + _statusController = null; + } + + // Creates an HTTP client - in a real app, use http package + dynamic _createHttpClient() { + // This is a placeholder - actual implementation uses http package + throw UnimplementedError('HTTP client should be injected'); + } +} + +/// Batch job operations +class JobBatch { + final List> jobs; + + JobBatch(this.jobs); + + /// Wait for all jobs to complete + Future>> waitAll({ + Duration timeout = const Duration(minutes: 10), + }) async { + return Future.wait( + jobs.map((job) => job.wait(timeout: timeout)), + ); + } + + /// Wait for first job to complete + Future> waitAny({ + Duration timeout = const Duration(minutes: 5), + }) async { + return Future.any( + jobs.map((job) => job.wait(timeout: timeout)), + ); + } + + /// Cancel all jobs + Future cancelAll() async { + await Future.wait(jobs.map((job) => job.cancel())); + } + + /// Dispose all job resources + void dispose() { + for (final job in jobs) { + job.dispose(); + } + } +} diff --git a/sdk/flutter/lib/src/tensor.dart b/sdk/flutter/lib/src/tensor.dart new file mode 100644 index 0000000..82b8b31 --- /dev/null +++ b/sdk/flutter/lib/src/tensor.dart @@ -0,0 +1,520 @@ +/// Tensor implementation for Synor Compute SDK +library synor_compute.tensor; + +import 'dart:convert'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'types.dart'; + +/// Multi-dimensional tensor for compute operations +class Tensor { + /// Tensor shape (dimensions) + final List shape; + + /// Underlying data as Float64List + final Float64List data; + + /// Data type + final DType dtype; + + /// Unique identifier (assigned by server) + final String? id; + + /// Creates a tensor with given shape and data + Tensor({ + required this.shape, + required List data, + this.dtype = DType.float64, + this.id, + }) : data = Float64List.fromList(data) { + final expectedSize = shape.fold(1, (a, b) => a * b); + if (this.data.length != expectedSize) { + throw ArgumentError( + 'Data length ${this.data.length} does not match shape $shape ' + '(expected $expectedSize elements)', + ); + } + } + + /// Creates a tensor from Float64List + Tensor.fromTypedData({ + required this.shape, + required this.data, + this.dtype = DType.float64, + this.id, + }) { + final expectedSize = shape.fold(1, (a, b) => a * b); + if (data.length != expectedSize) { + throw ArgumentError( + 'Data length ${data.length} does not match shape $shape ' + '(expected $expectedSize elements)', + ); + } + } + + /// Creates a tensor filled with zeros + factory Tensor.zeros(List shape, {DType dtype = DType.float64}) { + final size = shape.fold(1, (a, b) => a * b); + return Tensor( + shape: shape, + data: List.filled(size, 0.0), + dtype: dtype, + ); + } + + /// Creates a tensor filled with ones + factory Tensor.ones(List shape, {DType dtype = DType.float64}) { + final size = shape.fold(1, (a, b) => a * b); + return Tensor( + shape: shape, + data: List.filled(size, 1.0), + dtype: dtype, + ); + } + + /// Creates a tensor filled with a specific value + factory Tensor.full( + List shape, + double value, { + DType dtype = DType.float64, + }) { + final size = shape.fold(1, (a, b) => a * b); + return Tensor( + shape: shape, + data: List.filled(size, value), + dtype: dtype, + ); + } + + /// Creates a tensor with random values from uniform distribution [0, 1) + factory Tensor.rand(List shape, {DType dtype = DType.float64}) { + final size = shape.fold(1, (a, b) => a * b); + final random = math.Random(); + return Tensor( + shape: shape, + data: List.generate(size, (_) => random.nextDouble()), + dtype: dtype, + ); + } + + /// Creates a tensor with random values from normal distribution + factory Tensor.randn( + List shape, { + double mean = 0.0, + double std = 1.0, + DType dtype = DType.float64, + }) { + final size = shape.fold(1, (a, b) => a * b); + final random = math.Random(); + + // Box-Muller transform for normal distribution + double nextGaussian() { + final u1 = random.nextDouble(); + final u2 = random.nextDouble(); + return math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2); + } + + return Tensor( + shape: shape, + data: List.generate(size, (_) => mean + std * nextGaussian()), + dtype: dtype, + ); + } + + /// Creates an identity matrix + factory Tensor.eye(int n, {DType dtype = DType.float64}) { + final data = List.filled(n * n, 0.0); + for (var i = 0; i < n; i++) { + data[i * n + i] = 1.0; + } + return Tensor(shape: [n, n], data: data, dtype: dtype); + } + + /// Creates a tensor with evenly spaced values + factory Tensor.linspace( + double start, + double end, + int steps, { + DType dtype = DType.float64, + }) { + if (steps < 2) { + throw ArgumentError('Steps must be at least 2'); + } + final step = (end - start) / (steps - 1); + return Tensor( + shape: [steps], + data: List.generate(steps, (i) => start + i * step), + dtype: dtype, + ); + } + + /// Creates a tensor with values in a range + factory Tensor.arange( + double start, + double end, { + double step = 1.0, + DType dtype = DType.float64, + }) { + final data = []; + for (var v = start; v < end; v += step) { + data.add(v); + } + return Tensor(shape: [data.length], data: data, dtype: dtype); + } + + /// Creates a tensor from JSON + factory Tensor.fromJson(Map json) { + final shape = (json['shape'] as List).cast(); + final rawData = json['data']; + + List data; + if (rawData is String) { + // Base64-encoded binary data + final bytes = base64Decode(rawData); + data = Float64List.view(bytes.buffer).toList(); + } else if (rawData is List) { + data = _flattenList(rawData); + } else { + throw ArgumentError('Invalid tensor data format'); + } + + return Tensor( + shape: shape, + data: data, + dtype: DType.fromString(json['dtype'] as String? ?? 'float64'), + id: json['id'] as String?, + ); + } + + /// Flattens a nested list to 1D + static List _flattenList(List nested) { + final result = []; + void flatten(dynamic item) { + if (item is List) { + for (final e in item) { + flatten(e); + } + } else if (item is num) { + result.add(item.toDouble()); + } + } + flatten(nested); + return result; + } + + /// Number of dimensions + int get ndim => shape.length; + + /// Total number of elements + int get size => data.length; + + /// Number of bytes + int get nbytes => data.lengthInBytes; + + /// Get element at index (for 1D tensors) + double operator [](int index) { + if (ndim != 1) { + throw StateError('Use at() for multi-dimensional indexing'); + } + return data[index]; + } + + /// Get element at multi-dimensional index + double at(List indices) { + if (indices.length != ndim) { + throw ArgumentError( + 'Expected $ndim indices, got ${indices.length}', + ); + } + var flatIndex = 0; + var stride = 1; + for (var i = ndim - 1; i >= 0; i--) { + if (indices[i] < 0 || indices[i] >= shape[i]) { + throw RangeError('Index ${indices[i]} out of bounds for axis $i ' + 'with size ${shape[i]}'); + } + flatIndex += indices[i] * stride; + stride *= shape[i]; + } + return data[flatIndex]; + } + + /// Reshape tensor to new shape + Tensor reshape(List newShape) { + final newSize = newShape.fold(1, (a, b) => a * b); + if (newSize != size) { + throw ArgumentError( + 'Cannot reshape tensor of size $size to shape $newShape ' + '(size $newSize)', + ); + } + return Tensor.fromTypedData( + shape: newShape, + data: data, + dtype: dtype, + id: id, + ); + } + + /// Flatten tensor to 1D + Tensor flatten() => reshape([size]); + + /// Transpose tensor (swap last two dimensions) + Tensor transpose() { + if (ndim < 2) { + return this; + } + + final newShape = List.from(shape); + final tmp = newShape[ndim - 1]; + newShape[ndim - 1] = newShape[ndim - 2]; + newShape[ndim - 2] = tmp; + + final newData = Float64List(size); + final rows = shape[ndim - 2]; + final cols = shape[ndim - 1]; + final batchSize = size ~/ (rows * cols); + + for (var b = 0; b < batchSize; b++) { + final offset = b * rows * cols; + for (var i = 0; i < rows; i++) { + for (var j = 0; j < cols; j++) { + newData[offset + j * rows + i] = data[offset + i * cols + j]; + } + } + } + + return Tensor.fromTypedData( + shape: newShape, + data: newData, + dtype: dtype, + ); + } + + /// Sum of all elements + double sum() => data.fold(0.0, (a, b) => a + b); + + /// Mean of all elements + double mean() => sum() / size; + + /// Standard deviation of all elements + double std() { + final m = mean(); + final variance = data.fold(0.0, (sum, x) => sum + (x - m) * (x - m)) / size; + return math.sqrt(variance); + } + + /// Minimum value + double min() => data.reduce(math.min); + + /// Maximum value + double max() => data.reduce(math.max); + + /// Index of minimum value + int argmin() { + var minIdx = 0; + var minVal = data[0]; + for (var i = 1; i < size; i++) { + if (data[i] < minVal) { + minVal = data[i]; + minIdx = i; + } + } + return minIdx; + } + + /// Index of maximum value + int argmax() { + var maxIdx = 0; + var maxVal = data[0]; + for (var i = 1; i < size; i++) { + if (data[i] > maxVal) { + maxVal = data[i]; + maxIdx = i; + } + } + return maxIdx; + } + + /// Element-wise addition + Tensor add(Tensor other) { + _checkShapesMatch(other); + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] + other.data[i]; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// Element-wise subtraction + Tensor sub(Tensor other) { + _checkShapesMatch(other); + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] - other.data[i]; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// Element-wise multiplication + Tensor mul(Tensor other) { + _checkShapesMatch(other); + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] * other.data[i]; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// Element-wise division + Tensor div(Tensor other) { + _checkShapesMatch(other); + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] / other.data[i]; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// Scalar operations + Tensor addScalar(double scalar) { + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] + scalar; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + Tensor mulScalar(double scalar) { + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = data[i] * scalar; + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// Apply function element-wise + Tensor map(double Function(double) fn) { + final result = Float64List(size); + for (var i = 0; i < size; i++) { + result[i] = fn(data[i]); + } + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + + /// ReLU activation + Tensor relu() => map((x) => x > 0 ? x : 0); + + /// Sigmoid activation + Tensor sigmoid() => map((x) => 1.0 / (1.0 + math.exp(-x))); + + /// Tanh activation + Tensor tanh() => map(math.tanh); + + /// Softmax (for 1D or last axis of 2D) + Tensor softmax() { + if (ndim == 1) { + final maxVal = max(); + final expData = data.map((x) => math.exp(x - maxVal)).toList(); + final sumExp = expData.fold(0.0, (a, b) => a + b); + return Tensor( + shape: shape, + data: expData.map((x) => x / sumExp).toList(), + dtype: dtype, + ); + } else if (ndim == 2) { + final rows = shape[0]; + final cols = shape[1]; + final result = Float64List(size); + + for (var i = 0; i < rows; i++) { + var maxVal = double.negativeInfinity; + for (var j = 0; j < cols; j++) { + final v = data[i * cols + j]; + if (v > maxVal) maxVal = v; + } + + var sumExp = 0.0; + for (var j = 0; j < cols; j++) { + final exp = math.exp(data[i * cols + j] - maxVal); + result[i * cols + j] = exp; + sumExp += exp; + } + + for (var j = 0; j < cols; j++) { + result[i * cols + j] /= sumExp; + } + } + + return Tensor.fromTypedData(shape: shape, data: result, dtype: dtype); + } + throw UnsupportedError('Softmax only supported for 1D and 2D tensors'); + } + + void _checkShapesMatch(Tensor other) { + if (shape.length != other.shape.length) { + throw ArgumentError('Shape mismatch: $shape vs ${other.shape}'); + } + for (var i = 0; i < shape.length; i++) { + if (shape[i] != other.shape[i]) { + throw ArgumentError('Shape mismatch: $shape vs ${other.shape}'); + } + } + } + + /// Convert to JSON for API serialization + Map toJson() => { + 'shape': shape, + 'data': base64Encode(data.buffer.asUint8List()), + 'dtype': dtype.value, + if (id != null) 'id': id, + }; + + /// Convert to nested list representation + List toNestedList() { + if (ndim == 1) { + return data.toList(); + } + + List buildNested(int dim, int offset) { + if (dim == ndim - 1) { + return data.sublist(offset, offset + shape[dim]).toList(); + } + + final stride = + shape.sublist(dim + 1).fold(1, (a, b) => a * b); + return List.generate( + shape[dim], + (i) => buildNested(dim + 1, offset + i * stride), + ); + } + + return buildNested(0, 0); + } + + @override + String toString() { + if (size <= 20) { + return 'Tensor(shape: $shape, data: ${toNestedList()})'; + } + return 'Tensor(shape: $shape, dtype: ${dtype.value})'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Tensor) return false; + if (shape.length != other.shape.length) return false; + for (var i = 0; i < shape.length; i++) { + if (shape[i] != other.shape[i]) return false; + } + for (var i = 0; i < size; i++) { + if (data[i] != other.data[i]) return false; + } + return true; + } + + @override + int get hashCode => Object.hash(shape, data); +} diff --git a/sdk/flutter/lib/src/types.dart b/sdk/flutter/lib/src/types.dart new file mode 100644 index 0000000..f4c934d --- /dev/null +++ b/sdk/flutter/lib/src/types.dart @@ -0,0 +1,371 @@ +/// Type definitions for Synor Compute SDK +library synor_compute.types; + +import 'package:collection/collection.dart'; + +/// Numeric precision for compute operations +enum Precision { + fp64('fp64'), + fp32('fp32'), + fp16('fp16'), + bf16('bf16'), + int8('int8'), + int4('int4'); + + const Precision(this.value); + final String value; + + static Precision fromString(String s) => + Precision.values.firstWhere((p) => p.value == s, orElse: () => fp32); +} + +/// Target processor type for compute operations +enum ProcessorType { + cpu('cpu'), + gpu('gpu'), + tpu('tpu'), + npu('npu'), + lpu('lpu'), + fpga('fpga'), + dsp('dsp'), + webgpu('webgpu'), + wasm('wasm'), + auto('auto'); + + const ProcessorType(this.value); + final String value; + + static ProcessorType fromString(String s) => + ProcessorType.values.firstWhere((p) => p.value == s, orElse: () => auto); +} + +/// Job priority levels +enum Priority { + low('low'), + normal('normal'), + high('high'), + critical('critical'); + + const Priority(this.value); + final String value; + + static Priority fromString(String s) => + Priority.values.firstWhere((p) => p.value == s, orElse: () => normal); +} + +/// Job execution status +enum JobStatus { + pending('pending'), + queued('queued'), + running('running'), + completed('completed'), + failed('failed'), + cancelled('cancelled'); + + const JobStatus(this.value); + final String value; + + bool get isTerminal => + this == completed || this == failed || this == cancelled; + + static JobStatus fromString(String s) => + JobStatus.values.firstWhere((p) => p.value == s, orElse: () => pending); +} + +/// Balancing strategy for load distribution +enum BalancingStrategy { + speed('speed'), + energy('energy'), + balanced('balanced'), + cost('cost'), + latency('latency'); + + const BalancingStrategy(this.value); + final String value; + + static BalancingStrategy fromString(String s) => BalancingStrategy.values + .firstWhere((p) => p.value == s, orElse: () => balanced); +} + +/// Tensor data type +enum DType { + float64('float64'), + float32('float32'), + float16('float16'), + bfloat16('bfloat16'), + int64('int64'), + int32('int32'), + int16('int16'), + int8('int8'), + uint8('uint8'), + bool_('bool'); + + const DType(this.value); + final String value; + + static DType fromString(String s) => + DType.values.firstWhere((p) => p.value == s, orElse: () => float32); +} + +/// Configuration for SDK client +class SynorConfig { + final String apiKey; + final String baseUrl; + final Duration timeout; + final int maxRetries; + final ProcessorType defaultProcessor; + final Precision defaultPrecision; + final Priority defaultPriority; + + const SynorConfig({ + required this.apiKey, + this.baseUrl = 'https://compute.synor.io', + this.timeout = const Duration(seconds: 30), + this.maxRetries = 3, + this.defaultProcessor = ProcessorType.auto, + this.defaultPrecision = Precision.fp32, + this.defaultPriority = Priority.normal, + }); + + SynorConfig copyWith({ + String? apiKey, + String? baseUrl, + Duration? timeout, + int? maxRetries, + ProcessorType? defaultProcessor, + Precision? defaultPrecision, + Priority? defaultPriority, + }) { + return SynorConfig( + apiKey: apiKey ?? this.apiKey, + baseUrl: baseUrl ?? this.baseUrl, + timeout: timeout ?? this.timeout, + maxRetries: maxRetries ?? this.maxRetries, + defaultProcessor: defaultProcessor ?? this.defaultProcessor, + defaultPrecision: defaultPrecision ?? this.defaultPrecision, + defaultPriority: defaultPriority ?? this.defaultPriority, + ); + } +} + +/// Matrix multiplication options +class MatMulOptions { + final Precision? precision; + final ProcessorType? processor; + final Priority? priority; + final bool transposeA; + final bool transposeB; + + const MatMulOptions({ + this.precision, + this.processor, + this.priority, + this.transposeA = false, + this.transposeB = false, + }); + + Map toJson() => { + if (precision != null) 'precision': precision!.value, + if (processor != null) 'processor': processor!.value, + if (priority != null) 'priority': priority!.value, + 'transpose_a': transposeA, + 'transpose_b': transposeB, + }; +} + +/// Convolution options +class Conv2dOptions { + final List kernel; + final List stride; + final List padding; + final List dilation; + final int groups; + final Precision? precision; + final ProcessorType? processor; + final Priority? priority; + + const Conv2dOptions({ + this.kernel = const [3, 3], + this.stride = const [1, 1], + this.padding = const [0, 0], + this.dilation = const [1, 1], + this.groups = 1, + this.precision, + this.processor, + this.priority, + }); + + Map toJson() => { + 'kernel': kernel, + 'stride': stride, + 'padding': padding, + 'dilation': dilation, + 'groups': groups, + if (precision != null) 'precision': precision!.value, + if (processor != null) 'processor': processor!.value, + if (priority != null) 'priority': priority!.value, + }; +} + +/// Flash attention options +class AttentionOptions { + final int numHeads; + final double? scale; + final bool causal; + final double? dropoutP; + final Precision? precision; + final ProcessorType? processor; + final Priority? priority; + + const AttentionOptions({ + required this.numHeads, + this.scale, + this.causal = false, + this.dropoutP, + this.precision, + this.processor, + this.priority, + }); + + Map toJson() => { + 'num_heads': numHeads, + if (scale != null) 'scale': scale, + 'causal': causal, + if (dropoutP != null) 'dropout_p': dropoutP, + if (precision != null) 'precision': precision!.value, + if (processor != null) 'processor': processor!.value, + if (priority != null) 'priority': priority!.value, + }; +} + +/// LLM inference options +class InferenceOptions { + final int maxTokens; + final double temperature; + final double topP; + final int? topK; + final double? frequencyPenalty; + final double? presencePenalty; + final List? stopSequences; + final bool stream; + final ProcessorType? processor; + final Priority? priority; + + const InferenceOptions({ + this.maxTokens = 256, + this.temperature = 0.7, + this.topP = 1.0, + this.topK, + this.frequencyPenalty, + this.presencePenalty, + this.stopSequences, + this.stream = false, + this.processor, + this.priority, + }); + + Map toJson() => { + 'max_tokens': maxTokens, + 'temperature': temperature, + 'top_p': topP, + if (topK != null) 'top_k': topK, + if (frequencyPenalty != null) 'frequency_penalty': frequencyPenalty, + if (presencePenalty != null) 'presence_penalty': presencePenalty, + if (stopSequences != null) 'stop_sequences': stopSequences, + 'stream': stream, + if (processor != null) 'processor': processor!.value, + if (priority != null) 'priority': priority!.value, + }; +} + +/// Pricing information for compute resources +class PricingInfo { + final ProcessorType processor; + final double pricePerSecond; + final double pricePerGflop; + final int availableUnits; + final double utilizationPercent; + final String region; + + const PricingInfo({ + required this.processor, + required this.pricePerSecond, + required this.pricePerGflop, + required this.availableUnits, + required this.utilizationPercent, + required this.region, + }); + + factory PricingInfo.fromJson(Map json) => PricingInfo( + processor: ProcessorType.fromString(json['processor'] as String), + pricePerSecond: (json['price_per_second'] as num).toDouble(), + pricePerGflop: (json['price_per_gflop'] as num).toDouble(), + availableUnits: json['available_units'] as int, + utilizationPercent: (json['utilization_percent'] as num).toDouble(), + region: json['region'] as String, + ); +} + +/// Compute usage statistics +class UsageStats { + final int totalJobs; + final int completedJobs; + final int failedJobs; + final double totalComputeSeconds; + final double totalCost; + final Map costByProcessor; + + const UsageStats({ + required this.totalJobs, + required this.completedJobs, + required this.failedJobs, + required this.totalComputeSeconds, + required this.totalCost, + required this.costByProcessor, + }); + + factory UsageStats.fromJson(Map json) { + final costMap = {}; + final rawCostMap = json['cost_by_processor'] as Map?; + if (rawCostMap != null) { + for (final entry in rawCostMap.entries) { + costMap[ProcessorType.fromString(entry.key)] = + (entry.value as num).toDouble(); + } + } + + return UsageStats( + totalJobs: json['total_jobs'] as int, + completedJobs: json['completed_jobs'] as int, + failedJobs: json['failed_jobs'] as int, + totalComputeSeconds: (json['total_compute_seconds'] as num).toDouble(), + totalCost: (json['total_cost'] as num).toDouble(), + costByProcessor: costMap, + ); + } +} + +/// Exception thrown by Synor Compute operations +class SynorException implements Exception { + final String message; + final String? code; + final int? statusCode; + final Map? details; + + const SynorException( + this.message, { + this.code, + this.statusCode, + this.details, + }); + + @override + String toString() => 'SynorException: $message (code: $code)'; + + factory SynorException.fromJson(Map json) => SynorException( + json['message'] as String? ?? 'Unknown error', + code: json['code'] as String?, + statusCode: json['status_code'] as int?, + details: json['details'] as Map?, + ); +} diff --git a/sdk/flutter/lib/synor_compute.dart b/sdk/flutter/lib/synor_compute.dart new file mode 100644 index 0000000..81f5228 --- /dev/null +++ b/sdk/flutter/lib/synor_compute.dart @@ -0,0 +1,92 @@ +/// Synor Compute SDK for Flutter/Dart +/// +/// A high-performance SDK for distributed heterogeneous computing. +/// Supports CPU, GPU, TPU, NPU, LPU, FPGA, DSP, WebGPU, and WASM processors. +/// +/// ## Quick Start +/// +/// ```dart +/// import 'package:synor_compute/synor_compute.dart'; +/// +/// void main() async { +/// // Create client +/// final client = SynorCompute(apiKey: 'your-api-key'); +/// +/// // Matrix multiplication +/// final a = Tensor.rand([512, 512]); +/// final b = Tensor.rand([512, 512]); +/// final result = await client.matmul(a, b, options: MatMulOptions( +/// precision: Precision.fp16, +/// processor: ProcessorType.gpu, +/// )); +/// +/// print('Result shape: ${result.result!.shape}'); +/// print('Execution time: ${result.executionTimeMs}ms'); +/// +/// // LLM Inference +/// final response = await client.inference( +/// 'llama-3-70b', +/// 'Explain quantum computing', +/// options: InferenceOptions(maxTokens: 256), +/// ); +/// print(response.result); +/// +/// // Streaming inference +/// await for (final token in client.inferenceStream( +/// 'llama-3-70b', +/// 'Write a haiku about computing', +/// )) { +/// stdout.write(token); +/// } +/// +/// // Clean up +/// client.dispose(); +/// } +/// ``` +/// +/// ## Features +/// +/// - **Matrix Operations**: matmul, conv2d, attention, elementwise, reduce +/// - **LLM Inference**: Standard and streaming inference +/// - **Tensor Management**: Upload, download, and delete tensors +/// - **Job Management**: Submit, poll, cancel, and list jobs +/// - **Pricing**: Get real-time pricing for all processor types +/// - **Usage Statistics**: Track compute usage and costs +/// +/// ## Supported Processors +/// +/// | Processor | Best For | +/// |-----------|----------| +/// | CPU | General compute, small batches | +/// | GPU | Large matrix operations, training | +/// | TPU | Tensor operations, inference | +/// | NPU | Neural network inference | +/// | LPU | Large language model inference | +/// | FPGA | Custom operations, low latency | +/// | DSP | Signal processing | +/// | WebGPU | Browser-based compute | +/// | WASM | Portable compute | +library synor_compute; + +export 'src/types.dart' + show + Precision, + ProcessorType, + Priority, + JobStatus, + BalancingStrategy, + DType, + SynorConfig, + MatMulOptions, + Conv2dOptions, + AttentionOptions, + InferenceOptions, + PricingInfo, + UsageStats, + SynorException; + +export 'src/tensor.dart' show Tensor; + +export 'src/job.dart' show JobResult, JobStatusUpdate, Job, JobBatch; + +export 'src/client.dart' show SynorCompute; diff --git a/sdk/flutter/pubspec.yaml b/sdk/flutter/pubspec.yaml new file mode 100644 index 0000000..eaeea06 --- /dev/null +++ b/sdk/flutter/pubspec.yaml @@ -0,0 +1,37 @@ +name: synor_compute +description: Flutter/Dart SDK for Synor Compute - distributed heterogeneous computing platform +version: 0.1.0 +homepage: https://github.com/mrgulshanyadav/Blockchain.cc +repository: https://github.com/mrgulshanyadav/Blockchain.cc/tree/main/sdk/flutter +issue_tracker: https://github.com/mrgulshanyadav/Blockchain.cc/issues + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.10.0' + +dependencies: + flutter: + sdk: flutter + http: ^1.1.0 + web_socket_channel: ^2.4.0 + json_annotation: ^4.8.1 + crypto: ^3.0.3 + collection: ^1.18.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + build_runner: ^2.4.0 + json_serializable: ^6.7.0 + mockito: ^5.4.0 + +flutter: + +platforms: + android: + ios: + linux: + macos: + web: + windows: