Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions deploy/makermodslab-station.service
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@
# sudo systemctl restart makermodslab-station
# sudo systemctl stop makermodslab-station # e.g. before running `makermodslab --dev`
#
# The service runs the offline posture (HF_HUB_OFFLINE=1 via makermodslab-station):
# every hardware flow works, Hub calls fail fast. For an upload/Hub session,
# stop the service and run `.venv/bin/makermodslab --lan` in a proxied shell — or
# uncomment the Environment lines below to run the service itself through a
# proxy (then it is NOT offline-deterministic; see INSTALL.md "server
# posture").
# The service runs the LAN posture (makermodslab-station == `makermodslab --lan`):
# it binds 0.0.0.0 so other machines on the network can reach it, and never
# opens a browser. The Hub is expected to be reachable — login, dataset upload,
# and cloud training all need it — so give the station outbound network. If it
# sits behind a proxy, uncomment the Environment lines below.

[Unit]
Description=MakerMods Lab robot station (headless, LAN-served, Hub-offline)
Description=MakerMods Lab robot station (headless, LAN-served)
After=network-online.target
Wants=network-online.target

Expand All @@ -30,7 +29,7 @@ ExecStart=/home/makermods/MakerModsLab/.venv/bin/makermodslab-station
Restart=on-failure
RestartSec=3
# Serial ports: the unit user must be in dialout (usermod -aG dialout <user>).
# Proxy for an online posture (see note above) — normally leave commented:
# Proxy, if the station reaches the Hub through one — normally leave commented:
#Environment=https_proxy=http://127.0.0.1:7897
#Environment=http_proxy=http://127.0.0.1:7897
#Environment=no_proxy=localhost,127.0.0.1,192.168.0.0/16
Expand Down
27 changes: 2 additions & 25 deletions frontend/src/components/landing/ManageCachesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2, Trash2, HardDrive, AlertTriangle } from "lucide-react";
import { Loader2, Trash2, HardDrive } from "lucide-react";
import { useApi } from "@/contexts/ApiContext";
import {
DatasetItem,
deleteDataset,
getDatasetInfo,
} from "@/lib/replayApi";
import { listRunnerHardware } from "@/lib/jobsApi";

