Tangle Orchestration System

repository·master·Indexed 42 days ago

https://github.com/tangleml/tangle

An orchestration system for creating and running ML experiments and production pipelines. Tangle uses a visual interface to build graphs of containerized tasks that can be executed locally or in the cloud via launchers for Docker, Kubernetes, and Google Cloud Kubernetes Engine (GKE). It features a backend architecture consisting of an API Server, an Orchestrator, and a database supporting SQLite and MySQL, with built-in observability integration for OpenTelemetry, Jaeger, and Prometheus.

Tokens
18.4K
Snippets
47
Records
65
Agent score
59%

What's inside Tangle

  1. How Tangle pipelines, components, and tasks work

    master

    Tangle orchestrates containerized command-line programs using a graph-based model:

    • Component: A description of a containerized command-line program. It includes a ComponentSpec/component.yaml defining its signature (inputs/outputs), metadata, and implementation (container image, program to start, and how to map inputs/outputs to CLI arguments). Components can be written in any language (Python, Shell, R, etc.).
    • Task: An instance of a component with specific input arguments. Tasks are connected by linking the output of one task to the input of another.
    • Pipeline: A graph of interconnected tasks. When a pipeline is submitted for execution, Tangle runs the tasks (in parallel where possible) and manages the transfer of output artifacts between them.

    This architecture is compatible with Google Cloud Vertex AI Pipelines and Kubeflow Pipelines.

  2. Understand the Tangle backend architecture

    master

    The Tangle backend is composed of three main parts:

    • API Server: Handles API requests and manages database access. Documentation is available at http://localhost:8000/docs.
    • Orchestrator: Operates independently from the API Server. It launches container executions and facilitates data passing. It communicates with the API Server via the database and uses Launchers to execute tasks.
    • Database: Uses SQLAlchemy to abstract database access. Officially supported engines are SQLite and MySQL.

    Launchers

    Launchers are responsible for running containers on specific environments. Supported launchers include:

    • Local Docker: Uses local storage.
    • Local Kubernetes: Uses local storage via HostPath volumes.
    • Google Cloud Kubernetes Engine (GKE): Uses Google Cloud Storage.
  3. Deploy a single-tenant Tangle instance on HuggingFace

    master

    While Tangle is available as a multi-tenant service at TangleML/tangle, you can deploy your own single-tenant instance for complete data isolation and control.

    1. Duplicate the Tangle space to your own HuggingFace account.
    2. Provide a HuggingFace token during setup.

    Note for Teams: If you duplicate Tangle to an organization, you will create a single-tenant multi-user deployment where team members can share pipeline runs and benefit from organization-wide caching.

  4. Start the observability stack (OTel, Jaeger, Prometheus)

    master

    This example stack uses Docker Compose to run an OpenTelemetry (OTel) Collector, which receives telemetry from Tangle and forwards traces to Jaeger and metrics to Prometheus.

    To start the stack, run docker-compose up -d from the examples/observability/otel-jaeger-prometheus/ directory. You can view the logs using docker-compose logs -f and stop the stack using docker-compose down.

    docker-compose up -d
  5. Install Tangle on a local machine

    master

    To run Tangle locally, you need Docker and uv installed. Follow these steps to clone the necessary repositories and start the application:

    1. Download the app code (required once):

      git clone https://github.com/TangleML/tangle.git tangle/backend --branch stable
      git clone https://github.com/TangleML/tangle-ui.git tangle/ui_build --branch stable_local_build
    2. Start the app:

      • Linux and Mac OS:
        cd tangle && backend/start_local.sh
      • Windows:
        cd tangle && backend\start_local.cmd
    3. Access the UI: Once the terminal displays start_local: Starting the orchestrator, open http://localhost:8000 in your web browser.

    git clone https://github.com/TangleML/tangle.git tangle/backend --branch stable
    git clone https://github.com/TangleML/tangle-ui.git tangle/ui_build --branch stable_local_build
    
    # For Linux/Mac
    cd tangle && backend/start_local.sh
  6. Configure Tangle to export telemetry to the OTel collector

    master

    To enable tracing and metrics export from the Tangle API to the OTel collector, you must set the TANGLE_OTEL_EXPORTER_ENDPOINT environment variable. If this variable is unset, telemetry export is disabled.

    Use the following environment variables:

    VariableValueDescription
    TANGLE_OTEL_EXPORTER_ENDPOINThttp://localhost:4317OTLP collector address (gRPC). Use host.docker.internal:4317 if Tangle runs inside Docker.
    TANGLE_OTEL_EXPORTER_PROTOCOLgrpcProtocol for the exporter (default). This stack only accepts gRPC.
    TANGLE_ENVoptionalIncluded in the service name (e.g. tangle-development). Default: development.

    You can set these in your shell or via a .env file in your project root.

    export TANGLE_OTEL_EXPORTER_ENDPOINT=http://localhost:4317
    export TANGLE_OTEL_EXPORTER_PROTOCOL=grpc
    # then start your Tangle API
  7. Try Tangle in Google Cloud Shell

    master

    You can use Google Cloud Shell for a free environment (up to 50 hours per week).

    1. Open Google Cloud Shell in your browser.
    2. Download the app code:
      git clone https://github.com/TangleML/tangle.git tangle/backend --branch stable
      git clone https://github.com/TangleML/tangle-ui.git tangle/ui_build --branch stable_local_build
    3. Start the app:
      cd tangle && backend/start_local.sh
    4. Access the UI: Wait for the start_local: Starting the orchestrator and View app at messages, then open the proxy URL provided in the terminal (typically formatted as <https://shell.cloud.google.com/devshell/proxy?port=8000>) in a new browser tab.
    git clone https://github.com/TangleML/tangle.git tangle/backend --branch stable
    git clone https://github.com/TangleML/tangle-ui.git tangle/ui_build --branch stable_local_build
    
    cd tangle && backend/start_local.sh
  8. How the Orchestrator manages container execution lifecycle

    master

    The Orchestrator manages the lifecycle of containerized tasks through a state machine that transitions between statuses like PENDING, RUNNING, SUCCEEDED, FAILED, and CANCELLED.

    Key Lifecycle Transitions:

    • Launching: When a task is launched, the Orchestrator creates a ContainerExecution record, links it to ExecutionNodes, and stores metadata via a launcher_interfaces.LaunchedContainer object.
    • Polling/Refreshing: The Orchestrator periodically refreshes the container state using _launcher.get_refreshed_launched_container_from_dict. If the status changes (e.g., to SUCCEEDED), it triggers downstream updates.
    • Success: Upon SUCCEEDED, the Orchestrator validates output artifacts, updates ExecutionNode statuses to SUCCEEDED, and wakes up direct downstream executions by moving them from WAITING_FOR_UPSTREAM to QUEUED.
    • Failure: If the container fails or outputs are missing, the Orchestrator marks the ContainerExecution as FAILED and uses _mark_all_downstream_executions_as_skipped to recursively skip all dependent tasks.
    • Cancellation: If an ExecutionNode has a desired_state of TERMINATED, the Orchestrator attempts to upload logs and then calls launched_container.terminate() to cancel the execution.
    # Conceptual flow of status updates
    if new_status == launcher_interfaces.ContainerStatus.RUNNING:
        container_execution.status = bts.ContainerExecutionStatus.RUNNING
        for execution_node in execution_nodes:
            execution_node.container_execution_status = bts.ContainerExecutionStatus.RUNNING
    
    elif new_status == launcher_interfaces.ContainerStatus.SUCCEEDED:
        container_execution.status = bts.ContainerExecutionStatus.SUCCEEDED
        # ... update nodes and wake up downstream ...
    
    elif new_status == launcher_interfaces.ContainerStatus.FAILED:
        container_execution.status = bts.ContainerExecutionStatus.FAILED
        # ... skip downstream ...
  9. How component digests are calculated

    master

    Component digests are generated using SHA-256 to ensure content-addressable storage. The process involves:

    1. Encoding the component text to utf-8.
    2. Normalizing line endings by replacing \r\n with \n.
    3. Calculating the SHA-256 hex digest.

    This ensures that identical component definitions result in the same digest, preventing duplication.

    # Internal logic used by services:
    def calculate_digest_for_component_text(text: str) -> str:
        data = text.encode("utf-8")
        data = data.replace(b"\r\n", b"\n")
        digest = hashlib.sha256(data).hexdigest()
        return digest
  10. Implement a QueuedExecutionInterceptor to intercept execution launches

    master

    The QueuedExecutionInterceptor is a protocol used to take control of an execution before it is launched by the orchestrator.

    An implementation of this protocol is called once the orchestrator determines an execution is ready to launch (inputs are present, not skipped, no cache hit, etc.). If the intercept method returns True, the interceptor 'owns' the execution: it is responsible for setting the desired status and committing the transaction. The orchestrator will then skip the standard launch path for that execution.

    This is useful for implementing custom gating logic, manual approvals, or specialized handling of specific execution types.

    class QueuedExecutionInterceptor(typing.Protocol):
        def intercept(self, *, session: orm.Session, execution: bts.ExecutionNode) -> bool:
            """True if this execution was taken over and must not launch; False to continue."""
            ...
    
    # Example implementation
    class MyCustomInterceptor:
        def intercept(self, *, session: orm.Session, execution: bts.ExecutionNode) -> bool:
            if some_custom_condition(execution):
                execution.container_execution_status = bts.ContainerExecutionStatus.SKIPPED
                session.commit()
                return True
            return False
  11. ArtifactNode and ArtifactData models

    master

    Tangle uses a two-part system to manage data produced or consumed by tasks:

    1. ArtifactNode: Represents the logical presence of an artifact in a pipeline graph. It tracks the type of data, its producer (an ExecutionNode), and its relationships to upstream and downstream tasks via InputArtifactLink and OutputArtifactLink.
    2. ArtifactData: Represents the physical storage or value of the artifact. It contains the uri (for large files), the actual value (for small constants), the total_size, and a hash for integrity.

    Key Relationships:

    • ArtifactNode.artifact_data: Links the logical node to its physical data.
    • ArtifactNode.producer_execution: The execution that created this artifact.
    • ArtifactNode.upstream_execution_links / downstream_execution_links: Links to other nodes in the graph via artifact consumption/production.
    class ArtifactData(_TableBase):
        __tablename__ = "artifact_data"
        id: orm.Mapped[IdType] = orm.mapped_column(primary_key=True, init=False, insert_default=generate_unique_id)
        total_size: orm.Mapped[int] = orm.mapped_column(sql.BigInteger())
        is_dir: orm.Mapped[bool]
        hash: orm.Mapped[str]
        uri: orm.Mapped[str | None] = orm.mapped_column(default=None)
        value: orm.Mapped[str | None] = orm.mapped_column(sql.Text(), default=None)
    
    class ArtifactNode(_TableBase):
        __tablename__ = "artifact_node"
        id: orm.Mapped[IdType] = orm.mapped_column(primary_key=True, init=False, insert_default=generate_unique_id)
        type_name: orm.Mapped[str | None] = orm.mapped_column(default=None)
        type_properties: orm.Mapped[dict[str, Any] | None] = orm.mapped_column(default=None)
        producer_execution_id: orm.Mapped[IdType | None] = orm.mapped_column(sql.ForeignKey("execution_node.id"), init=False)
        artifact_data_id: orm.Mapped[IdType | None] = orm.mapped_column(sql.ForeignKey("artifact_data.id"), init=False)