@qvac/ggml-rpc-server 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0] - 2026-10-15
4
+
5
+ ### Changed
6
+
7
+ - Without a `port`, the native server binds a free port itself and reports it,
8
+ instead of this package picking a free port and releasing it before the
9
+ server bound it. Another process could take the port in between, which showed
10
+ up as a rare `RpcServerStartError`. `allocateFreePort()` is still exported.
11
+ - `rdmaCapable` and `expectRdma` now use the capability that `@qvac/fabric`
12
+ reports, replacing a search of the RPC module for a log line. They mean the
13
+ server will try RDMA, which on Linux also needs `libibverbs.so.1` installed,
14
+ an RDMA device, and `GGML_RPC_NO_RDMA` unset. A host with the library but no
15
+ RDMA device now reports `rdmaCapable: false`, so `expectRdma: true` fails
16
+ closed there.
17
+ - Requires `@qvac/fabric` 0.22.0, whose RPC backend exports
18
+ `ggml_backend_rpc_server_get_port` and `ggml_backend_rpc_rdma_supported`.
19
+ A mobile app that still installs `@qvac/fabric-android-arm64` or
20
+ `@qvac/fabric-ios` 0.21 fails to start the server with
21
+ `RpcServerStartError`.
22
+
3
23
  ## [0.4.0] - 2026-10-14
4
24
 
5
25
  ### Changed
package/README.md CHANGED
@@ -24,6 +24,11 @@ try {
24
24
  }
25
25
  ```
26
26
 
27
+ Without a `port`, the server binds a free port itself and reports it in
28
+ `server.port`, so no other process can take the port before the server binds
29
+ it. `allocateFreePort()` only finds a port that is free now; prefer leaving
30
+ `port` unset.
31
+
27
32
  The listener accepts IPv4 addresses; `localhost` is normalized to `127.0.0.1`.
28
33
  The default host is `127.0.0.1`, but loopback is not private to the host
29
34
  application: other local processes (including apps with local TCP access on
@@ -48,9 +53,10 @@ Native server output is written to the host application's platform log.
48
53
 
49
54
  Errors are classes that can be matched by `name` or `instanceof`. Invalid
50
55
  options throw `RpcServerInvalidHostError`, `RpcServerNonLoopbackHostError`,
51
- `RangeError` (port) or `TypeError` (threads); a failed port lookup throws
52
- `RpcServerPortAllocationError`; and `expectRdma: true` on a backend without RDMA
53
- throws `RpcServerRdmaUnavailableError`. Failures reported by the native server
56
+ `RangeError` (port) or `TypeError` (threads); `allocateFreePort()` throws
57
+ `RpcServerPortAllocationError` when it cannot find a port; and
58
+ `expectRdma: true` when the server would not try RDMA throws
59
+ `RpcServerRdmaUnavailableError`. Failures reported by the native server
54
60
  extend `RpcServerNativeError`, which carries the native error as `cause` and a
55
61
  `code` equal to its `name`:
56
62
 
@@ -58,31 +64,31 @@ extend `RpcServerNativeError`, which carries the native error as `cause` and a
58
64
  |---|---|
59
65
  | `RpcServerDeviceError` | No requested device exists, or no device is available |
60
66
  | `RpcServerCacheError` | The cache directory cannot be resolved or created |
61
- | `RpcServerStartError` | The server cannot be created or bound, or the RPC backend is missing |
67
+ | `RpcServerStartError` | The server cannot be created or bound, the RPC backend is missing, or the installed `@qvac/fabric` is too old |
62
68
  | `RpcServerBackendError` | The Fabric backends directory is invalid or cannot be inspected |
63
69
  | `RpcServerStopError` | The server does not stop cleanly |
64
70
 
65
71
  Anything else from the native addon, such as an out-of-memory failure, is
66
72
  rethrown unchanged.
67
73
 
68
- ## Linux requirements
69
-
70
- The Linux RPC backend in `@qvac/fabric` links `libibverbs.so.1`, so Linux
71
- hosts must provide it (`libibverbs1` on Debian/Ubuntu) even when connections
72
- use TCP. Without it, `startRpcServer()` rejects with an error whose `code` is
73
- `RpcServerStartError`.
74
- RDMA also requires the provider package for the host's hardware.
74
+ ## RDMA on Linux
75
75
 
76
- ## RDMA-capable builds
76
+ The Linux RPC backend in `@qvac/fabric` is built with RDMA and loads
77
+ `libibverbs.so.1` (`libibverbs1` on Debian/Ubuntu) at runtime. When the library
78
+ is installed and the host has an RDMA device, the server tries RDMA with each
79
+ client that also supports it and falls back to TCP otherwise. Without the
80
+ library or a device, the server runs over TCP only. RDMA also requires the
81
+ provider package for the host's hardware.
77
82
 
78
- On Linux (not Android), `rdmaCapable` reports whether the installed
79
- `@qvac/fabric` RPC backend was built with RDMA. Such a backend negotiates RDMA
80
- with each client that also supports it and falls back to TCP otherwise, so
81
- `rdmaCapable: true` does not guarantee that a given connection uses RDMA. Other
82
- platforms always report `false`.
83
+ `rdmaCapable` reports whether the server will try RDMA: the library loaded, it
84
+ lists an RDMA device, and `GGML_RPC_NO_RDMA` is not set. A host with libibverbs
85
+ but no RDMA device, common where MPI or UCX installs the library, reports
86
+ `false`. `rdmaCapable: true` does not guarantee that a given connection uses
87
+ RDMA, since the client and the link must support it too. Android, iOS, macOS
88
+ and Windows always report `false`.
83
89
 
84
90
  To fail closed when RDMA is required, pass `expectRdma: true`. Startup rejects
85
- with `RpcServerRdmaUnavailableError` when the installed backend lacks RDMA:
91
+ with `RpcServerRdmaUnavailableError` when the server would not try RDMA:
86
92
 
87
93
  ```js
