Closed
Description
File: morphling/entrypoint/run_device.py:98
Current FIXME text: FIXME(#45): server-side trust model for device-reported FLOPS/bandwidth/latency.
The device currently self-reports its FLOPS, bandwidth, and latency to the server. The contract should be:
- Latency and bandwidth measured by the server, not the device.
- Server sends random GEMM tasks to measure FLOPS; result verification needed.
This is a known design gap surfaced during the open-source readiness audit. Not in scope for the EdgeSys '26 release pass; tracked here so the marker has a stable reference.
See also commit chore(release): purge paper figures, plot scripts, and morphling.evaluation for the broader audit context.
Metadata
Metadata
Assignees
Labels
No labels
Activity
chore(code): link 6 high-visibility TODOs to tracking issues
drunkcoding commentedon May 30, 2026
Issue #45 — Device measurement trust model
Problem (verified against current code)
The device runner self-reports
flops,memory,ul_bw,dl_bw,ul_lat,dl_latvia CLI flags inmorphling/entrypoint/run_device.py:80-108. These values are stuffed intoMORPHLING_*environment variables, read back by the proxy client incsrc/backend/proxy_cli.cpp:559-565as aDeviceProfileDatapayload, and forwarded to the server which registers them verbatim inDEVICE_TRACKER.RegisterDevice(csrc/backend/proxy_svr.cpp:188-192).The FIXME at
run_device.py:98flags this: a malicious or misconfigured device can lie about its capabilities, and the server takes the numbers on faith.Scope decision
This is a design-gap issue, not a release blocker. We will land the measurement infrastructure, but the trust-model policy (replace vs. cross-check vs. clamp) is left as a follow-up because it requires paper-level decisions (does Morphling's stride-aware model assume honest input?).
This plan implements the measurement infrastructure only.
Out of scope (explicit non-goals)
DEVICE_TRACKER(we record both; reconciliation policy is a separate decision).Design
Three independent measurement paths, each gated by a server-side config flag (default off, so existing behavior is unchanged):
M1. Server-measured network latency (
ul_lat,dl_lat)DeviceProfileDataregistration, server sends an N-byte ping (N=64) and records RTT.min_interval_ms=10ms, take median, split intoul_lat = dl_lat = median/2(we cannot distinguish directions without device cooperation; record that limitation in the result).DeviceProfileData.measured_lat_ns(uint64, 0 = not measured).M2. Server-measured bandwidth (
ul_bw,dl_bw)measured_dl_bw_bps(server→device, i.e. device downlink).measured_ul_bw_bps.proxy_cli.cppis a new opcode; reuse the same socket; no new transport.measured_ul_bw_bps,measured_dl_bw_bps(uint64, 0 = not measured).M3. Server-measured FLOPS via challenge GEMM
(m, n, k)from a small fixed set (e.g.{(1024,1024,1024), (2048,2048,2048)}) plus a uniformly random(m, n, k)from[256..2048]^3withseed=device_uuid, ships aMatMulRequestMessagevia the existing path, and times the round-trip minus measured 2 ×dl_lat.seed=device_uuidso it can recompute the expected C and check element-wise tolerance (atol=1e-3,rtol=1e-3for float32). Mismatch ⇒measured_flops_bps = 0and a warning is logged.2 * m * n * k / elapsed_seconds. Reported in same units asprofile.flops.measured_flops,measured_flops_verified(bool).M4. Wire format
Correction after first review pass:
DeviceProfileDataIS protobuf-backed. Percsrc/backend/server_base.h:214-219,Serialize/Deserializeonly acceptSerializationFormat::PROTOBUF; the underlying message lives inproto/global_api.proto:39-47:This is a
proto/*.protochange, whichCLAUDE.md§5 flags as requiring explicit user approval before edit. Options:Option A (chosen, contingent on user approval): append new
optionalfields (measured_flops = 8,measured_flops_verified = 9,measured_lat_ns = 10,measured_ul_bw_bps = 11,measured_dl_bw_bps = 12). Alloptional= backward-compatible: older clients ignore unknown fields; newer servers seehas_measured_flops() == falsefor old payloads. No tag number reuse. UpdateSerializeProto/DeserializeProtoincsrc/backend/matrix_partition.cpp:719-791to gate the new fields withhas_*checks. Before starting implementation, ask the user explicitly: "OK to extendproto/global_api.protoper #45 plan §M4 Option A?"Option B (fallback if proto change is rejected): store the server-measured numbers exclusively server-side. Specifically: extend
DeviceTrackerwith a parallelMeasuredProfilemap keyed bydevice_id, populated byDeviceMeasurementServiceafter registration. Logs include both; no wire-format change. Schedulers that want to use measured values consult the map. Downside: device cannot learn its own measured numbers without an out-of-band channel.Option C (also fallback): introduce a new top-level proto message
DeviceMeasurementReportand a newMessageTypefor server-to-device delivery. Higher cost; not justified by current consumers; skip unless A and B are both rejected.M5. Config knobs
New section in
config/proxy/server.yaml(introduce file if absent; otherwise extend existing):Loaded in
ProxySvrHandlector; passed to the newDeviceMeasurementServicecollaborator.Atomic implementation steps
Each step lists: edit, tool to verify, command, expected output.
Step 0 — User-approval gate (BLOCKING)
proto/global_api.protoper §M4 Option A?"Step 1 — Proto + serializer extension (Option A only)
proto/global_api.proto:39-47— append 5 newoptionalfields at tags 8-12.csrc/backend/server_base.h:204-226— add 5 new fields onDeviceProfileDatastruct.csrc/backend/matrix_partition.cpp:691-791— extendSerializeProto,DeserializeProto,DebugStringwithhas_*guards for the new optional fields.lsp_diagnosticsoncsrc/backend/server_base.h,csrc/backend/matrix_partition.cpp→ no new errors.docker build -t device-emulator:latest .→ exits 0.tests/cpp/unit/backend/test_device_profile_roundtrip.cpp(new) using gtest + the existingDeviceProfileData::Serialize/Deserialize. Wire it intests/cpp/CMakeLists.txtfollowing thetest_dispatch_gatepattern at L231-247 (executable +add_test).docker run --rm device-emulator:latest /app/tests/cpp/build/test_device_profile_roundtrip→ "PASSED" line forLegacyFieldsOnlyandLegacyPlusMeasuredcases.Step 2 —
DeviceMeasurementService(server-side coordinator)csrc/backend/device_measurement.h+csrc/backend/device_measurement.cpp.MeasureLatency(conn, profile),MeasureBandwidth(conn, profile),MeasureFlops(conn, profile). Each no-op if its config flag is off.ProxySvrHandle— no new sockets.BACKEND_SRCglob inCMakeLists.txt:148(already a wildcardcsrc/backend/*.cpp, so the file picks up automatically; verify with the build).lsp_diagnosticson the two new files → clean (LSPcore/logger.hnot-found errors are pre-existing and unchanged).docker build -t device-emulator:latest .→ exits 0. The new symbols link into_Msg(server-side) via theBACKEND_SRCglob.docker run --rm device-emulator:latest bash -lc 'nm /app/morphling/_Msg.cpython-*.so | grep -E "DeviceMeasurementService" | head'→ at least 3 lines, one per public method. (VerifiedDockerfile:63 WORKDIR /appand_setup_helpers.py:93,210confirming extensions are dropped at/app/morphling/_Msg.cpython-*.so.) Defer behavior validation to step 4.Step 3 — Device-side probe handlers
csrc/backend/proxy_cli.cpp— register 3 new opcode handlersOP_PROBE_LAT,OP_PROBE_BW,OP_PROBE_GEMM(constants added incsrc/backend/proto_opcodes.hor wherever opcodes live; verify during impl).lsp_diagnosticsclean on the touched files.tests/cpp/integration/test_device_measurement_loopback.cpp(new), wire intotests/cpp/CMakeLists.txtfollowingtest_barrier_integrationpattern at L249-264. Test runs server + client in the same process, drives a full M1+M2+M3 sequence, asserts measured values are within +/-30% of synthetic ground truth.docker run --rm --gpus all device-emulator:latest /app/tests/cpp/build/test_device_measurement_loopback→ PASSED.Step 4 — Server-side wiring
csrc/backend/proxy_svr.cpp:188-192— afterDEVICE_TRACKER.RegisterDevice(...), ifdevice_measurement.enable_*flags are on, callDeviceMeasurementServicein sequence M1→M2→M3 (each gated by its own flag). For Option A, write measured fields back into the protobuf-backed struct viaDEVICE_TRACKER.UpdateMeasuredProfile(device_id, measured)(new method). For Option B, store in a parallelstd::unordered_map<int64_t, MeasuredProfile>inDeviceTracker.scripts/run_devices.py(note:.py, not.sh). Server log MUST contain a line of the formDevice <uuid> reported flops=X, measured=Yfor each registered device. Capture log excerpt for PR.Step 5 — Config knob loading
docker run --rm device-emulator:latest bash -lc 'grep -rn "yaml\|YAML\|env_or" morphling/common/ csrc/backend/ csrc/core/ 2>/dev/null | head -20'. Read the output to decide between:morphling/common/config.py, orenv_or(...)lambda already used inproxy_cli.cpp:553-557for env-var defaults.MORPHLING_MEASURE_LAT,MORPHLING_MEASURE_BW,MORPHLING_MEASURE_FLOPS, each defaulting tofalse/0. Wire them intoDeviceMeasurementServiceflags.lsp_diagnosticsclean on every file touched in this step.tests/python/unit/common/test_measurement_flags.py(new) with one test reading the flags via the same loader path and asserting defaults are off.docker run --rm --gpus all device-emulator:latest python3 -m pytest tests/python/unit/common/test_measurement_flags.py -v→ 1 passed.Step 6 — Source-level marker
# FIXME(#45)atrun_device.py:98with a docstring-style block describing the trust model: device-reported values are advisory; server-side measurement (when enabled) takes precedence. No code logic change.git grep "FIXME(#45)"returns empty.Step 7 — Docs
docs/deployment.mdwith a "Server-measured device profile" subsection: enumerate the 3 enable flags, what each probes, the accuracy expectations, and the reconciliation policy decision deferred to the follow-up issue.Dockerfile:67only installsrequirements.txt(notrequirements-lint.txt), sopre-commitis NOT in the image. Run the gate on the host instead:pre-commit run codespell --files docs/deployment.md(executed by the developer or CI in the lint job per the existing.pre-commit-config.yaml; commite6cfc89"run pre-commit auto-fixes and exclude generated docs from codespell" confirms this is the gate). Expected output: exits 0.grep -l "deployment" docs/web/*.html 2>/dev/null— if any file matches, mirror the new subsection; otherwise note in the PR description that there is no HTML mirror to update.Step 8 — Final E2E
docker build -t device-emulator:latest . && docker run --rm --gpus all device-emulator:latest python3 -m pytest tests -v.docker run --rm --gpus all device-emulator:latest python3 scripts/run_devices.py --measurement on --duration 60s(flag added during step 5; verify name during impl).Device <uuid>: reported flops=<X>, measured=<Y>, verified=<bool>line for each of the N devices launched.Final verification wave (after all steps)
docker build -t device-emulator:latest .→ exits 0.test_device_profile_roundtrip,test_device_measurement_loopback) are listed intests/cpp/CMakeLists.txtand pass when run via/app/tests/run_cpp_tests.shinside the container.docker run --rm --gpus all device-emulator:latest python3 -m pytest tests -vshows no new failures vs. baseline.docker run --rm --gpus all device-emulator:latest python3 scripts/run_devices.py --measurement on(or equivalent flag from step 5); capture server logs showing reported vs. measured for at least 2 devices. Cite log excerpts in PR description.DeviceProfileData::DebugString()line for each device whose content is identical to pre-PR for the 7 legacy fields.Risk & rollback
(device_uuid, seed)-keyed pattern and verifies the echo; mismatch ⇒ probe result discarded, warning logged.Follow-up issues (not in this plan)
flopsreported to schedulers bemin(reported, measured),measured, orweighted? Open a separate issue once measurement data exists.drunkcoding commentedon May 30, 2026
Progress update
Steps 1 and 2 of the approved plan landed:
Step 1 — wire format: commit 0e4f087. Added 5
optionalfields toproto.DeviceProfileData(tags 8-12:measured_flops,measured_flops_verified,measured_lat_ns,measured_ul_bw_bps,measured_dl_bw_bps). Extended the C++ struct +SerializeProto/DeserializeProto/DebugString. Locked in bytest_device_profile_roundtrip(3 cases includingLegacyFieldsOnlyfor backward compat).Step 2 — service skeleton: commit 05e5f80. Added
DeviceMeasurementServicewithMORPHLING_MEASURE_{LAT,BW,FLOPS}env-var gates (all default off).MeasureLatency/MeasureBandwidth/MeasureFlopsare no-op returns until the probe wire protocol lands. Verified bytest_device_measurement_service(3 cases).Steps 3-8 (probe opcodes, server wiring, integration test, device-side execution, docs, E2E demo) tracked in #55 to keep this issue focused. Will leave #45 open until #55 lands so the FIXME-to-resolution chain stays linkable.
drunkcoding commentedon May 31, 2026
Closed alongside #55. The trust model gap originally documented here is now addressed by the server-measured device profile pipeline:
Full chain and E2E result tracked on #55. The seven device-reported legacy fields are preserved untouched; the five new measured_* fields are populated by the server when the operator enables probes. Reconciliation policy (whether schedulers should consume
reported,measured, or a blend) is deliberately out of scope and will be filed as a follow-up once enough measured data has accumulated to inform that decision.feat(proto): add server-measured fields to DeviceProfileData (#45 ste…
feat(backend): add DeviceMeasurementService skeleton (#45 step 2)
feat(proto): add probe wire messages for device measurement (#55 step…
docs(device): replace FIXME(#45) with trust-model NOTE block (#55 ste…
feat(backend): add ProbeMessageCodec + device-side probe handlers (#55 …
chore(code): link 6 high-visibility TODOs to tracking issues
feat(proto): add server-measured fields to DeviceProfileData (#45 ste…
feat(backend): add DeviceMeasurementService skeleton (#45 step 2)
feat(proto): add probe wire messages for device measurement (#55 step…
docs(device): replace FIXME(#45) with trust-model NOTE block (#55 ste…
feat(backend): add ProbeMessageCodec + device-side probe handlers (#55 …