|
25 | 25 | mask_sensitive_config, |
26 | 26 | ProviderTemplateLaunchRequest, |
27 | 27 | ProviderTemplateFileUploadResponse, |
| 28 | + ResumeFromCheckpointRequest, |
28 | 29 | ) |
29 | 30 | from transformerlab.shared.models.models import ProviderType, TeamComputeProvider |
30 | 31 | from transformerlab.compute_providers.base import ComputeProvider |
@@ -1703,6 +1704,230 @@ async def get_sweep_results( |
1703 | 1704 | } |
1704 | 1705 |
|
1705 | 1706 |
|
| 1707 | +@router.post("/jobs/{job_id}/resume_from_checkpoint") |
| 1708 | +async def resume_from_checkpoint( |
| 1709 | + job_id: str, |
| 1710 | + experimentId: str = Query(..., description="Experiment ID"), |
| 1711 | + request: ResumeFromCheckpointRequest = ..., |
| 1712 | + user_and_team=Depends(get_user_and_team), |
| 1713 | + session: AsyncSession = Depends(get_async_session), |
| 1714 | +): |
| 1715 | + """ |
| 1716 | + Resume a REMOTE job from a checkpoint by creating a new job with the same configuration |
| 1717 | + and setting parent_job_id and resumed_from_checkpoint in job_data. |
| 1718 | + """ |
| 1719 | + import json |
| 1720 | + from transformerlab.services import job_service |
| 1721 | + from lab.dirs import get_job_checkpoints_dir |
| 1722 | + from lab import storage |
| 1723 | + import time |
| 1724 | + |
| 1725 | + # Get the original job |
| 1726 | + original_job = job_service.job_get(job_id) |
| 1727 | + if not original_job or str(original_job.get("experiment_id")) != str(experimentId): |
| 1728 | + raise HTTPException(status_code=404, detail="Job not found") |
| 1729 | + |
| 1730 | + # Validate it's a REMOTE job |
| 1731 | + if original_job.get("type") != "REMOTE": |
| 1732 | + raise HTTPException(status_code=400, detail="Resume from checkpoint is only supported for REMOTE jobs") |
| 1733 | + |
| 1734 | + # Get job_data |
| 1735 | + job_data = original_job.get("job_data") or {} |
| 1736 | + if not isinstance(job_data, dict): |
| 1737 | + try: |
| 1738 | + job_data = json.loads(job_data) |
| 1739 | + except json.JSONDecodeError: |
| 1740 | + job_data = {} |
| 1741 | + |
| 1742 | + # Validate required fields for REMOTE job relaunch |
| 1743 | + provider_id = job_data.get("provider_id") |
| 1744 | + command = job_data.get("command") |
| 1745 | + if not provider_id or not command: |
| 1746 | + raise HTTPException( |
| 1747 | + status_code=400, |
| 1748 | + detail="Original job is missing required fields (provider_id or command) for resume", |
| 1749 | + ) |
| 1750 | + |
| 1751 | + # Verify checkpoint exists using workspace-aware path resolution |
| 1752 | + checkpoints_dir = get_job_checkpoints_dir(job_id) |
| 1753 | + checkpoint_path = storage.join(checkpoints_dir, request.checkpoint) |
| 1754 | + if not storage.exists(checkpoint_path): |
| 1755 | + raise HTTPException(status_code=404, detail=f"Checkpoint '{request.checkpoint}' not found") |
| 1756 | + |
| 1757 | + # Get provider |
| 1758 | + team_id = user_and_team["team_id"] |
| 1759 | + provider = await get_team_provider(session, team_id, provider_id) |
| 1760 | + if not provider: |
| 1761 | + raise HTTPException(status_code=404, detail="Provider not found") |
| 1762 | + |
| 1763 | + # Create new REMOTE job |
| 1764 | + initial_status = "INTERACTIVE" if job_data.get("subtype") == "interactive" else "LAUNCHING" |
| 1765 | + new_job_id = job_service.job_create(type="REMOTE", status=initial_status, experiment_id=experimentId, job_data={}) |
| 1766 | + |
| 1767 | + # Set parent_job_id and resumed_from_checkpoint in job_data |
| 1768 | + job_service.job_update_job_data_insert_key_value(new_job_id, "parent_job_id", job_id, experimentId) |
| 1769 | + job_service.job_update_job_data_insert_key_value( |
| 1770 | + new_job_id, "resumed_from_checkpoint", request.checkpoint, experimentId |
| 1771 | + ) |
| 1772 | + |
| 1773 | + # Copy all original job launch configuration |
| 1774 | + config_fields = [ |
| 1775 | + "command", |
| 1776 | + "task_name", |
| 1777 | + "subtype", |
| 1778 | + "interactive_type", |
| 1779 | + "cpus", |
| 1780 | + "memory", |
| 1781 | + "disk_space", |
| 1782 | + "accelerators", |
| 1783 | + "num_nodes", |
| 1784 | + "setup", |
| 1785 | + "env_vars", |
| 1786 | + "file_mounts", |
| 1787 | + "parameters", |
| 1788 | + "provider_id", |
| 1789 | + "provider_type", |
| 1790 | + "provider_name", |
| 1791 | + "github_repo_url", |
| 1792 | + "github_directory", |
| 1793 | + "user_info", |
| 1794 | + "team_id", |
| 1795 | + ] |
| 1796 | + |
| 1797 | + for field in config_fields: |
| 1798 | + value = job_data.get(field) |
| 1799 | + if value is not None: |
| 1800 | + job_service.job_update_job_data_insert_key_value(new_job_id, field, value, experimentId) |
| 1801 | + |
| 1802 | + # Relaunch via provider - replicate launch logic from compute_provider.py |
| 1803 | + try: |
| 1804 | + provider_instance = get_provider_instance(provider) |
| 1805 | + except Exception as exc: |
| 1806 | + await job_service.job_update_status(new_job_id, "FAILED", experimentId, error_msg=str(exc)) |
| 1807 | + raise HTTPException(status_code=500, detail=f"Failed to initialize provider: {exc}") from exc |
| 1808 | + |
| 1809 | + # Build cluster name |
| 1810 | + base_name = job_data.get("task_name") or provider.name |
| 1811 | + formatted_cluster_name = f"{_sanitize_cluster_basename(base_name)}-job-{new_job_id}" |
| 1812 | + |
| 1813 | + # Get user info |
| 1814 | + user = user_and_team.get("user") |
| 1815 | + user_info = {} |
| 1816 | + if user: |
| 1817 | + if getattr(user, "first_name", None) or getattr(user, "last_name", None): |
| 1818 | + user_info["name"] = " ".join( |
| 1819 | + part for part in [getattr(user, "first_name", ""), getattr(user, "last_name", "")] if part |
| 1820 | + ).strip() |
| 1821 | + if getattr(user, "email", None): |
| 1822 | + user_info["email"] = getattr(user, "email") |
| 1823 | + |
| 1824 | + provider_display_name = job_data.get("provider_name") or provider.name |
| 1825 | + |
| 1826 | + # Prepare environment variables |
| 1827 | + env_vars = (job_data.get("env_vars") or {}).copy() |
| 1828 | + env_vars["_TFL_JOB_ID"] = str(new_job_id) |
| 1829 | + env_vars["_TFL_EXPERIMENT_ID"] = experimentId |
| 1830 | + |
| 1831 | + # Get TFL_STORAGE_URI from storage context |
| 1832 | + tfl_storage_uri = None |
| 1833 | + try: |
| 1834 | + storage_root = storage.root_uri() |
| 1835 | + if storage_root and any(storage_root.startswith(prefix) for prefix in ("s3://", "gs://", "gcs://", "abfs://")): |
| 1836 | + tfl_storage_uri = storage_root |
| 1837 | + except Exception: |
| 1838 | + pass |
| 1839 | + |
| 1840 | + if tfl_storage_uri: |
| 1841 | + env_vars["TFL_STORAGE_URI"] = tfl_storage_uri |
| 1842 | + env_vars["_TFL_REMOTE_SKYPILOT_WORKSPACE"] = "true" |
| 1843 | + |
| 1844 | + # Build setup script |
| 1845 | + setup_commands = [] |
| 1846 | + # Add GitHub clone setup if enabled |
| 1847 | + github_repo_url = job_data.get("github_repo_url") |
| 1848 | + if github_repo_url: |
| 1849 | + workspace_dir = get_workspace_dir() |
| 1850 | + github_pat = read_github_pat_from_workspace(workspace_dir) |
| 1851 | + github_setup = generate_github_clone_setup( |
| 1852 | + repo_url=github_repo_url, |
| 1853 | + directory=job_data.get("github_directory"), |
| 1854 | + github_pat=github_pat, |
| 1855 | + ) |
| 1856 | + setup_commands.append(github_setup) |
| 1857 | + |
| 1858 | + # Add user-provided setup if any |
| 1859 | + original_setup = job_data.get("setup") |
| 1860 | + if original_setup: |
| 1861 | + setup_commands.append(original_setup) |
| 1862 | + |
| 1863 | + final_setup = ";".join(setup_commands) if setup_commands else None |
| 1864 | + |
| 1865 | + # Update job_data with launch configuration |
| 1866 | + launch_job_data = { |
| 1867 | + "task_name": job_data.get("task_name"), |
| 1868 | + "command": command, |
| 1869 | + "cluster_name": formatted_cluster_name, |
| 1870 | + "subtype": job_data.get("subtype"), |
| 1871 | + "interactive_type": job_data.get("interactive_type"), |
| 1872 | + "cpus": job_data.get("cpus"), |
| 1873 | + "memory": job_data.get("memory"), |
| 1874 | + "disk_space": job_data.get("disk_space"), |
| 1875 | + "accelerators": job_data.get("accelerators"), |
| 1876 | + "num_nodes": job_data.get("num_nodes"), |
| 1877 | + "setup": final_setup, |
| 1878 | + "env_vars": env_vars if env_vars else None, |
| 1879 | + "file_mounts": job_data.get("file_mounts") or None, |
| 1880 | + "parameters": job_data.get("parameters") or None, |
| 1881 | + "provider_id": provider.id, |
| 1882 | + "provider_type": provider.type, |
| 1883 | + "provider_name": provider_display_name, |
| 1884 | + "user_info": user_info or None, |
| 1885 | + "team_id": team_id, |
| 1886 | + "start_time": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()), |
| 1887 | + } |
| 1888 | + |
| 1889 | + for key, value in launch_job_data.items(): |
| 1890 | + if value is not None: |
| 1891 | + job_service.job_update_job_data_insert_key_value(new_job_id, key, value, experimentId) |
| 1892 | + |
| 1893 | + # Build ClusterConfig |
| 1894 | + disk_size = None |
| 1895 | + if job_data.get("disk_space"): |
| 1896 | + try: |
| 1897 | + disk_size = int(job_data.get("disk_space")) |
| 1898 | + except (TypeError, ValueError): |
| 1899 | + disk_size = None |
| 1900 | + |
| 1901 | + cluster_config = ClusterConfig( |
| 1902 | + cluster_name=formatted_cluster_name, |
| 1903 | + provider_name=provider_display_name, |
| 1904 | + provider_id=provider.id, |
| 1905 | + command=command, |
| 1906 | + setup=final_setup, |
| 1907 | + env_vars=env_vars, |
| 1908 | + cpus=job_data.get("cpus"), |
| 1909 | + memory=job_data.get("memory"), |
| 1910 | + accelerators=job_data.get("accelerators"), |
| 1911 | + num_nodes=job_data.get("num_nodes"), |
| 1912 | + disk_size=disk_size, |
| 1913 | + file_mounts=job_data.get("file_mounts") or {}, |
| 1914 | + provider_config={"requested_disk_space": job_data.get("disk_space")}, |
| 1915 | + ) |
| 1916 | + |
| 1917 | + # Launch cluster |
| 1918 | + try: |
| 1919 | + provider_instance.launch_cluster(formatted_cluster_name, cluster_config) |
| 1920 | + return { |
| 1921 | + "job_id": new_job_id, |
| 1922 | + "message": "Job relaunched from checkpoint", |
| 1923 | + "cluster_name": formatted_cluster_name, |
| 1924 | + } |
| 1925 | + except Exception as exc: |
| 1926 | + print(f"Failed to launch cluster: {exc}") |
| 1927 | + await job_service.job_update_status(new_job_id, "FAILED", experimentId, error_msg=str(exc)) |
| 1928 | + raise HTTPException(status_code=500, detail=f"Failed to relaunch job: {exc}") from exc |
| 1929 | + |
| 1930 | + |
1706 | 1931 | @router.post("/{provider_id}/clusters/{cluster_name}/stop") |
1707 | 1932 | async def stop_cluster( |
1708 | 1933 | provider_id: str, |
|
0 commit comments