feat: DGR-002 - Adopt the versioned gRPC Shard protocol
This commit is contained in:
74
packages/node/native/CMakeLists.txt
Normal file
74
packages/node/native/CMakeLists.txt
Normal file
@@ -0,0 +1,74 @@
|
||||
# Native Shard protocol: C++ schema generation, build wiring and conformance test.
|
||||
#
|
||||
# The C++ stubs are generated into the build tree at configure time — they are
|
||||
# never committed. A C++ consumer already needs a toolchain, so committing
|
||||
# generated C++ would only create a second copy of the schema that can rot.
|
||||
#
|
||||
# gRPC C++ is optional here on purpose. The conformance test only needs message
|
||||
# types, so the schema can be verified on a machine that has protobuf but not
|
||||
# the gRPC C++ stack. When gRPC *is* found, the service stubs are generated too
|
||||
# and exported as `shard_runtime_grpc` for the worker (DGR-008) to link.
|
||||
#
|
||||
# Build:
|
||||
# cmake -S packages/node/native -B build/native -DCMAKE_PREFIX_PATH=<protobuf-install>
|
||||
# cmake --build build/native -j
|
||||
# ctest --test-dir build/native --output-on-failure
|
||||
#
|
||||
# `scripts/bootstrap_native_toolchain.sh` builds a protobuf install from source
|
||||
# when the system has none.
|
||||
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(meshnet_shard_protocol CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(protobuf CONFIG REQUIRED)
|
||||
find_package(gRPC CONFIG QUIET)
|
||||
|
||||
set(SHARD_PROTO "${CMAKE_CURRENT_SOURCE_DIR}/proto/shard_runtime.proto")
|
||||
|
||||
# Message types: always available.
|
||||
add_library(shard_runtime_proto STATIC "${SHARD_PROTO}")
|
||||
target_link_libraries(shard_runtime_proto PUBLIC protobuf::libprotobuf)
|
||||
target_include_directories(shard_runtime_proto PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
protobuf_generate(
|
||||
TARGET shard_runtime_proto
|
||||
LANGUAGE cpp
|
||||
IMPORT_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/proto"
|
||||
PROTOC_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
|
||||
# Service stubs: only when the gRPC C++ stack is present.
|
||||
if(gRPC_FOUND)
|
||||
add_library(shard_runtime_grpc STATIC "${SHARD_PROTO}")
|
||||
target_link_libraries(shard_runtime_grpc PUBLIC shard_runtime_proto gRPC::grpc++)
|
||||
target_include_directories(shard_runtime_grpc PUBLIC "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
protobuf_generate(
|
||||
TARGET shard_runtime_grpc
|
||||
LANGUAGE grpc
|
||||
GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc
|
||||
PLUGIN "protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>"
|
||||
IMPORT_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/proto"
|
||||
PROTOC_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
message(STATUS "gRPC C++ found: building ShardRuntime service stubs")
|
||||
else()
|
||||
message(STATUS "gRPC C++ not found: building message types only "
|
||||
"(sufficient for the conformance test)")
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(shard_protocol_conformance tests/test_shard_protocol_conformance.cpp)
|
||||
target_link_libraries(shard_protocol_conformance PRIVATE shard_runtime_proto)
|
||||
|
||||
# The test asserts against the same committed vectors the Python test uses, and
|
||||
# writes its own re-serialization back out so Python can prove the two languages
|
||||
# agree byte-for-byte.
|
||||
add_test(
|
||||
NAME shard_protocol_conformance
|
||||
COMMAND shard_protocol_conformance
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/testdata"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
69
packages/node/native/README.md
Normal file
69
packages/node/native/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Native Shard protocol
|
||||
|
||||
`proto/shard_runtime.proto` is the semantic contract between a Meshnet node and
|
||||
a Shard worker: Protocol Buffers over gRPC/HTTP2 (ADR-0020). It is the source of
|
||||
truth. The Python and C++ types are generated from it; neither is the contract.
|
||||
|
||||
## What lives here
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `proto/shard_runtime.proto` | The schema: capability, health, session stream, release, cancel |
|
||||
| `testdata/*.binpb` | Committed conformance vectors both languages assert against |
|
||||
| `tests/test_shard_protocol_conformance.cpp` | C++ conformance test |
|
||||
| `CMakeLists.txt` | C++ generation, build wiring, and `ctest` registration |
|
||||
|
||||
The Python stubs are generated into
|
||||
`packages/node/meshnet_node/native_protocol/generated/` and are committed, so
|
||||
installing a node needs no protoc. The C++ stubs are generated into the build
|
||||
tree and are never committed — a C++ consumer already has a toolchain, and a
|
||||
committed copy could only rot.
|
||||
|
||||
## Regenerating
|
||||
|
||||
```bash
|
||||
pip install grpcio-tools==1.82.1 # bundles protoc; no system protoc needed
|
||||
python scripts/generate_native_protocol.py # rewrite the Python stubs
|
||||
python scripts/generate_native_protocol.py --check # fail if they drifted
|
||||
python scripts/generate_protocol_goldens.py --check # fail if the vectors drifted
|
||||
```
|
||||
|
||||
Both `--check` modes run in CI via `tests/test_native_shard_protocol.py`, so a
|
||||
schema edit that is not accompanied by regenerated output fails the suite rather
|
||||
than shipping stubs that disagree with the schema they claim to implement.
|
||||
|
||||
## Building and running the C++ conformance test
|
||||
|
||||
If the machine has no protobuf C++ toolchain:
|
||||
|
||||
```bash
|
||||
scripts/bootstrap_native_toolchain.sh build/native-toolchain
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
cmake -S packages/node/native -B build/native \
|
||||
-DCMAKE_PREFIX_PATH="$PWD/build/native-toolchain"
|
||||
cmake --build build/native -j
|
||||
ctest --test-dir build/native --output-on-failure
|
||||
```
|
||||
|
||||
gRPC C++ is optional: without it, CMake builds the message types only, which is
|
||||
all the conformance test needs. When gRPC C++ *is* found, the `ShardRuntime`
|
||||
service stubs are built too and exported as `shard_runtime_grpc` for the worker
|
||||
(DGR-008) to link.
|
||||
|
||||
## How the cross-language check actually proves something
|
||||
|
||||
Two codecs that each round-trip their own output prove only that each is
|
||||
self-consistent. Instead:
|
||||
|
||||
1. Python builds the canonical message and commits its bytes to `testdata/`.
|
||||
2. The C++ test parses *those* bytes, asserts every field, independently
|
||||
recomputes the CRC32C from the polynomial, and re-serializes to
|
||||
`cpp_roundtrip.binpb` in the build tree.
|
||||
3. `test_cpp_and_python_agree_byte_for_byte` compares that file to the golden.
|
||||
|
||||
Byte equality across the two implementations is the claim; anything less is two
|
||||
parallel test suites that can drift apart.
|
||||
591
packages/node/native/proto/shard_runtime.proto
Normal file
591
packages/node/native/proto/shard_runtime.proto
Normal file
@@ -0,0 +1,591 @@
|
||||
// The native Shard data plane: Protocol Buffers over gRPC/HTTP2 (ADR-0020).
|
||||
//
|
||||
// This schema — not any Python or C++ type — is the semantic contract between a
|
||||
// Meshnet node and a Shard worker. Direct hops speak gRPC. When direct
|
||||
// connectivity is unavailable the existing relay carries these exact serialized
|
||||
// frames as opaque binary, which is why every deadline, cancellation and
|
||||
// identity field is carried *in the schema* rather than left to gRPC metadata:
|
||||
// a relayed frame has no HTTP/2 context to inherit them from.
|
||||
//
|
||||
// Meshnet remains the only control plane. Nothing here selects routes, prices
|
||||
// work, or authenticates peers; the worker executes a layer range it has been
|
||||
// admitted for and reports what it did.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package meshnet.shard.v1;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "github.com/meshnet/shard/v1;shardv1";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Versioning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Wire-compatibility generation of this schema. A worker rejects a peer whose
|
||||
// SCHEMA_VERSION it cannot satisfy instead of guessing at field meaning.
|
||||
//
|
||||
// This is deliberately NOT the `X-Meshnet-Wire` version of the legacy HTTP
|
||||
// activation format (currently "2", see ADR-0008). The native protocol is a
|
||||
// separate contract with its own generation counter and starts at 1.
|
||||
enum SchemaVersion {
|
||||
SCHEMA_VERSION_UNSPECIFIED = 0;
|
||||
SCHEMA_VERSION_1 = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tensor bundle — the public activation boundary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Element type of a tensor payload.
|
||||
//
|
||||
// bfloat16 is the canonical activation dtype at every Shard boundary regardless
|
||||
// of the weight quantization a node uses internally (ADR-0008): weights may be
|
||||
// NF4 or INT8, compute upcasts, and the boundary stays bfloat16. The other
|
||||
// values exist for auxiliary tensors (position ids, attention masks) and for
|
||||
// architectures whose boundary is not a plain hidden state.
|
||||
enum DType {
|
||||
DTYPE_UNSPECIFIED = 0;
|
||||
DTYPE_BFLOAT16 = 1;
|
||||
DTYPE_FLOAT16 = 2;
|
||||
DTYPE_FLOAT32 = 3;
|
||||
DTYPE_INT32 = 4;
|
||||
DTYPE_INT64 = 5;
|
||||
DTYPE_UINT8 = 6;
|
||||
DTYPE_INT8 = 7;
|
||||
DTYPE_BOOL = 8;
|
||||
}
|
||||
|
||||
// Byte order of a tensor payload. Little-endian is canonical on the wire; the
|
||||
// field is explicit so a big-endian peer is rejected loudly rather than
|
||||
// silently reading byte-swapped activations.
|
||||
enum ByteOrder {
|
||||
BYTE_ORDER_UNSPECIFIED = 0;
|
||||
BYTE_ORDER_LITTLE_ENDIAN = 1;
|
||||
BYTE_ORDER_BIG_ENDIAN = 2;
|
||||
}
|
||||
|
||||
// Payload compression. Mirrors the policy-driven zstd decision the existing
|
||||
// activation seam already makes (`activation_compression.py`): compression is a
|
||||
// transport optimisation and NONE is always a legal choice for any payload.
|
||||
enum Compression {
|
||||
COMPRESSION_UNSPECIFIED = 0;
|
||||
COMPRESSION_NONE = 1;
|
||||
COMPRESSION_ZSTD = 2;
|
||||
}
|
||||
|
||||
// Integrity algorithm covering a payload.
|
||||
enum ChecksumAlgorithm {
|
||||
CHECKSUM_ALGORITHM_UNSPECIFIED = 0;
|
||||
CHECKSUM_ALGORITHM_NONE = 1;
|
||||
CHECKSUM_ALGORITHM_CRC32C = 2;
|
||||
}
|
||||
|
||||
// An integrity check over the *uncompressed* canonical payload bytes.
|
||||
//
|
||||
// Checksumming the uncompressed bytes (not the compressed frame) means the
|
||||
// value is stable whether a hop compressed the payload or not, so a relay may
|
||||
// re-frame or a peer may decline compression without invalidating it.
|
||||
message Checksum {
|
||||
ChecksumAlgorithm algorithm = 1;
|
||||
// Big-endian encoding of the checksum value.
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
// One slice of a tensor's wire payload.
|
||||
//
|
||||
// Fragments bound the size of a single gRPC message. `byte_offset` lets a
|
||||
// receiver verify the fragments tile the body exactly — no hole, no overlap —
|
||||
// instead of trusting arrival order.
|
||||
//
|
||||
// Offsets and payloads describe the *wire* body, which is the compressed frame
|
||||
// when the parent tensor declares a compression. A zstd frame is not decodable
|
||||
// per fragment, so a receiver reassembles first and decompresses once. The
|
||||
// uncompressed size is not repeated here: `NamedTensor.total_bytes` is the
|
||||
// single source of truth, and a second copy could only ever disagree with it.
|
||||
message TensorFragment {
|
||||
uint32 fragment_index = 1;
|
||||
uint32 fragment_count = 2;
|
||||
// Offset of this fragment within the tensor's wire body.
|
||||
uint64 byte_offset = 3;
|
||||
// Fragment bytes, compressed iff the parent tensor declares a compression.
|
||||
bytes payload = 4;
|
||||
|
||||
reserved 5;
|
||||
reserved "uncompressed_size";
|
||||
}
|
||||
|
||||
// One named tensor at an architecture boundary.
|
||||
message NamedTensor {
|
||||
// Architecture-defined boundary name, e.g. "hidden_states", "position_ids".
|
||||
// A boundary may need more than one tensor, which is why the payload is a
|
||||
// named bundle rather than a bare buffer (ADR-0020).
|
||||
string name = 1;
|
||||
repeated int64 shape = 2;
|
||||
DType dtype = 3;
|
||||
ByteOrder byte_order = 4;
|
||||
// Total uncompressed payload size. A receiver sizes its buffer from this
|
||||
// before reading fragments and refuses a bundle that exceeds its budget.
|
||||
uint64 total_bytes = 5;
|
||||
Compression compression = 6;
|
||||
// Over the uncompressed payload; see Checksum.
|
||||
Checksum checksum = 7;
|
||||
// Ordered fragments. A tensor small enough for one message carries exactly
|
||||
// one fragment with fragment_count == 1.
|
||||
repeated TensorFragment fragments = 8;
|
||||
}
|
||||
|
||||
// A versioned named-tensor bundle: the public activation boundary payload.
|
||||
message TensorBundle {
|
||||
// Generation of the bundle layout itself, independent of SchemaVersion so a
|
||||
// boundary payload can evolve without a whole-protocol version bump.
|
||||
uint32 bundle_version = 1;
|
||||
repeated NamedTensor tensors = 2;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identity: what work this is, for which route, against which artifact
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Exact identity of the executable thing a Shard runs.
|
||||
//
|
||||
// Both fingerprints must match end to end across a route. Two nodes agreeing on
|
||||
// a model name but not on quantization, kernel or tail-norm placement would
|
||||
// produce silently wrong tokens, so identity is a digest, not a label.
|
||||
message Fingerprint {
|
||||
// Digest of the exact Model Artifact (weights + config), e.g. the GGUF hash.
|
||||
string model_artifact_digest = 1;
|
||||
// Digest of the exact runtime recipe (backend, quantization, kernels, dtype).
|
||||
string runtime_recipe_digest = 2;
|
||||
// Human-readable recipe identity, mirroring the capability report an admitted
|
||||
// node already registers with (ADR-0023). Labels are for diagnosis; the
|
||||
// digests above are what a peer actually compares.
|
||||
string recipe_id = 3;
|
||||
string recipe_version = 4;
|
||||
string catalogue_version = 5;
|
||||
}
|
||||
|
||||
// The contiguous transformer layer range a Shard owns.
|
||||
message ShardRange {
|
||||
// Registered range, inclusive start, exclusive end.
|
||||
uint32 start_layer = 1;
|
||||
uint32 end_layer = 2;
|
||||
// The layer this Shard must actually begin at for this route (ADR-0012).
|
||||
//
|
||||
// Shard ranges may overlap; the Tracker resolves the overlap when it builds
|
||||
// the route and every hop is told where the previous hop stopped. Executing
|
||||
// from `start_layer` when `effective_start_layer` is higher would re-apply
|
||||
// layers already applied to the incoming activation and silently corrupt the
|
||||
// result. A worker executes [effective_start_layer, end_layer).
|
||||
uint32 effective_start_layer = 3;
|
||||
}
|
||||
|
||||
// Which part of generation a message belongs to.
|
||||
enum Phase {
|
||||
PHASE_UNSPECIFIED = 0;
|
||||
PHASE_PREFILL = 1;
|
||||
PHASE_DECODE = 2;
|
||||
PHASE_RELEASE = 3;
|
||||
PHASE_CANCEL = 4;
|
||||
}
|
||||
|
||||
// Token span carried by one chunk.
|
||||
message PositionSpan {
|
||||
// Absolute position of the first token in this chunk within the sequence.
|
||||
uint64 first_position = 1;
|
||||
// Number of token positions in this chunk. A decode step carries 1.
|
||||
uint32 token_count = 2;
|
||||
}
|
||||
|
||||
// Bounded chunking for a prefill.
|
||||
//
|
||||
// A long prompt is split into token-aligned chunks before the first forward
|
||||
// pass (ADR-0008) so peak transfer per boundary stays bounded regardless of
|
||||
// prompt length. Splits never fall mid-token.
|
||||
message ChunkInfo {
|
||||
uint32 chunk_index = 1;
|
||||
uint32 chunk_count = 2;
|
||||
// True on the final chunk of a prefill. A receiver that has not seen a final
|
||||
// chunk knows the prefill is still incomplete.
|
||||
bool final_chunk = 3;
|
||||
}
|
||||
|
||||
// How the sender expects the receiver's Hot KV State to be positioned.
|
||||
enum CacheMode {
|
||||
CACHE_MODE_UNSPECIFIED = 0;
|
||||
// No session state; the payload carries everything needed. Always a legal
|
||||
// fallback and the recovery path after a miss.
|
||||
CACHE_MODE_STATELESS = 1;
|
||||
// Establish fresh session state for this Shard's layer range.
|
||||
CACHE_MODE_PREFILL = 2;
|
||||
// Continue existing session state. `expected_past_len` must match exactly.
|
||||
CACHE_MODE_DECODE = 3;
|
||||
}
|
||||
|
||||
// The sender's expectation about receiver-side cache state (ADR-0022).
|
||||
//
|
||||
// A mismatch is always a declared cache miss, never a silent stateless forward:
|
||||
// running a single-token decode payload without the matching cache would emit
|
||||
// plausible garbage. The receiver answers a miss with ShardError.CACHE_MISS and
|
||||
// the head re-prefills the whole sequence under the same Route Session ID.
|
||||
message CacheExpectation {
|
||||
CacheMode mode = 1;
|
||||
// Decode only: the number of tokens the receiver's session cache must already
|
||||
// hold for this Shard's layer range.
|
||||
uint64 expected_past_len = 2;
|
||||
}
|
||||
|
||||
// What the receiver's cache actually did.
|
||||
message CacheResult {
|
||||
CacheMode mode = 1;
|
||||
// Tokens held for this Shard's range after the step completed.
|
||||
uint64 past_len = 2;
|
||||
// True when session state was reused rather than rebuilt.
|
||||
bool cache_hit = 3;
|
||||
}
|
||||
|
||||
// Everything required to interpret one unit of work, independent of transport.
|
||||
//
|
||||
// Carried on every request and response because a relayed frame arrives as
|
||||
// opaque binary with no gRPC metadata, no deadline and no channel identity.
|
||||
message Envelope {
|
||||
SchemaVersion schema_version = 1;
|
||||
// Unique id for this unit of work; also the unit of billing attribution.
|
||||
string work_id = 2;
|
||||
// The Route Session this work belongs to. Together with `route_epoch` it maps
|
||||
// to exactly one isolated llama sequence / bounded context (ADR-0020).
|
||||
string route_session_id = 3;
|
||||
// Bumped by the control plane whenever the route changes. A worker refuses
|
||||
// work from a stale epoch rather than mixing it into live session state.
|
||||
uint64 route_epoch = 4;
|
||||
Fingerprint fingerprint = 5;
|
||||
ShardRange shard_range = 6;
|
||||
Phase phase = 7;
|
||||
PositionSpan position = 8;
|
||||
// Monotonic step within (route_session_id, route_epoch).
|
||||
//
|
||||
// Idempotency key: a retried or duplicated step carries the same value and
|
||||
// must be acknowledged, not re-applied. Re-applying a decode step would
|
||||
// advance the KV cache twice and desynchronise the route.
|
||||
uint64 idempotency_step = 9;
|
||||
CacheExpectation cache_expectation = 10;
|
||||
// Absolute deadline. Carried in-band so a relayed frame keeps its deadline;
|
||||
// on a direct hop it is set consistently with the gRPC deadline.
|
||||
int64 deadline_unix_nanos = 11;
|
||||
// Chunking, when this message is part of a bounded prefill.
|
||||
ChunkInfo chunk = 12;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Structured errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Why a unit of work could not be executed as asked.
|
||||
//
|
||||
// These are semantic outcomes, distinct from transport failure. They travel as
|
||||
// a ShardStatus on the stream (and as google.rpc.Status details on unary RPCs)
|
||||
// so a relayed frame carries the same diagnosis a direct gRPC hop would.
|
||||
enum ErrorCode {
|
||||
ERROR_CODE_UNSPECIFIED = 0;
|
||||
// Peer cannot satisfy the requested SchemaVersion.
|
||||
ERROR_CODE_SCHEMA_UNSUPPORTED = 1;
|
||||
// Model artifact or runtime recipe digest differs from this worker's.
|
||||
ERROR_CODE_FINGERPRINT_MISMATCH = 2;
|
||||
// Work arrived for a route epoch the worker has already moved past.
|
||||
ERROR_CODE_EPOCH_STALE = 3;
|
||||
// Requested layer range is not the range this worker serves.
|
||||
ERROR_CODE_SHARD_RANGE_MISMATCH = 4;
|
||||
// Session state absent or positioned differently than expected (ADR-0022).
|
||||
// The head recovers with one full re-prefill; this is not a fatal error.
|
||||
ERROR_CODE_CACHE_MISS = 5;
|
||||
// Admission budget (weights, KV, scratch, queue depth) would be exceeded.
|
||||
ERROR_CODE_RESOURCE_EXHAUSTED = 6;
|
||||
// Payload failed its checksum, or fragments did not cover the tensor.
|
||||
ERROR_CODE_PAYLOAD_CORRUPT = 7;
|
||||
// Work was cancelled by the control plane or the peer.
|
||||
ERROR_CODE_CANCELLED = 8;
|
||||
// Deadline in the envelope had already passed when work was dequeued.
|
||||
ERROR_CODE_DEADLINE_EXCEEDED = 9;
|
||||
// Sender exceeded its granted flow-control credit.
|
||||
ERROR_CODE_FLOW_CONTROL_VIOLATION = 10;
|
||||
// The worker failed while executing; detail is sanitized.
|
||||
ERROR_CODE_INTERNAL = 11;
|
||||
}
|
||||
|
||||
message ShardError {
|
||||
ErrorCode code = 1;
|
||||
// Sanitized, operator-facing explanation. Never a raw exception, file path or
|
||||
// credential (ADR-0023 sanitization rule).
|
||||
string detail = 2;
|
||||
// True when the same work may be retried unchanged. A CACHE_MISS is not
|
||||
// retryable unchanged — the head must re-prefill first.
|
||||
bool retryable = 3;
|
||||
// Set on CACHE_MISS so the head knows where the receiver actually is.
|
||||
uint64 actual_past_len = 4;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flow control
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Application-level credit, layered on top of HTTP/2 flow control.
|
||||
//
|
||||
// HTTP/2 bounds bytes in flight; it does not bound how much *work* a worker has
|
||||
// queued, and a relayed frame gets no HTTP/2 window at all. Credits bound the
|
||||
// number of un-acked chunks a sender may have outstanding, which is what keeps
|
||||
// worker queues and KV pressure bounded and lets prefill avoid starving decode.
|
||||
message FlowControl {
|
||||
// Additional chunks the receiver is willing to accept beyond those already
|
||||
// granted. Purely additive: a grant never reduces outstanding credit.
|
||||
uint32 credits_granted = 1;
|
||||
// Hard ceiling on un-acked chunks, independent of granted credit.
|
||||
uint32 max_inflight_chunks = 2;
|
||||
// Hard ceiling on the serialized size of one chunk message.
|
||||
uint64 max_chunk_bytes = 3;
|
||||
// Hard ceiling on token positions per prefill chunk.
|
||||
uint32 max_prefill_chunk_tokens = 4;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session stream messages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Opens one long-lived bidirectional stream for one Route Session Activation
|
||||
// Seam. The handshake settles version, identity and the initial flow-control
|
||||
// window before any activation is sent, so an incompatible peer fails at open
|
||||
// rather than mid-generation.
|
||||
message SessionOpen {
|
||||
// Highest schema version the initiator supports; the acceptor replies with
|
||||
// the version actually negotiated.
|
||||
SchemaVersion schema_version = 1;
|
||||
string route_session_id = 2;
|
||||
uint64 route_epoch = 3;
|
||||
Fingerprint fingerprint = 4;
|
||||
ShardRange shard_range = 5;
|
||||
// Flow-control limits the initiator can honour. The acceptor replies with the
|
||||
// limits that actually apply.
|
||||
FlowControl proposed_flow_control = 6;
|
||||
// Compressions the initiator can decode. NONE is implicitly always supported.
|
||||
repeated Compression accepted_compression = 7;
|
||||
}
|
||||
|
||||
message SessionAccepted {
|
||||
// The version both peers will use for the life of this stream.
|
||||
SchemaVersion schema_version = 1;
|
||||
string route_session_id = 2;
|
||||
uint64 route_epoch = 3;
|
||||
// Authoritative limits. The initiator must not exceed these.
|
||||
FlowControl flow_control = 4;
|
||||
repeated Compression accepted_compression = 5;
|
||||
// Fingerprint the worker actually serves, so a mismatch is visible at open.
|
||||
Fingerprint fingerprint = 6;
|
||||
}
|
||||
|
||||
// One unit of activation work: a bounded prefill chunk or a decode step.
|
||||
message ActivationChunk {
|
||||
Envelope envelope = 1;
|
||||
TensorBundle bundle = 2;
|
||||
}
|
||||
|
||||
// The small decode fast path.
|
||||
//
|
||||
// A decode step is one token: the envelope's identity fields are already fixed
|
||||
// for the life of the stream, so repeating them per token is pure overhead on
|
||||
// the hottest path. A DecodeStep carries only what changes — the step, the
|
||||
// position, and one tensor — and inherits the rest from the SessionOpen
|
||||
// handshake. A peer may always fall back to ActivationChunk with PHASE_DECODE.
|
||||
message DecodeStep {
|
||||
// Idempotency step within the session; also orders the stream.
|
||||
uint64 idempotency_step = 1;
|
||||
// Absolute position of this token.
|
||||
uint64 position = 2;
|
||||
// Tokens the receiver's cache must already hold. A mismatch is a CACHE_MISS.
|
||||
uint64 expected_past_len = 3;
|
||||
// The single boundary tensor for this token, typically [1, 1, hidden].
|
||||
NamedTensor tensor = 4;
|
||||
// Work id for attribution; the session/epoch/fingerprint/range are inherited.
|
||||
string work_id = 5;
|
||||
int64 deadline_unix_nanos = 6;
|
||||
}
|
||||
|
||||
// Drop session state for a Route Session. Bounded memory does not depend on
|
||||
// this arriving — workers also evict by TTL and LRU (ADR-0022) — but an
|
||||
// explicit release returns KV immediately instead of holding it for the TTL.
|
||||
message ReleaseSignal {
|
||||
string route_session_id = 1;
|
||||
uint64 route_epoch = 2;
|
||||
string work_id = 3;
|
||||
}
|
||||
|
||||
// Abandon in-flight work.
|
||||
//
|
||||
// Cancellation must remain possible when the stream is wedged behind flow
|
||||
// control, which is why Cancel also exists as a unary RPC on a fresh call.
|
||||
message CancelSignal {
|
||||
string route_session_id = 1;
|
||||
uint64 route_epoch = 2;
|
||||
// Cancel one unit of work; empty cancels every in-flight unit for the session.
|
||||
string work_id = 3;
|
||||
string reason = 4;
|
||||
}
|
||||
|
||||
// Acknowledges a chunk or decode step, releasing its flow-control credit and
|
||||
// recording where the receiver's cache now stands.
|
||||
message Ack {
|
||||
string work_id = 1;
|
||||
uint64 idempotency_step = 2;
|
||||
CacheResult cache_result = 3;
|
||||
// True when the step was recognised as an already-applied duplicate and was
|
||||
// therefore acknowledged without being re-applied.
|
||||
bool duplicate = 4;
|
||||
// Observed execution time, for Generation Telemetry.
|
||||
uint64 execution_nanos = 5;
|
||||
}
|
||||
|
||||
// A terminal outcome for one unit of work, or for the session.
|
||||
message ShardStatus {
|
||||
string work_id = 1;
|
||||
string route_session_id = 2;
|
||||
uint64 idempotency_step = 3;
|
||||
ShardError error = 4;
|
||||
// True when the stream itself is finished and no further work will be served.
|
||||
bool terminal = 5;
|
||||
}
|
||||
|
||||
message SessionRequest {
|
||||
oneof kind {
|
||||
SessionOpen open = 1;
|
||||
ActivationChunk chunk = 2;
|
||||
DecodeStep decode = 3;
|
||||
FlowControl flow_control = 4;
|
||||
ReleaseSignal release = 5;
|
||||
CancelSignal cancel = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message SessionResponse {
|
||||
oneof kind {
|
||||
SessionAccepted accepted = 1;
|
||||
// Result activations forwarded toward the next Shard, or to the head.
|
||||
ActivationChunk chunk = 2;
|
||||
Ack ack = 3;
|
||||
FlowControl flow_control = 4;
|
||||
ShardStatus status = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Capability and health
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message CapabilityRequest {
|
||||
SchemaVersion schema_version = 1;
|
||||
}
|
||||
|
||||
// What this worker can actually execute, proven by a bounded real forward
|
||||
// (ADR-0023). Detected hardware is not a capability; only a validated recipe is.
|
||||
message CapabilityReport {
|
||||
SchemaVersion schema_version = 1;
|
||||
Fingerprint fingerprint = 2;
|
||||
ShardRange shard_range = 3;
|
||||
// Backend/device identity, e.g. "rocm:gfx1151", "cpu:avx512".
|
||||
string backend = 4;
|
||||
string device = 5;
|
||||
// True only when a bounded real forward has passed for exactly this
|
||||
// (artifact, range, recipe, device). Never set from hardware detection alone.
|
||||
bool validated = 6;
|
||||
// Sanitized reason when `validated` is false.
|
||||
string detail = 7;
|
||||
// Admission budgets this worker will enforce.
|
||||
uint64 max_concurrent_sessions = 8;
|
||||
uint64 max_context_tokens = 9;
|
||||
FlowControl flow_control = 10;
|
||||
repeated Compression accepted_compression = 11;
|
||||
repeated SchemaVersion supported_schema_versions = 12;
|
||||
int64 validated_at_unix_nanos = 13;
|
||||
}
|
||||
|
||||
message HealthRequest {
|
||||
SchemaVersion schema_version = 1;
|
||||
}
|
||||
|
||||
enum ServingState {
|
||||
SERVING_STATE_UNSPECIFIED = 0;
|
||||
// Accepting new Route Sessions.
|
||||
SERVING_STATE_SERVING = 1;
|
||||
// Alive but refusing new sessions; existing sessions continue draining.
|
||||
SERVING_STATE_DRAINING = 2;
|
||||
// Alive but not routable — e.g. registered-but-dark pending certification.
|
||||
SERVING_STATE_NOT_SERVING = 3;
|
||||
}
|
||||
|
||||
// Live load, for backpressure and Generation Telemetry.
|
||||
message HealthReport {
|
||||
SchemaVersion schema_version = 1;
|
||||
ServingState state = 2;
|
||||
uint32 active_sessions = 3;
|
||||
uint32 queued_chunks = 4;
|
||||
// Occupancy of the continuous decode batch.
|
||||
uint32 batch_occupancy = 5;
|
||||
// Fraction of the KV budget in use, 0..1.
|
||||
float kv_pressure = 6;
|
||||
uint64 resident_bytes = 7;
|
||||
string detail = 8;
|
||||
}
|
||||
|
||||
message ReleaseRequest {
|
||||
SchemaVersion schema_version = 1;
|
||||
string route_session_id = 2;
|
||||
uint64 route_epoch = 3;
|
||||
}
|
||||
|
||||
message ReleaseResponse {
|
||||
// True when session state existed and was dropped; false when there was
|
||||
// nothing to drop, which is a success, not an error — release is idempotent.
|
||||
bool released = 1;
|
||||
ShardError error = 2;
|
||||
}
|
||||
|
||||
message CancelRequest {
|
||||
SchemaVersion schema_version = 1;
|
||||
string route_session_id = 2;
|
||||
uint64 route_epoch = 3;
|
||||
// Empty cancels every in-flight unit for the session.
|
||||
string work_id = 4;
|
||||
string reason = 5;
|
||||
}
|
||||
|
||||
message CancelResponse {
|
||||
uint32 cancelled_work_items = 1;
|
||||
ShardError error = 2;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service ShardRuntime {
|
||||
// What this worker can execute. Read before a route is built.
|
||||
rpc GetCapability(CapabilityRequest) returns (CapabilityReport);
|
||||
|
||||
// Live load and serving state.
|
||||
rpc Health(HealthRequest) returns (HealthReport);
|
||||
|
||||
// One long-lived bidirectional stream per Route Session Activation Seam.
|
||||
//
|
||||
// The stream opens with SessionOpen/SessionAccepted, then carries bounded
|
||||
// prefill chunks and decode steps in both directions for the life of the
|
||||
// session. Per-token channel creation is a non-goal: the handshake cost is
|
||||
// paid once and the hot path carries only what changes.
|
||||
rpc Session(stream SessionRequest) returns (stream SessionResponse);
|
||||
|
||||
// Drop session state out of band. Idempotent.
|
||||
rpc Release(ReleaseRequest) returns (ReleaseResponse);
|
||||
|
||||
// Cancel out of band, on a fresh call.
|
||||
//
|
||||
// In-band CancelSignal is preferred, but a sender that is blocked on flow
|
||||
// control cannot write one — a cancel that can only travel down a wedged
|
||||
// stream is not a cancel. This RPC always has a path to the worker.
|
||||
rpc Cancel(CancelRequest) returns (CancelResponse);
|
||||
}
|
||||
2
packages/node/native/testdata/capability_report_golden.binpb
vendored
Normal file
2
packages/node/native/testdata/capability_report_golden.binpb
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
F
|
||||
sha256:1f0c9d2esha256:ab77e410llama-gguf-q4km-rocm"3* 2026.07.1"rocm*gfx11510@H€@R€€€ €Zbh€€Ð<E282AC>éθý
|
||||
BIN
packages/node/native/testdata/session_request_golden.binpb
vendored
Normal file
BIN
packages/node/native/testdata/session_request_golden.binpb
vendored
Normal file
Binary file not shown.
337
packages/node/native/tests/test_shard_protocol_conformance.cpp
Normal file
337
packages/node/native/tests/test_shard_protocol_conformance.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
// C++ conformance test for the native Shard protocol.
|
||||
//
|
||||
// This test does not check that C++ can round-trip its own output — that would
|
||||
// only prove C++ is self-consistent. It parses the *Python-produced* committed
|
||||
// vectors, asserts every field the protocol promises to carry, independently
|
||||
// recomputes the CRC32C over the reassembled tensor, and re-serializes the
|
||||
// message back out for the Python test to compare byte-for-byte. Only that
|
||||
// closes the loop: both languages agree on the same bytes and the same meaning.
|
||||
//
|
||||
// Run via ctest; see packages/node/native/CMakeLists.txt.
|
||||
|
||||
#include "shard_runtime.pb.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// `pb` is already taken by protobuf's own generated headers.
|
||||
namespace sp = meshnet::shard::v1;
|
||||
|
||||
namespace {
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::cerr << "FAIL " << __FILE__ << ":" << __LINE__ << ": " << #cond \
|
||||
<< "\n"; \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_EQ(actual, expected) \
|
||||
do { \
|
||||
auto &&a_ = (actual); \
|
||||
auto &&e_ = (expected); \
|
||||
if (!(a_ == e_)) { \
|
||||
std::cerr << "FAIL " << __FILE__ << ":" << __LINE__ << ": " << #actual \
|
||||
<< " == " << #expected << "\n actual: " << a_ \
|
||||
<< "\n expected: " << e_ << "\n"; \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Independent CRC32C (Castagnoli). Written from the polynomial rather than
|
||||
// shared with the Python side on purpose: a checksum that both languages
|
||||
// compute with the *same* code proves nothing about interoperability.
|
||||
uint32_t Crc32c(const std::string &data) {
|
||||
static uint32_t table[256];
|
||||
static bool built = false;
|
||||
if (!built) {
|
||||
for (uint32_t i = 0; i < 256; ++i) {
|
||||
uint32_t c = i;
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
c = (c & 1) ? (c >> 1) ^ 0x82F63B78u : (c >> 1);
|
||||
}
|
||||
table[i] = c;
|
||||
}
|
||||
built = true;
|
||||
}
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (unsigned char byte : data) {
|
||||
crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF];
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path &path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) {
|
||||
std::cerr << "FAIL cannot read " << path << "\n";
|
||||
++g_failures;
|
||||
return {};
|
||||
}
|
||||
std::ostringstream buffer;
|
||||
buffer << in.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
// Reassemble a tensor's fragments and validate coverage, exactly as a worker
|
||||
// must before it feeds the bytes to a forward pass.
|
||||
std::string ReassembleUncompressed(const sp::NamedTensor &tensor) {
|
||||
std::string body;
|
||||
std::vector<const sp::TensorFragment *> fragments;
|
||||
for (const auto &fragment : tensor.fragments()) {
|
||||
fragments.push_back(&fragment);
|
||||
}
|
||||
CHECK(!fragments.empty());
|
||||
for (uint32_t index = 0; index < fragments.size(); ++index) {
|
||||
const sp::TensorFragment *found = nullptr;
|
||||
for (const auto *fragment : fragments) {
|
||||
if (fragment->fragment_index() == index) {
|
||||
found = fragment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(found != nullptr);
|
||||
if (found == nullptr) {
|
||||
return {};
|
||||
}
|
||||
CHECK_EQ(found->fragment_count(),
|
||||
static_cast<uint32_t>(fragments.size()));
|
||||
// Offsets must tile the wire body exactly: no hole, no overlap.
|
||||
CHECK_EQ(found->byte_offset(), static_cast<uint64_t>(body.size()));
|
||||
body.append(found->payload());
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
void CheckFingerprint(const sp::Fingerprint &fingerprint) {
|
||||
CHECK_EQ(fingerprint.model_artifact_digest(), std::string("sha256:1f0c9d2e"));
|
||||
CHECK_EQ(fingerprint.runtime_recipe_digest(), std::string("sha256:ab77e410"));
|
||||
CHECK_EQ(fingerprint.recipe_id(), std::string("llama-gguf-q4km-rocm"));
|
||||
CHECK_EQ(fingerprint.recipe_version(), std::string("3"));
|
||||
CHECK_EQ(fingerprint.catalogue_version(), std::string("2026.07.1"));
|
||||
}
|
||||
|
||||
// The canonical session request, as produced by Python.
|
||||
void TestSessionRequestVector(const std::string &bytes) {
|
||||
sp::SessionRequest request;
|
||||
CHECK(request.ParseFromString(bytes));
|
||||
CHECK(request.kind_case() == sp::SessionRequest::kChunk);
|
||||
|
||||
const sp::ActivationChunk &chunk = request.chunk();
|
||||
const sp::Envelope &envelope = chunk.envelope();
|
||||
|
||||
// Every field the protocol promises to carry (acceptance criterion 4).
|
||||
CHECK_EQ(envelope.schema_version(), sp::SCHEMA_VERSION_1);
|
||||
CHECK_EQ(envelope.work_id(), std::string("work-7f3a"));
|
||||
CHECK_EQ(envelope.route_session_id(), std::string("rs-2b91"));
|
||||
CHECK_EQ(envelope.route_epoch(), 7u);
|
||||
CheckFingerprint(envelope.fingerprint());
|
||||
CHECK_EQ(envelope.shard_range().start_layer(), 12u);
|
||||
CHECK_EQ(envelope.shard_range().end_layer(), 24u);
|
||||
// Overlap-safe effective start (ADR-0012): a worker that begins at
|
||||
// start_layer instead would re-apply layers 12..15.
|
||||
CHECK_EQ(envelope.shard_range().effective_start_layer(), 16u);
|
||||
CHECK_EQ(envelope.phase(), sp::PHASE_PREFILL);
|
||||
CHECK_EQ(envelope.position().first_position(), 256u);
|
||||
CHECK_EQ(envelope.position().token_count(), 128u);
|
||||
CHECK_EQ(envelope.idempotency_step(), 42u);
|
||||
CHECK_EQ(envelope.cache_expectation().mode(), sp::CACHE_MODE_PREFILL);
|
||||
CHECK_EQ(envelope.cache_expectation().expected_past_len(), 256u);
|
||||
CHECK_EQ(envelope.deadline_unix_nanos(), 1800000000000000000LL);
|
||||
CHECK_EQ(envelope.chunk().chunk_index(), 1u);
|
||||
CHECK_EQ(envelope.chunk().chunk_count(), 3u);
|
||||
CHECK_EQ(envelope.chunk().final_chunk(), false);
|
||||
|
||||
// The versioned named-tensor bundle (acceptance criterion 5).
|
||||
const sp::TensorBundle &bundle = chunk.bundle();
|
||||
CHECK_EQ(bundle.bundle_version(), 1u);
|
||||
CHECK_EQ(bundle.tensors_size(), 1);
|
||||
|
||||
const sp::NamedTensor &tensor = bundle.tensors(0);
|
||||
CHECK_EQ(tensor.name(), std::string("hidden_states"));
|
||||
CHECK_EQ(tensor.shape_size(), 3);
|
||||
CHECK_EQ(tensor.shape(0), 1);
|
||||
CHECK_EQ(tensor.shape(1), 128);
|
||||
CHECK_EQ(tensor.shape(2), 8);
|
||||
CHECK_EQ(tensor.dtype(), sp::DTYPE_BFLOAT16);
|
||||
CHECK_EQ(tensor.byte_order(), sp::BYTE_ORDER_LITTLE_ENDIAN);
|
||||
CHECK_EQ(tensor.total_bytes(), 1u * 128u * 8u * 2u);
|
||||
CHECK_EQ(tensor.compression(), sp::COMPRESSION_NONE);
|
||||
|
||||
// Multi-fragment on purpose: reassembly is what a worker actually does.
|
||||
CHECK(tensor.fragments_size() > 1);
|
||||
const std::string payload = ReassembleUncompressed(tensor);
|
||||
CHECK_EQ(payload.size(), static_cast<size_t>(tensor.total_bytes()));
|
||||
|
||||
// The payload Python generated, recomputed here from its rule.
|
||||
std::string expected;
|
||||
expected.reserve(payload.size());
|
||||
for (size_t i = 0; i < payload.size(); ++i) {
|
||||
expected.push_back(static_cast<char>((i * 7 + 11) % 256));
|
||||
}
|
||||
CHECK(payload == expected);
|
||||
|
||||
// Checksum: computed by Python, verified by an independent C++ CRC32C.
|
||||
CHECK_EQ(tensor.checksum().algorithm(), sp::CHECKSUM_ALGORITHM_CRC32C);
|
||||
const std::string &checksum = tensor.checksum().value();
|
||||
CHECK_EQ(checksum.size(), 4u);
|
||||
if (checksum.size() == 4) {
|
||||
const uint32_t actual = Crc32c(payload);
|
||||
const uint32_t declared =
|
||||
(static_cast<uint32_t>(static_cast<unsigned char>(checksum[0])) << 24) |
|
||||
(static_cast<uint32_t>(static_cast<unsigned char>(checksum[1])) << 16) |
|
||||
(static_cast<uint32_t>(static_cast<unsigned char>(checksum[2])) << 8) |
|
||||
static_cast<uint32_t>(static_cast<unsigned char>(checksum[3]));
|
||||
CHECK_EQ(actual, declared);
|
||||
}
|
||||
}
|
||||
|
||||
void TestCapabilityReportVector(const std::string &bytes) {
|
||||
sp::CapabilityReport report;
|
||||
CHECK(report.ParseFromString(bytes));
|
||||
CHECK_EQ(report.schema_version(), sp::SCHEMA_VERSION_1);
|
||||
CheckFingerprint(report.fingerprint());
|
||||
CHECK_EQ(report.backend(), std::string("rocm"));
|
||||
CHECK_EQ(report.device(), std::string("gfx1151"));
|
||||
CHECK_EQ(report.validated(), true);
|
||||
CHECK_EQ(report.max_concurrent_sessions(), 4u);
|
||||
CHECK_EQ(report.max_context_tokens(), 8192u);
|
||||
CHECK_EQ(report.flow_control().max_prefill_chunk_tokens(), 128u);
|
||||
CHECK_EQ(report.flow_control().max_chunk_bytes(), 4u * 1024u * 1024u);
|
||||
CHECK_EQ(report.accepted_compression_size(), 2);
|
||||
CHECK_EQ(report.supported_schema_versions_size(), 1);
|
||||
}
|
||||
|
||||
// Forward compatibility: a field this build has never heard of must survive a
|
||||
// parse/serialize cycle untouched. Without this, an old Shard silently strips
|
||||
// fields a newer peer depends on as it forwards activations down the route.
|
||||
void TestUnknownFieldsArePreserved(const std::string &bytes) {
|
||||
// Field 9999, wire type 0 (varint), value 12345 — not in this schema.
|
||||
std::string forward = bytes;
|
||||
forward.push_back(static_cast<char>(0xB8)); // tag: (9999 << 3) | 0
|
||||
forward.push_back(static_cast<char>(0xE0));
|
||||
forward.push_back(static_cast<char>(0x04));
|
||||
forward.push_back(static_cast<char>(0xB9)); // value: 12345
|
||||
forward.push_back(static_cast<char>(0x60));
|
||||
|
||||
sp::SessionRequest request;
|
||||
CHECK(request.ParseFromString(forward));
|
||||
// Known fields still parse.
|
||||
CHECK_EQ(request.chunk().envelope().work_id(), std::string("work-7f3a"));
|
||||
// And the unknown field is retained rather than dropped.
|
||||
CHECK(!request.GetReflection()->GetUnknownFields(request).empty());
|
||||
|
||||
std::string reserialized;
|
||||
CHECK(request.SerializeToString(&reserialized));
|
||||
CHECK_EQ(reserialized.size(), forward.size());
|
||||
|
||||
sp::SessionRequest reparsed;
|
||||
CHECK(reparsed.ParseFromString(reserialized));
|
||||
CHECK(!reparsed.GetReflection()->GetUnknownFields(reparsed).empty());
|
||||
}
|
||||
|
||||
// Backward compatibility: a message from a peer that sets none of the optional
|
||||
// groups must still parse, yielding proto3 defaults rather than an error.
|
||||
void TestSparseMessageParses() {
|
||||
sp::SessionRequest minimal;
|
||||
minimal.mutable_chunk()->mutable_envelope()->set_work_id("w");
|
||||
std::string bytes;
|
||||
CHECK(minimal.SerializeToString(&bytes));
|
||||
|
||||
sp::SessionRequest parsed;
|
||||
CHECK(parsed.ParseFromString(bytes));
|
||||
CHECK_EQ(parsed.chunk().envelope().work_id(), std::string("w"));
|
||||
CHECK_EQ(parsed.chunk().envelope().route_epoch(), 0u);
|
||||
CHECK_EQ(parsed.chunk().envelope().phase(), sp::PHASE_UNSPECIFIED);
|
||||
CHECK_EQ(parsed.chunk().bundle().tensors_size(), 0);
|
||||
}
|
||||
|
||||
// The decode fast path must stay small: repeating the full envelope per token
|
||||
// is pure overhead on the hottest path in the system.
|
||||
void TestDecodeFastPathIsSmall() {
|
||||
sp::SessionRequest decode;
|
||||
sp::DecodeStep *step = decode.mutable_decode();
|
||||
step->set_idempotency_step(9);
|
||||
step->set_position(1024);
|
||||
step->set_expected_past_len(1024);
|
||||
step->set_work_id("work-7f3a");
|
||||
sp::NamedTensor *tensor = step->mutable_tensor();
|
||||
tensor->set_name("hidden_states");
|
||||
tensor->add_shape(1);
|
||||
tensor->add_shape(1);
|
||||
tensor->add_shape(8);
|
||||
tensor->set_dtype(sp::DTYPE_BFLOAT16);
|
||||
tensor->set_byte_order(sp::BYTE_ORDER_LITTLE_ENDIAN);
|
||||
tensor->set_total_bytes(16);
|
||||
tensor->set_compression(sp::COMPRESSION_NONE);
|
||||
sp::TensorFragment *fragment = tensor->add_fragments();
|
||||
fragment->set_fragment_index(0);
|
||||
fragment->set_fragment_count(1);
|
||||
fragment->set_byte_offset(0);
|
||||
fragment->set_payload(std::string(16, '\x01'));
|
||||
|
||||
std::string bytes;
|
||||
CHECK(decode.SerializeToString(&bytes));
|
||||
// Payload is 16 bytes; the framing around it must stay well under the
|
||||
// envelope-carrying prefill path.
|
||||
CHECK(bytes.size() < 96);
|
||||
|
||||
sp::SessionRequest parsed;
|
||||
CHECK(parsed.ParseFromString(bytes));
|
||||
CHECK(parsed.kind_case() == sp::SessionRequest::kDecode);
|
||||
CHECK_EQ(parsed.decode().position(), 1024u);
|
||||
CHECK_EQ(parsed.decode().tensor().total_bytes(), 16u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
||||
if (argc < 3) {
|
||||
std::cerr << "usage: " << argv[0] << " <testdata-dir> <output-dir>\n";
|
||||
return 2;
|
||||
}
|
||||
const std::filesystem::path testdata(argv[1]);
|
||||
const std::filesystem::path output(argv[2]);
|
||||
|
||||
const std::string session_bytes =
|
||||
ReadFile(testdata / "session_request_golden.binpb");
|
||||
const std::string capability_bytes =
|
||||
ReadFile(testdata / "capability_report_golden.binpb");
|
||||
|
||||
TestSessionRequestVector(session_bytes);
|
||||
TestCapabilityReportVector(capability_bytes);
|
||||
TestUnknownFieldsArePreserved(session_bytes);
|
||||
TestSparseMessageParses();
|
||||
TestDecodeFastPathIsSmall();
|
||||
|
||||
// Re-serialize the canonical message from the C++ object model and hand it
|
||||
// back for Python to compare against the golden bytes. If the two languages
|
||||
// disagreed about any field's encoding, this file would differ.
|
||||
sp::SessionRequest request;
|
||||
if (request.ParseFromString(session_bytes)) {
|
||||
std::string reserialized;
|
||||
if (request.SerializeToString(&reserialized)) {
|
||||
std::ofstream out(output / "cpp_roundtrip.binpb", std::ios::binary);
|
||||
out.write(reserialized.data(),
|
||||
static_cast<std::streamsize>(reserialized.size()));
|
||||
}
|
||||
}
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " check(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "all C++ conformance checks passed\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user