[Feature]Report FD statistical information - #5646
Conversation
|
Thanks for your contribution! |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #5646 +/- ##
==========================================
Coverage ? 67.29%
==========================================
Files ? 349
Lines ? 45017
Branches ? 6934
==========================================
Hits ? 30294
Misses ? 12485
Partials ? 2238
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a telemetry/usage statistics collection feature for FastDeploy. The system automatically reports platform, hardware, and configuration information to a remote server when the service launches, with the ability to opt-out via the DO_NOT_TRACK=1 environment variable.
Key Changes:
- Implements comprehensive usage statistics collection including CPU, GPU, environment, and configuration data
- Adds automatic reporting mechanism that triggers on worker initialization and continues every 10 minutes
- Integrates reporting into all worker types (GPU, XPU, HPU, GCU)
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 27 comments.
Show a summary per file
| File | Description |
|---|---|
| fastdeploy/usage/usage_lib.py | Core implementation of usage statistics collection, serialization, and reporting |
| tests/usage/test_usage_lib.py | Unit tests for usage statistics functionality |
| fastdeploy/usage/init.py | Package initialization file with copyright header |
| fastdeploy/worker/gpu_worker.py | Integrates usage reporting into GPU worker initialization |
| fastdeploy/worker/xpu_worker.py | Integrates usage reporting into XPU worker initialization |
| fastdeploy/worker/hpu_worker.py | Integrates usage reporting into HPU worker initialization |
| fastdeploy/worker/gcu_worker.py | Integrates usage reporting into GCU worker initialization |
| fastdeploy/envs.py | Adds environment variables for controlling usage statistics (DO_NOT_TRACK, server URL, source, config path) |
| fastdeploy/platforms/base.py | Adds is_cuda_alike() helper method to detect CUDA or ROCm platforms |
| requirements.txt | Adds py-cpuinfo dependency for CPU information collection |
| requirements_dcu.txt | Adds py-cpuinfo dependency |
| requirements_iluvatar.txt | Adds py-cpuinfo dependency |
| requirements_metaxgpu.txt | Adds py-cpuinfo dependency |
| except: | ||
| return str(obj) |
There was a problem hiding this comment.
Bare except clause used without specifying exception type. This catches all exceptions including SystemExit and KeyboardInterrupt, which could hide serious bugs. Specify the expected exception type or at minimum use "except Exception:".
|
|
||
| def _send_to_server(self, data: dict[str, Any]) -> None: | ||
| try: | ||
| requests.post(url=_USAGE_STATS_SERVER, json=data, timeout=10) |
There was a problem hiding this comment.
The HTTP POST request has a 10-second timeout, which could block the reporting thread during network issues. While exceptions are caught, this could still cause delays in the continuous reporting loop. Consider reducing the timeout or making it configurable.
| return tuple([None] * len(names)) | ||
|
|
||
| # Run in subprocess to avoid initializing CUDA as a side effect. | ||
| mp_ctx = multiprocessing.get_context("fork") |
There was a problem hiding this comment.
The function uses multiprocessing with "fork" context which can be problematic on some platforms (especially macOS). Consider using "spawn" context or making the context configurable, especially since this is for a one-time property query.
| mp_ctx = multiprocessing.get_context("fork") | |
| # Prefer "spawn" for better portability (e.g., macOS); fall back to default if unavailable. | |
| try: | |
| mp_ctx = multiprocessing.get_context("spawn") | |
| except ValueError: | |
| mp_ctx = multiprocessing.get_context() |
| # Try detecting through vendor file | ||
| vendor_files = [ | ||
| "/sys/class/dmi/id/product_version", | ||
| "/sys/class/dmi/id/bios_vendor", | ||
| "/sys/class/dmi/id/product_name", | ||
| "/sys/class/dmi/id/chassis_asset_tag", | ||
| "/sys/class/dmi/id/sys_vendor", | ||
| ] | ||
| # Mapping of identifiable strings to cloud providers | ||
| cloud_identifiers = { | ||
| "amazon": "AWS", | ||
| "microsoft corporation": "AZURE", | ||
| "google": "GCP", | ||
| "oraclecloud": "OCI", | ||
| } | ||
|
|
||
| for vendor_file in vendor_files: | ||
| path = Path(vendor_file) | ||
| if path.is_file(): | ||
| file_content = path.read_text().lower() |
There was a problem hiding this comment.
The file path /sys/class/dmi/id/ files are Linux-specific and will fail on other operating systems. Consider wrapping this detection logic in a try-except block or checking the OS first to avoid errors on non-Linux systems.
| # Try detecting through vendor file | |
| vendor_files = [ | |
| "/sys/class/dmi/id/product_version", | |
| "/sys/class/dmi/id/bios_vendor", | |
| "/sys/class/dmi/id/product_name", | |
| "/sys/class/dmi/id/chassis_asset_tag", | |
| "/sys/class/dmi/id/sys_vendor", | |
| ] | |
| # Mapping of identifiable strings to cloud providers | |
| cloud_identifiers = { | |
| "amazon": "AWS", | |
| "microsoft corporation": "AZURE", | |
| "google": "GCP", | |
| "oraclecloud": "OCI", | |
| } | |
| for vendor_file in vendor_files: | |
| path = Path(vendor_file) | |
| if path.is_file(): | |
| file_content = path.read_text().lower() | |
| # Try detecting through vendor file (Linux-specific paths) | |
| if platform.system().lower() == "linux": | |
| vendor_files = [ | |
| "/sys/class/dmi/id/product_version", | |
| "/sys/class/dmi/id/bios_vendor", | |
| "/sys/class/dmi/id/product_name", | |
| "/sys/class/dmi/id/chassis_asset_tag", | |
| "/sys/class/dmi/id/sys_vendor", | |
| ] | |
| # Mapping of identifiable strings to cloud providers | |
| cloud_identifiers = { | |
| "amazon": "AWS", | |
| "microsoft corporation": "AZURE", | |
| "google": "GCP", | |
| "oraclecloud": "OCI", | |
| } | |
| for vendor_file in vendor_files: | |
| path = Path(vendor_file) | |
| try: | |
| if path.is_file(): | |
| file_content = path.read_text().lower() | |
| else: | |
| continue | |
| except (OSError, IOError): | |
| # Ignore unreadable or missing vendor files | |
| continue |
|
|
||
| def get_xpu_model(): | ||
| try: | ||
| result = subprocess.run(["xpu-smi"], capture_output=True, text=True) |
There was a problem hiding this comment.
The subprocess.run call for xpu-smi doesn't specify a timeout parameter. If the xpu-smi command hangs, it could block the usage reporting indefinitely. Add a timeout parameter (e.g., timeout=5) to prevent hanging.
| result = subprocess.run(["xpu-smi"], capture_output=True, text=True) | |
| result = subprocess.run(["xpu-smi"], capture_output=True, text=True, timeout=5) |
| data = {"test": "data"} | ||
| # Should not raise exception, just log debug message | ||
| self.usage_message._send_to_server(data) | ||
|
|
There was a problem hiding this comment.
Missing test coverage for the complete flow of _report_usage_once() which collects CPU info, environment variables, and config serialization. These are critical paths that should be tested to ensure data is collected and formatted correctly.
| return "Unknown" | ||
|
|
||
|
|
||
| def simple_convert(obj): |
There was a problem hiding this comment.
Missing docstring for the simple_convert() function. This is a complex function that handles various object types and conversion strategies. Add a comprehensive docstring explaining:
- What the function does
- Parameter types and meanings
- Return type and format
- Examples of conversions
| def simple_convert(obj): | |
| def simple_convert(obj): | |
| """ | |
| Recursively convert an arbitrary Python object into a JSON-serializable | |
| structure (composed of primitives, lists and dictionaries) or a string. | |
| The conversion strategy is: | |
| 1. Primitive values (`None`, `str`, `int`, `float`, `bool`) are returned | |
| as-is. | |
| 2. Mappings (`dict`) are converted by recursively converting each value. | |
| 3. Iterables (`list`, `tuple`, `set`) are converted into lists with each | |
| element recursively converted. | |
| 4. If the object is (or becomes) a string that looks like JSON, an | |
| attempt is made to parse it with :func:`json.loads`. On failure, the | |
| original string is returned. | |
| 5. For custom objects that expose one of ``to_dict()``, ``to_json()``, | |
| ``__getstate__()`` or ``as_dict()``, the corresponding method is | |
| called and its result is processed recursively if it is a ``dict`` or | |
| parsed as JSON if it is a ``str``. | |
| 6. If no conversion method is available, the object's ``__dict__`` (via | |
| :func:`vars`) is converted recursively, excluding private attributes | |
| whose names start with an underscore. | |
| 7. As a last resort, :func:`str` is used to obtain a string representation. | |
| This helper is designed to prepare data for logging, telemetry and other | |
| serialization scenarios where complex objects need to be reduced to basic | |
| JSON-friendly structures. | |
| Parameters | |
| ---------- | |
| obj : Any | |
| The object to convert. This can be a primitive value, container | |
| (``dict``, ``list``, ``tuple``, ``set``), string (including JSON | |
| strings), or an arbitrary user-defined object. | |
| Returns | |
| ------- | |
| Any | |
| A value that is typically JSON-serializable, composed of nested | |
| primitives, lists and dictionaries. If an object cannot be reduced to | |
| such a structure, its string representation is returned instead. | |
| Examples | |
| -------- | |
| Basic types are returned unchanged:: | |
| simple_convert(1) # -> 1 | |
| simple_convert("text") # -> "text" | |
| simple_convert(True) # -> True | |
| Containers are converted recursively:: | |
| simple_convert({"a": 1, "b": [2, 3]}) | |
| # -> {"a": 1, "b": [2, 3]} | |
| JSON strings are parsed when possible:: | |
| simple_convert('{"x": 10}') | |
| # -> {"x": 10} | |
| User-defined objects with ``to_dict`` are expanded:: | |
| class Config: | |
| def __init__(self): | |
| self.a = 1 | |
| def to_dict(self): | |
| return {"a": self.a} | |
| simple_convert(Config()) | |
| # -> {"a": 1} | |
| """ |
| einops | ||
| setproctitle | ||
| aistudio_sdk | ||
| py-cpuinfo |
There was a problem hiding this comment.
The new dependency py-cpuinfo is added without a fixed version, so deployments will always pull the latest release from PyPI, which increases exposure to supply-chain attacks. If the package or its distribution channel is compromised in the future, new deployments could automatically execute malicious code with access to your runtime environment and secrets. Pin this dependency to a vetted version (or vendor it) and update it explicitly after review instead of relying on a floating latest version.
| partial_json_parser | ||
| msgspec | ||
| safetensors>=0.7.0 | ||
| py-cpuinfo |
There was a problem hiding this comment.
The new dependency py-cpuinfo is added without a fixed version, so deployments will always pull the latest release from PyPI, which increases exposure to supply-chain attacks. If the package or its distribution channel is compromised in the future, new deployments could automatically execute malicious code with access to your runtime environment and secrets. Pin this dependency to a vetted version (or vendor it) and update it explicitly after review instead of relying on a floating latest version.
| opentelemetry-instrumentation-fastapi | ||
| opentelemetry-instrumentation-logging>=0.57b0 | ||
| partial_json_parser | ||
| py-cpuinfo |
There was a problem hiding this comment.
The new dependency py-cpuinfo is added without a fixed version, so deployments will always pull the latest release from PyPI, which increases exposure to supply-chain attacks. If the package or its distribution channel is compromised in the future, new deployments could automatically execute malicious code with access to your runtime environment and secrets. Pin this dependency to a vetted version (or vendor it) and update it explicitly after review instead of relying on a floating latest version.
* add usage commit * update envs and xpu * add requirements * fix quantization value * add unit test * add unit test * fix unit test * add unit test * add unit test * add unit test * add unit test * add unit test * add unit test * fix FD_USAGE_STATS_SERVER * fix * fix * add doc * add doc * add doc * add doc * add doc * fix file name
* add usage commit * update envs and xpu * add requirements * fix quantization value * add unit test * add unit test * fix unit test * add unit test * add unit test * add unit test * add unit test * add unit test * add unit test * fix FD_USAGE_STATS_SERVER * fix * fix * add doc * add doc * add doc * add doc * add doc * fix file name
* add usage commit * update envs and xpu * add requirements * fix quantization value * add unit test * add unit test * fix unit test * add unit test * add unit test * add unit test * add unit test * add unit test * add unit test * fix FD_USAGE_STATS_SERVER * fix * fix * add doc * add doc * add doc * add doc * add doc * fix file name
Motivation
Modifications
Add an information reporting function; it will automatically report when the service is launched. For specific details
Usage or Command
Automatic reporting is enabled by default. To disable this feature, set DO_NOT_TRACK="1".
Accuracy Tests
Checklist
[FDConfig],[APIServer],[Engine],[Scheduler],[PD Disaggregation],[Executor],[Graph Optimization],[Speculative Decoding],[RL],[Models],[Quantization],[Loader],[OP],[KVCache],[DataProcessor],[BugFix],[Docs],[CI],[Optimization],[Feature],[Benchmark],[Others],[XPU],[HPU],[GCU],[DCU],[Iluvatar],[Metax]]pre-commitbefore commit.releasebranch, make sure the PR has been submitted to thedevelopbranch, then cherry-pick it to thereleasebranch with the[Cherry-Pick]PR tag.