Module neuralops.experiment_utils.tasks

Background training task (Django integration) with WebSocket broadcasting and AI assistant.

Functions

def broadcast_terminal(msg: str) ‑> None
Expand source code
def broadcast_terminal(msg: str) -> None:
    """Send a log message to the terminal WebSocket group."""
    if not HAS_CHANNELS:
        return
    try:
        channel_layer = get_channel_layer()
        if channel_layer and async_to_sync:
            async_to_sync(channel_layer.group_send)(
                "terminal", {"type": "terminal_message", "data": msg}
            )
    except Exception as e:
        print(f"WS broadcast error: {e}")

Send a log message to the terminal WebSocket group.

def run_training_async(experiment, stop_event: threading.Event) ‑> None
Expand source code
def run_training_async(experiment, stop_event: threading.Event) -> None:
    """
    Main entry point called by Django in a background thread.

    Args:
        experiment: Django model instance with fields:
            id, config (JSON), status, logs, current_step, total_steps, etc.
        stop_event: threading.Event to signal cancellation.
    """
    config = experiment.config
    run_id = str(experiment.id)
    base_dir = _get_media_root() / "experiments"

    def log(msg: str) -> None:
        """Log."""
        experiment.logs = (experiment.logs or "") + msg + "\n"
        experiment.save(update_fields=["logs"])
        WSMessageQueue.send(run_id, {"type": "log", "data": msg})
        broadcast_terminal(msg)

    def update_status(status: str) -> None:
        """Update status."""
        experiment.status = status
        experiment.save(update_fields=["status"])
        WSMessageQueue.send(run_id, {"type": "status", "status": status})

    def progress_callback(current: int, total: int, pct: float) -> None:
        """Progress callback."""
        experiment.current_step = current
        experiment.total_steps = total
        experiment.estimated_remaining_seconds = (total - current) * 0.1  # crude
        experiment.cpu_usage = psutil.cpu_percent()
        experiment.ram_usage_mb = psutil.virtual_memory().used / 1024**2
        experiment.save(
            update_fields=[
                "current_step",
                "total_steps",
                "estimated_remaining_seconds",
                "cpu_usage",
                "ram_usage_mb",
            ]
        )
        WSMessageQueue.send(
            run_id,
            {
                "type": "progress",
                "progress": pct,
                "current_step": current,
                "total_steps": total,
            },
        )

    def log_callback(msg: str):
        # Called by the orchestrator for log messages
        """Log callback."""
        log(msg)

    def final_callback(metrics: dict) -> None:
        """Final callback."""
        experiment.config["final_metrics"] = metrics
        experiment.save(update_fields=["config"])
        WSMessageQueue.send(run_id, {"type": "final", "data": metrics})
        log(f"Training finished. Final metrics: {json.dumps(metrics)}")
        save_artifacts(experiment, metrics)

    try:
        update_status("running")
        orchestrator = MegaOrchestrator(
            config=config,
            run_id=run_id,
            base_dir=base_dir,
            progress_callback=progress_callback,
            log_callback=log_callback,
            final_callback=final_callback,
            stop_event=stop_event,
        )
        orchestrator.run()

        if stop_event.is_set():
            update_status("stopped")
            log("Training cancelled by user.")
        else:
            update_status("completed")

    except Exception as e:
        error_msg = traceback.format_exc()
        log(f"ERROR: {str(e)}\n{error_msg}")
        update_status("failed")

Main entry point called by Django in a background thread.

Args
-----=
experiment
Django model instance with fields: id, config (JSON), status, logs, current_step, total_steps, etc.
stop_event
threading.Event to signal cancellation.
def save_artifacts(experiment, final_metrics: dict) ‑> None
Expand source code
def save_artifacts(experiment, final_metrics: dict) -> None:
    """Save model, plots, history, etc. to the experiment's media directory."""
    output_dir = _get_media_root() / "experiments" / str(experiment.id)
    output_dir.mkdir(parents=True, exist_ok=True)

    # History is saved by the saver inside orchestrator, we just add a few extra things
    import matplotlib.pyplot as plt

    # Loss plot from training history (if available)
    history = experiment.config.get("history", [])
    if history:
        steps = [h.get("step", i) for i, h in enumerate(history) if "loss" in h]
        losses = [h["loss"] for h in history if "loss" in h]
        if steps:
            plt.figure(figsize=(10, 6))
            plt.plot(steps, losses, label="Train Loss")
            plt.xlabel("Step")
            plt.ylabel("Loss")
            plt.legend()
            plt.grid(True)
            plt.savefig(output_dir / "loss_curve.png", dpi=150)
            plt.close()

    # Final metrics JSON
    with open(output_dir / "final_metrics.json", "w") as f:
        json.dump(final_metrics, f, indent=2)

    # Copy model if present
    model_src = _get_media_root() / "experiments" / str(experiment.id) / "artifacts" / "model"
    if model_src.exists():
        import shutil

        shutil.copytree(model_src, output_dir / "model", dirs_exist_ok=True)

Save model, plots, history, etc. to the experiment's media directory.

Classes

class AIAssistant (llm_function)
Expand source code
class AIAssistant:
    """
    Monitors training metrics and suggests actions (e.g., early stop).
    Uses a callable LLM function for decision making.
    """

    def __init__(self, llm_function) -> None:
        self.llm = llm_function

    def monitor(self, exp_id: str, metrics_history: list) -> None:
        """Monitor."""
        last_losses = [m.get("loss") for m in metrics_history[-5:] if "loss" in m]
        if len(last_losses) >= 5 and all(
            abs(last_losses[i] - last_losses[i - 1]) < 0.01 for i in range(1, 5)
        ):
            suggestion = "early_stop"
            reason = "Loss plateau detected"
            WSMessageQueue.send(
                exp_id,
                {"type": "ai_suggestion", "suggestion": suggestion, "reason": reason},
            )
            # Optionally trigger early stop
            # stop_event.set()

Monitors training metrics and suggests actions (e.g., early stop). Uses a callable LLM function for decision making.

Methods

def monitor(self, exp_id: str, metrics_history: list) ‑> None
Expand source code
def monitor(self, exp_id: str, metrics_history: list) -> None:
    """Monitor."""
    last_losses = [m.get("loss") for m in metrics_history[-5:] if "loss" in m]
    if len(last_losses) >= 5 and all(
        abs(last_losses[i] - last_losses[i - 1]) < 0.01 for i in range(1, 5)
    ):
        suggestion = "early_stop"
        reason = "Loss plateau detected"
        WSMessageQueue.send(
            exp_id,
            {"type": "ai_suggestion", "suggestion": suggestion, "reason": reason},
        )
        # Optionally trigger early stop
        # stop_event.set()

Monitor.

class WSMessageQueue
Expand source code
class WSMessageQueue:
    """Thread‑safe WebSocket message sender."""

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    @staticmethod
    def send(experiment_id: str, data: dict) -> None:
        """Send."""
        try:
            channel_layer = get_channel_layer()
            async_to_sync(channel_layer.group_send)(
                f"job_{experiment_id}", {"type": "job_update", "data": data}
            )
        except Exception as e:
            print(f"WS send error: {e}")

Thread‑safe WebSocket message sender.

Static methods

def send(experiment_id: str, data: dict) ‑> None
Expand source code
@staticmethod
def send(experiment_id: str, data: dict) -> None:
    """Send."""
    try:
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            f"job_{experiment_id}", {"type": "job_update", "data": data}
        )
    except Exception as e:
        print(f"WS send error: {e}")

Send.