interface Props {
open: boolean;
Expand Down Expand Up @@ -49,25 +48,15 @@ const ManageCachesDialog: React.FC<Props> = ({
// Repo ids currently being cleared (per-row spinner + disabled buttons).
const [clearing, setClearing] = useState<Set<string>>(new Set());
const [error, setError] = useState<string | null>(null);
// HF_HUB_OFFLINE on the backend: a cleared cache can't be re-downloaded.
const [offline, setOffline] = useState(false);

// On open: reset transient state and fetch sizes + the offline signal.
// On open: reset transient state and fetch sizes.
useEffect(() => {
if (!open) return;
setError(null);
setClearing(new Set());
setSizes({});

let cancelled = false;
listRunnerHardware(baseUrl, fetchWithHeaders)
.then((h) => {
if (!cancelled) setOffline(!!h.offline);
})
.catch(() => {
if (!cancelled) setOffline(false);
});

for (const d of cached) {
getDatasetInfo(baseUrl, fetchWithHeaders, d.repo_id)
.then((info) => {
Expand Down Expand Up @@ -137,18 +126,6 @@ const ManageCachesDialog: React.FC<Props> = ({
</DialogHeader>

<div className="space-y-4">
{offline && (
<div className="flex items-start gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 p-2 text-xs text-amber-700 dark:text-amber-200">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>
This backend is in offline mode (
<code className="text-amber-800 dark:text-amber-100">HF_HUB_OFFLINE</code>). A
cleared cache can&apos;t be re-downloaded from the Hub until
offline mode is switched off.
</span>
</div>
)}

{cached.length === 0 ? (
<p className="rounded-md border border-border p-3 text-sm text-muted-foreground">
No HF datasets are cached locally.
Expand Down
22 changes: 1 addition & 21 deletions frontend/src/components/training/TrainingConfigurator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,6 @@ const TrainingConfigurator: React.FC<TrainingConfiguratorProps> = ({
const [authenticated, setAuthenticated] = useState<boolean>(false);
const [flavors, setFlavors] = useState<RunnerFlavor[]>([]);
const [hardwareLoading, setHardwareLoading] = useState(true);
// HF_HUB_OFFLINE on the backend: Hub writes (incl. dataset upload) disabled.
const [offline, setOffline] = useState<boolean>(false);

// Whether the user has hand-toggled the AMP switch this session — once they
// have, their choice wins over the per-policy default below, same as any
Expand Down Expand Up @@ -356,12 +354,10 @@ const TrainingConfigurator: React.FC<TrainingConfiguratorProps> = ({
.then((data) => {
setAuthenticated(data.authenticated);
setFlavors(data.flavors);
setOffline(!!data.offline);
})
.catch(() => {
setAuthenticated(false);
setFlavors([]);
setOffline(false);
})
.finally(() => setHardwareLoading(false));
}, [baseUrl, fetchWithHeaders, auth.status]);
Expand Down Expand Up @@ -581,35 +577,21 @@ const TrainingConfigurator: React.FC<TrainingConfiguratorProps> = ({
config.steps <= resumeSeed.step
? `Total steps must be greater than the checkpoint's step (${resumeSeed.step.toLocaleString()}).`
: null;
// A local-only dataset on a cloud run is uploadable — unless the backend is
// in offline mode, in which case uploads are impossible and Start is a hard
// block. (needsUpload is already gated on isCloud.)
const uploadBlockedOffline = needsUpload && offline;
// A local run continued on the cloud has to push its checkpoint to the Hub
// first, which offline mode makes impossible — a hard block, exactly like the
// dataset case above.
const checkpointUploadBlockedOffline = needsCheckpointUpload && offline;
const startDisabled =
isStarting ||
uploading ||
!datasetRepoId ||
localBlocked ||
(targetRequiresAuth && !authenticated) ||
targetMissingFlavor ||
uploadBlockedOffline ||
checkpointUploadBlockedOffline ||
resumeStepError != null;
const startTooltip = localBlocked
? "Another local training is already running"
: targetRequiresAuth && !authenticated
? "Log in to Hugging Face to use cloud compute"
: targetMissingFlavor
? "Select a hardware flavor"
: uploadBlockedOffline
? "Offline mode is on — the dataset can't be uploaded to the Hub"
: checkpointUploadBlockedOffline
? "Offline mode is on — the checkpoint can't be uploaded to the Hub"
: undefined;
: undefined;

return (
<div className="w-full">
Expand Down Expand Up @@ -679,7 +661,6 @@ const TrainingConfigurator: React.FC<TrainingConfiguratorProps> = ({
<LocalDatasetCloudNotice
repoId={datasetRepoId}
sizeBytes={datasetSizeBytes}
offline={offline}
uploading={uploading}
errorMessage={uploadError}
/>
Expand All @@ -694,7 +675,6 @@ const TrainingConfigurator: React.FC<TrainingConfiguratorProps> = ({
<LocalCheckpointCloudNotice
runName={resumeSeed.name}
step={resumeSeed.step}
offline={offline}
/>
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import React from "react";
import { AlertTriangle, Lock, UploadCloud, WifiOff } from "lucide-react";
import { AlertTriangle, Lock, UploadCloud } from "lucide-react";

interface LocalCheckpointCloudNoticeProps {
/** The run being continued, for a notice that names what moves. */
runName: string;
/** The checkpoint step the continuation resumes from; null ⇒ the latest. */
step: number | null;
/** Backend is in HF_HUB_OFFLINE mode: nothing can be uploaded, so this
* continuation can't run on the cloud at all. */
offline: boolean;
}

/**
Expand All @@ -29,36 +26,10 @@ interface LocalCheckpointCloudNoticeProps {
const LocalCheckpointCloudNotice: React.FC<LocalCheckpointCloudNoticeProps> = ({
runName,
step,
offline,
}) => {
const stepLabel =
step != null ? `step ${step.toLocaleString()}` : "its latest checkpoint";

if (offline) {
return (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-100">
<div className="flex items-start gap-2">
<WifiOff className="w-4 h-4 mt-0.5 shrink-0 text-amber-600 dark:text-amber-300" />
<div>
<div className="font-semibold">
This checkpoint is only on this machine
</div>
<p className="mt-1 text-amber-700/80 dark:text-amber-200/80">
Hugging Face Cloud continues from the Hub, but the server is in
offline mode (
<code className="text-amber-700 dark:text-amber-100">
HF_HUB_OFFLINE
</code>
), so {stepLabel} of{" "}
<span className="font-medium">{runName}</span> can't be uploaded.
Switch off offline mode, or continue this run locally.
</p>
</div>
</div>
</div>
);
}

return (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-100">
<div className="flex items-start gap-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import React from "react";
import { AlertTriangle, Loader2, UploadCloud, WifiOff } from "lucide-react";
import { AlertTriangle, Loader2, UploadCloud } from "lucide-react";

interface LocalDatasetCloudNoticeProps {
/** The local-only dataset the cloud run would train on. */
repoId: string;
/** Approximate on-disk size in bytes, if the info endpoint had it cheaply.
* null when unknown (Hub-only detail, or the fetch hasn't resolved). */
sizeBytes: number | null;
/** Backend is in HF_HUB_OFFLINE mode: uploads are disabled, so training
* can't proceed for this dataset at all. */
offline: boolean;
/** True while this dataset's upload is in flight (drives the progress line). */
uploading: boolean;
/** Last upload error, shown in-place so the user doesn't lose it to a toast. */
Expand All @@ -33,39 +30,16 @@ const formatSize = (bytes: number): string => {
* targeted at a dataset that exists only on this machine. HF Jobs trains from
* the Hub, so the dataset must be uploaded first — the Start button becomes
* "Upload & start training" and the upload is chained before the job launches
* (see Training.tsx). In offline mode the upload is impossible, so this turns
* into a hard block instead.
* (see Training.tsx).
*/
const LocalDatasetCloudNotice: React.FC<LocalDatasetCloudNoticeProps> = ({
repoId,
sizeBytes,
offline,
uploading,
errorMessage,
}) => {
const sizeLabel = sizeBytes != null ? formatSize(sizeBytes) : null;

if (offline) {
return (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-100">
<div className="flex items-start gap-2">
<WifiOff className="w-4 h-4 mt-0.5 shrink-0 text-amber-600 dark:text-amber-300" />
<div>
<div className="font-semibold">
This dataset is only on this machine
</div>
<p className="mt-1 text-amber-700/80 dark:text-amber-200/80">
Hugging Face Cloud trains from the Hub, but the server is in
offline mode (<code className="text-amber-700 dark:text-amber-100">HF_HUB_OFFLINE</code>
), so <span className="font-medium">{repoId}</span> can't be
uploaded. Switch off offline mode, or run this training locally.
</p>
</div>
</div>
</div>
);
}

return (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-100">
<div className="flex items-start gap-2">
Expand Down
6 changes: 0 additions & 6 deletions frontend/src/lib/jobsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,18 +311,12 @@ export interface RunnerHardwareResponse {
authenticated: boolean;
username: string | null;
flavors: RunnerFlavor[];
// True when the backend is in HF_HUB_OFFLINE mode: every Hub write is
// disabled, so a local-only dataset can't be uploaded for a cloud run. The
// training page uses this to keep Start disabled + explain why. Absent on
// older backends → treated as online (false).
offline?: boolean;
}

const EMPTY_HARDWARE: RunnerHardwareResponse = {
authenticated: false,
username: null,
flavors: [],
offline: false,
};

export async function listRunnerHardware(
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/modelsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export async function getModelInfo(
/** POST /models/upload — push a local run's final checkpoint to the Hub as a
* PUBLIC, MakerModsLab-tagged model repo. `id` is the local run id; `repoId` optionally
* overrides the default namespaced repo id. MUTATES the Hub. Throws ApiError
* (400 offline, 403 no write access, 404 no checkpoint, 502 Hub failure) with
* the backend message in `.detail`. Returns {repo_id, url, tags}. */
* (403 no write access, 404 no checkpoint, 502 Hub failure) with the backend
* message in `.detail`. Returns {repo_id, url, tags}. */
export async function uploadModel(
baseUrl: string,
fetcher: Fetcher,
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/lib/replayApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,9 @@ export interface HubSettings {
tags: string[];
}

/** Read the current visibility + tags of a Hub dataset. Throws ApiError (400
* offline, 403 no read/write access, 502 Hub failure) with the backend message
* in `.detail`. */
/** Read the current visibility + tags of a Hub dataset. Throws ApiError (403
* no read/write access, 502 Hub failure) with the backend message in
* `.detail`. */
export async function getDatasetHubSettings(
baseUrl: string,
fetcher: Fetcher,
Expand All @@ -294,8 +294,8 @@ export async function getDatasetHubSettings(
}

/** Flip a Hub dataset's visibility (public <-> private). MUTATES the live repo.
* Throws ApiError (400 offline, 403 no write access, 502 Hub failure) with the
* backend message in `.detail`. */
* Throws ApiError (403 no write access, 502 Hub failure) with the backend
* message in `.detail`. */
export async function setDatasetVisibility(
baseUrl: string,
fetcher: Fetcher,
Expand All @@ -313,8 +313,8 @@ export async function setDatasetVisibility(

/** Replace a Hub dataset card's `tags:`. The backend re-adds the required org
* tags, so the returned list may include tags beyond the ones passed. MUTATES
* the live card. Throws ApiError (400 offline, 403 no write access, 502 Hub
* failure) with the backend message in `.detail`. */
* the live card. Throws ApiError (403 no write access, 502 Hub failure) with
* the backend message in `.detail`. */
export async function setDatasetTags(
baseUrl: string,
fetcher: Fetcher,
Expand Down
Loading
Loading