88
94
  const server = await startRpcServer({
@@ -95,8 +101,8 @@ const server = await startRpcServer({
95
101
  console.log(server.rdmaCapable)
96
102
  ```
97
103
 
98
- Without `expectRdma`, the server starts either way and reports the backend's
99
- capability in `rdmaCapable`.
104
+ Without `expectRdma`, the server starts either way and reports in
105
+ `rdmaCapable` whether it will try RDMA.
100
106
 
101
107
  ## Testing
102
108
 
package/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare class RpcServerInvalidHostError extends Error {
9
9
  constructor(host: string);
10
10
  }
11
11
  export declare class RpcServerRdmaUnavailableError extends Error {
12
- constructor();
12
+ constructor(cause?: unknown);
13
13
  }
14
14
  /**
15
15
  * A failure reported by the native server. The addon raises plain errors with a
@@ -55,8 +55,10 @@ export interface RpcServer {
55
55
  readonly url: string;
56
56
  readonly device?: string;
57
57
  /**
58
- * Whether the loaded Fabric RPC backend was built with RDMA. Such a backend
59
- * negotiates RDMA with each RDMA-capable client and falls back to TCP otherwise.
58
+ * Whether new connections will try RDMA: the Fabric RPC backend was built
59
+ * with it, loaded libibverbs, found an RDMA device, and `GGML_RPC_NO_RDMA` is
60
+ * unset. Each connection still falls back to TCP when the client or the link
61
+ * cannot use RDMA.
60
62
  */
61
63
  readonly rdmaCapable: boolean;
62
64
  stop(): Promise<void>;
@@ -64,5 +66,9 @@ export interface RpcServer {
64
66
  export interface AllocateFreePortOptions {
65
67
  readonly allowNonLoopbackHost?: boolean;
66
68
  }
69
+ /**
70
+ * Finds a port that is free now. Another process can take it before you bind
71
+ * it, so `startRpcServer()` without a `port` lets the server bind one itself.
72
+ */
67
73
  export declare function allocateFreePort(host?: string, options?: AllocateFreePortOptions): Promise<number>;
68
74
  export declare function startRpcServer(options?: StartRpcServerOptions): Promise<RpcServer>;
package/index.js CHANGED
@@ -23,9 +23,6 @@ const TRUSTED_LAN_WARNING_CODE = "QVAC_GGML_RPC_SERVER_TRUSTED_LAN";
23
23
  // Keep native handles alive until an explicit stop finishes. Otherwise their
24
24
  // finalizer can synchronously stop and join a live server during garbage collection.
25
25
  const activeServerHandles = new Set();
26
- // Fabric never unloads its RPC module, so its RDMA build cannot change within a
27
- // process. Cache the native check, which reads the module from disk.
28
- let rdmaSupport;
29
26
  class RpcServerPortAllocationError extends Error {
30
27
  constructor(cause) {
31
28
  super("Failed to allocate a free port for ggml-rpc-server", { cause });
@@ -50,8 +47,12 @@ class RpcServerInvalidHostError extends Error {
50
47
  }
51
48
  exports.RpcServerInvalidHostError = RpcServerInvalidHostError;
52
49
  class RpcServerRdmaUnavailableError extends Error {
53
- constructor() {
54
- super("RDMA is not available in the installed @qvac/fabric RPC backend");
50
+ constructor(cause) {
51
+ super(
52
+ "RDMA is not available: the @qvac/fabric RPC backend lacks it, libibverbs.so.1 " +
53
+ "could not be loaded, the host has no RDMA device, or GGML_RPC_NO_RDMA is set",
54
+ { cause },
55
+ );
55
56
  this.name = "RpcServerRdmaUnavailableError";
56
57
  }
57
58
  }
@@ -115,6 +116,9 @@ const nativeErrorClasses = new Map([
115
116
  ]);
116
117
  function toTypedError(error) {
117
118
  const code = error?.code;
119
+ if (code === "RpcServerRdmaUnavailableError") {
120
+ return new RpcServerRdmaUnavailableError(error);
121
+ }
118
122
  const ErrorClass =
119
123
  typeof code === "string" ? nativeErrorClasses.get(code) : undefined;
120
124
  return ErrorClass === undefined
@@ -192,17 +196,10 @@ function validateThreads(threads) {
192
196
  throw new TypeError("threads must be a positive integer");
193
197
  }
194
198
  }
195
- // The RPC module @qvac/fabric loads, from wherever fabric ships its backends.
196
- function rpcBackendSupportsRdma() {
197
- if (rdmaSupport === undefined) {
198
- try {
199
- rdmaSupport = binding.rpcBackendSupportsRdma({});
200
- } catch (error) {
201
- throw toTypedError(error);
202
- }
203
- }
204
- return rdmaSupport;
205
- }
199
+ /**
200
+ * Finds a port that is free now. Another process can take it before you bind
201
+ * it, so `startRpcServer()` without a `port` lets the server bind one itself.
202
+ */
206
203
  function allocateFreePort(
207
204
  host = exports.DEFAULT_RPC_SERVER_HOST,
208
205
  options = {},
@@ -231,23 +228,17 @@ async function startRpcServer(options = {}) {
231
228
  assertLoopbackHost(host, options.allowNonLoopbackHost);
232
229
  warnForTrustedLanHost(host, options.allowNonLoopbackHost);
233
230
  validateThreads(options.threads);
234
- const rdmaCapable = rpcBackendSupportsRdma();
235
- if (options.expectRdma === true && !rdmaCapable) {
236
- throw new RpcServerRdmaUnavailableError();
237
- }
238
- const port =
239
- options.port ??
240
- (await allocateFreePort(host, {
241
- allowNonLoopbackHost: options.allowNonLoopbackHost,
242
- }));
243
- validatePort(port);
231
+ if (options.port !== undefined) validatePort(options.port);
244
232
  const device = normalizeDevice(options.device);
245
- const handle = await callNative(() =>
233
+ // Without a port the server binds port 0 itself and reports the port it got,
234
+ // so no other process can take the port between choosing and binding it.
235
+ const { handle, port, rdmaCapable } = await callNative(() =>
246
236
  binding.startServer({
247
- endpoint: `${host}:${port}`,
237
+ endpoint: `${host}:${options.port ?? 0}`,
248
238
  device,
249
239
  cache: options.cache ?? false,
250
240
  threads: options.threads,
241
+ expectRdma: options.expectRdma === true,
251
242
  }),
252
243
  );
253
244
  activeServerHandles.add(handle);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/ggml-rpc-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Managed ggml RPC server for QVAC LLM inference",
5
5
  "addon": true,
6
6
  "scripts": {
@@ -7,14 +7,13 @@
7
7
  ],
8
8
  "current_versions": [
9
9
  {
10
- "version": "0.4"
10
+ "version": "0.5"
11
11
  }
12
12
  ],
13
13
  "exported_symbols": [
14
14
  {
15
15
  "text": {
16
16
  "global": [
17
- "__ZN10rpc_server23rpcBackendHasRdmaMarkerERKNSt3__14__fs10filesystem4pathE",
18
17
  "__ZN10rpc_server12ServerRunner4stopEv",
19
18
  "__ZN10rpc_server13selectDevicesERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE",
20
19
  "_bare_register_module_v0",
@@ -7,14 +7,13 @@
7
7
  ],
8
8
  "current_versions": [
9
9
  {
10
- "version": "0.4"
10
+ "version": "0.5"
11
11
  }
12
12
  ],
13
13
  "exported_symbols": [
14
14
  {
15
15
  "text": {
16
16
  "global": [
17
- "__ZN10rpc_server23rpcBackendHasRdmaMarkerERKNSt3__14__fs10filesystem4pathE",
18
17
  "__ZN10rpc_server12ServerRunner4stopEv",
19
18
  "__ZN10rpc_server13selectDevicesERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE",
20
19
  "_bare_register_module_v0",
@@ -7,14 +7,13 @@
7
7
  ],
8
8
  "current_versions": [
9
9
  {
10
- "version": "0.4"
10
+ "version": "0.5"
11
11
  }
12
12
  ],
13
13
  "exported_symbols": [
14
14
  {
15
15
  "text": {
16
16
  "global": [
17
- "__ZN10rpc_server23rpcBackendHasRdmaMarkerERKNSt3__14__fs10filesystem4pathE",
18
17
  "__ZN10rpc_server12ServerRunner4stopEv",
19
18
  "__ZN10rpc_server13selectDevicesERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE",
20
19
  "_bare_register_module_v0",
@@ -7,14 +7,13 @@
7
7
  ],
8
8
  "current_versions": [
9
9
  {
10
- "version": "0.4"
10
+ "version": "0.5"
11
11
  }
12
12
  ],
13
13
  "exported_symbols": [
14
14
  {
15
15
  "text": {
16
16
  "global": [
17
- "__ZN10rpc_server23rpcBackendHasRdmaMarkerERKNSt3__14__fs10filesystem4pathE",
18
17
  "__ZN10rpc_server12ServerRunner4stopEv",
19
18
  "__ZN10rpc_server13selectDevicesERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE",
20
19
  "_bare_register_module_v0",
@@ -7,14 +7,13 @@
7
7
  ],
8
8
  "current_versions": [
9
9
  {
10
- "version": "0.4"
10
+ "version": "0.5"
11
11
  }
12
12
  ],
13
13
  "exported_symbols": [
14
14
  {
15
15
  "text": {
16
16
  "global": [
17
- "__ZN10rpc_server23rpcBackendHasRdmaMarkerERKNSt3__14__fs10filesystem4pathE",
18
17
  "__ZN10rpc_server12ServerRunner4stopEv",
19
18
  "__ZN10rpc_server13selectDevicesERKNSt3__112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE",
20
19
  "_bare_register_module_v0",