-
Notifications
You must be signed in to change notification settings - Fork 6
Migrate Edge Configuration to SDK #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
timmarkhuff
wants to merge
2
commits into
main
Choose a base branch
from
tim/edge-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| """Example of constructing an edge endpoint configuration programmatically.""" | ||
|
|
||
| from groundlight import Groundlight | ||
| from groundlight.edge import DEFAULT, EDGE_WITH_ESCALATION, NO_CLOUD, EdgeInferenceConfig, RootEdgeConfig | ||
|
|
||
| gl = Groundlight() | ||
| detector1 = gl.get_detector("det_2z41nK0CyoFdWF6tEoB7DN5qwAx") | ||
| detector2 = gl.get_detector("det_2z41rs0Fo12LAk0oOZg0r4wR9Fn") | ||
| detector3 = gl.get_detector("det_2tYVTZrz8VLZhe94tjuPRl5rDeG") | ||
| detector4 = gl.get_detector("det_2sDfBz5xp6ZysB82kK7LfNYYSXx") | ||
| detector5 = gl.get_detector("det_2sDfGUP8cBt9Wrq0YFVLjVZhoI5") | ||
|
|
||
| config = RootEdgeConfig() | ||
|
|
||
| config.add_detector(detector1, NO_CLOUD) | ||
| config.add_detector(detector2, EDGE_WITH_ESCALATION) | ||
| config.add_detector(detector3, DEFAULT) | ||
|
|
||
| # Custom configs work alongside presets | ||
| my_custom_config = EdgeInferenceConfig( | ||
| name="my_custom_config", | ||
| always_return_edge_prediction=True, | ||
| min_time_between_escalations=0.5, | ||
| ) | ||
| detector_id = detector4.id | ||
| config.add_detector(detector_id, my_custom_config) | ||
|
|
||
| # Cannot reuse names on EdgeInferenceConfig | ||
| config_with_name_collision = EdgeInferenceConfig(name='default') | ||
| try: | ||
| config.add_detector(detector5, config_with_name_collision) | ||
| except ValueError as e: | ||
| print(e) | ||
|
|
||
| # Frozen -- mutation raises an error | ||
| try: | ||
| NO_CLOUD.enabled = False | ||
| except Exception as e: | ||
| print(e) | ||
|
|
||
| print(config.model_dump_json(indent=2)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from .config import ( | ||
| DEFAULT, | ||
| DISABLED, | ||
| EDGE_WITH_ESCALATION, | ||
| NO_CLOUD, | ||
| DetectorConfig, | ||
| EdgeInferenceConfig, | ||
| GlobalConfig, | ||
| RootEdgeConfig, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit, this is a little redundant since it includes all objects that could be importet |
||
| "DEFAULT", | ||
| "DISABLED", | ||
| "EDGE_WITH_ESCALATION", | ||
| "NO_CLOUD", | ||
| "DetectorConfig", | ||
| "EdgeInferenceConfig", | ||
| "GlobalConfig", | ||
| "RootEdgeConfig", | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| from typing import Union | ||
|
|
||
| from model import Detector | ||
| from pydantic import BaseModel, ConfigDict, Field, model_validator | ||
| from typing_extensions import Self | ||
|
|
||
|
|
||
| class GlobalConfig(BaseModel): | ||
| refresh_rate: float = Field( | ||
| default=60.0, | ||
| description="The interval (in seconds) at which the inference server checks for a new model binary update.", | ||
| ) | ||
| confident_audit_rate: float = Field( | ||
| default=1e-5, # A detector running at 1 FPS = ~100,000 IQ/day, so 1e-5 is ~1 confident IQ/day audited | ||
| description="The probability that any given confident prediction will be sent to the cloud for auditing.", | ||
| ) | ||
|
|
||
|
|
||
| class EdgeInferenceConfig(BaseModel): | ||
| """ | ||
| Configuration for edge inference on a specific detector. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(frozen=True) | ||
|
|
||
| name: str = Field(..., exclude=True, description="A unique name for this inference config preset.") | ||
| enabled: bool = Field( # TODO investigate and update the functionality of this option | ||
| default=True, description="Whether the edge endpoint should accept image queries for this detector." | ||
| ) | ||
| api_token: str | None = Field( | ||
| default=None, description="API token used to fetch the inference model for this detector." | ||
| ) | ||
| always_return_edge_prediction: bool = Field( | ||
| default=False, | ||
| description=( | ||
| "Indicates if the edge-endpoint should always provide edge ML predictions, regardless of confidence. " | ||
| "When this setting is true, whether or not the edge-endpoint should escalate low-confidence predictions " | ||
| "to the cloud is determined by `disable_cloud_escalation`." | ||
| ), | ||
| ) | ||
| disable_cloud_escalation: bool = Field( | ||
| default=False, | ||
| description=( | ||
| "Never escalate ImageQueries from the edge-endpoint to the cloud. " | ||
| "Requires `always_return_edge_prediction=True`." | ||
| ), | ||
| ) | ||
| min_time_between_escalations: float = Field( | ||
| default=2.0, | ||
| description=( | ||
| "The minimum time (in seconds) to wait between cloud escalations for a given detector. " | ||
| "Cannot be less than 0.0. " | ||
| "Only applies when `always_return_edge_prediction=True` and `disable_cloud_escalation=False`." | ||
| ), | ||
| ) | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_configuration(self) -> Self: | ||
| if self.disable_cloud_escalation and not self.always_return_edge_prediction: | ||
| raise ValueError( | ||
| "The `disable_cloud_escalation` flag is only valid when `always_return_edge_prediction` is set to True." | ||
| ) | ||
| if self.min_time_between_escalations < 0.0: | ||
| raise ValueError("`min_time_between_escalations` cannot be less than 0.0.") | ||
| return self | ||
|
|
||
|
|
||
| class DetectorConfig(BaseModel): | ||
| """ | ||
| Configuration for a specific detector. | ||
| """ | ||
|
|
||
| detector_id: str = Field(..., description="Detector ID") | ||
| edge_inference_config: str = Field(..., description="Config for edge inference.") | ||
|
|
||
|
|
||
| class RootEdgeConfig(BaseModel): | ||
| """ | ||
| Root configuration for edge inference. | ||
| """ | ||
|
|
||
| global_config: GlobalConfig = Field(default_factory=GlobalConfig) | ||
| edge_inference_configs: dict[str, EdgeInferenceConfig] = Field(default_factory=dict) | ||
| detectors: list[DetectorConfig] = Field(default_factory=list) | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_inference_configs(self): | ||
| for detector_config in self.detectors: | ||
| if detector_config.edge_inference_config not in self.edge_inference_configs: | ||
| raise ValueError(f"Edge inference config '{detector_config.edge_inference_config}' not defined.") | ||
| return self | ||
|
|
||
| def add_detector( | ||
| self, detector: Union[str, Detector], edge_inference_config: Union[str, EdgeInferenceConfig] | ||
| ) -> None: | ||
| detector_id = detector.id if isinstance(detector, Detector) else detector | ||
| if any(d.detector_id == detector_id for d in self.detectors): | ||
| raise ValueError(f"A detector with ID '{detector_id}' already exists.") | ||
| if isinstance(edge_inference_config, EdgeInferenceConfig): | ||
| config = edge_inference_config | ||
| existing = self.edge_inference_configs.get(config.name) | ||
| if existing is None: | ||
| self.edge_inference_configs[config.name] = config | ||
| elif existing is not config: | ||
| raise ValueError(f"A different inference config named '{config.name}' is already registered.") | ||
| config_name = config.name | ||
| else: | ||
| config_name = edge_inference_config | ||
| if config_name not in self.edge_inference_configs: | ||
| raise ValueError( | ||
| f"Edge inference config '{config_name}' not defined. " | ||
| f"Available configs: {list(self.edge_inference_configs.keys())}" | ||
| ) | ||
| self.detectors.append( | ||
| DetectorConfig( | ||
| detector_id=detector_id, | ||
| edge_inference_config=config_name, | ||
| ) | ||
| ) | ||
|
|
||
|
|
||
| # Preset inference configs matching the standard edge-endpoint defaults. | ||
| DEFAULT = EdgeInferenceConfig(name="default") | ||
| EDGE_WITH_ESCALATION = EdgeInferenceConfig( | ||
| name="edge_with_escalation", | ||
| always_return_edge_prediction=True, | ||
| min_time_between_escalations=2.0, | ||
| ) | ||
| NO_CLOUD = EdgeInferenceConfig( | ||
| name="no_cloud", | ||
| always_return_edge_prediction=True, | ||
| disable_cloud_escalation=True, | ||
| ) | ||
| DISABLED = EdgeInferenceConfig(name="disabled", enabled=False) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is temporary, I'll get rid of this before merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting rid of it is too easy, grab the most important snippets and put them in docs in a md file, we auto push those to a webpage