# Bring-Your-Own-Model: S3 Setup

GraphN can serve your own model weights from an S3 bucket. To make this work, GraphN's platform needs permission to read from your bucket. Rather than handing GraphN long-lived AWS keys, you grant access via an IAM role that GraphN assumes through STS — temporary credentials, scoped to your bucket, gated by an ExternalId only you and GraphN know.

This guide walks you through creating that role with one CloudFormation click.

## What you'll create

A single IAM role in your AWS account, with:

- A **trust policy** that lets GraphN's platform role (`arn:aws:iam::<graphn-account>:role/graphn-byom-s3-platform`) assume it, but only when the request carries the right **ExternalId**.
- A **permission policy** that lets it read objects from the S3 bucket and prefix you specify — and nothing else.

You stay in control: the role lives in your account, you can revoke it any time, and GraphN never sees a static AWS credential of yours.

## Setup

### Step 1 — Lay out your model in S3 (raw directory)

GraphN downloads weights with `aws s3 sync` on the AssumeRole path — there is no archive extraction, so **point GraphN at the prefix containing your model files, not a tar.gz**. The directory should be in HuggingFace layout:

```
s3://your-bucket/path/to/llama-3.1-8b/
├── config.json
├── tokenizer_config.json
├── tokenizer.json
├── model.safetensors                # or model.safetensors.index.json + shards
└── ...
```

If your existing pipeline produces tar.gz archives, extract them at upload time (`tar -xzf model.tar.gz -C s3://...` via your sync tool of choice). The raw-directory layout unlocks two things:

- **Faster downloads.** `aws s3 sync` runs object copies in parallel; a 30 GB raw directory is meaningfully faster end-to-end than a 30 GB tarball that has to be downloaded sequentially and extracted.
- **Pre-deploy architecture validation.** GraphN can read just `config.json` (~1 KB) before provisioning a download Job, so an unsupported architecture surfaces in seconds instead of after a multi-minute download.

### Step 2 — Start a model import in GraphN

1. Open **Models → Import Model → HuggingFace Weights**.
2. Switch the **Weight Source** to **S3 (Assume Role)**.
3. Fill in your **Model ID**, **S3 prefix** (`s3://your-bucket/path/to/llama-3.1-8b/` — must end with `/`), and approximate **Model Size**.
4. Pick how to provision the trust role:
   - **Create a new role** (default): GraphN auto-generates a fresh ExternalId and surfaces a CloudFormation Quick Create button.
   - **Reuse an existing role**: if you already deployed our CFN stack for an earlier import, paste the existing role's ARN and the ExternalId you used as the stack's `ExternalId` parameter (you can find it in the CloudFormation console under **Stacks → your stack → Parameters**). The role's IAM policy must cover the new prefix — see "Reusing roles" below.

Leave the GraphN tab open — you'll come back to paste in your role ARN after creating the stack (or skip directly to Continue if you're reusing).

### Step 3 — Launch the CloudFormation stack

Click the **Launch in CloudFormation** button. A new tab opens in the AWS Console with most of the parameters pre-filled:

| Parameter         | Pre-filled? | What it is                                                     |
| ----------------- | ----------- | -------------------------------------------------------------- |
| `PlatformRoleArn` | ✅ yes      | The GraphN role allowed to assume yours.                       |
| `ExternalId`      | ✅ yes      | The shared secret from the GraphN tab.                         |
| `BucketArn`       | ⚠️ verify   | Pre-filled from your S3 URL. Confirm it matches your bucket.   |
| `ObjectKeyPrefix` | optional    | Defaults to `*` (whole bucket). Scope down to a folder if you prefer (e.g. `llama-3.1-8b/*`). |
| `RoleName`        | optional    | Defaults to `graphn-byom-s3-reader`. If you change it, the name **must start with `graphn-byom-`** — GraphN's platform role policy is scoped to that prefix. |

1. Acknowledge the IAM capability checkbox at the bottom.
2. Click **Create stack**.
3. Wait ~30 seconds for `CREATE_COMPLETE`.
4. Open the stack's **Outputs** tab and copy the **`RoleArn`** value.

### Step 4 — Paste the role ARN back into GraphN

Back in the GraphN tab, paste the role ARN into the **IAM Role ARN** field and click **Continue**. GraphN runs a pre-deploy probe at this point — chained AssumeRole + a single `s3:GetObject` on `<prefix>config.json` — so the trust handshake, the IAM policy scope, *and* the architecture compatibility are all checked before any pod is provisioned. If anything is off (wrong bucket, wrong ExternalId, prefix not covered by the role's policy, unsupported architecture, missing `config.json`) you see the exact error inline and can fix it without burning a download Job.

That's it. GraphN will pull your weights and deploy the model.

## Doing it manually (without Quick Create)

If you'd rather create the role by hand or bake it into your own infra, the raw template is at [`/byom/graphn-byom-s3-role.yaml`](/byom/graphn-byom-s3-role.yaml). It takes the same four parameters. The trust policy and permissions are documented inline in the template.

> **Hosting the template yourself?** AWS CloudFormation only accepts S3-hosted URLs for `templateURL` in deep links — non-S3 origins (CDNs, GitHub raw, your own static site) return `TemplateURL must be a supported URL`. If you're scripting your own Quick Create flow, copy the YAML to a public-read S3 object (`https://<bucket>.s3.<region>.amazonaws.com/<key>`).

For a Terraform equivalent, attach this trust policy to a role whose name starts with `graphn-byom-` (the prefix is required — GraphN's platform IAM scopes its `sts:AssumeRole` permission to `arn:aws:iam::*:role/graphn-byom-*`):

```hcl
data "aws_iam_policy_document" "graphn_byom_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::<graphn-account>:role/graphn-byom-s3-platform"]
    }
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = ["<external-id-from-graphn-ui>"]
    }
  }
}

resource "aws_iam_role" "graphn_byom" {
  name               = "graphn-byom-s3-reader" # must start with graphn-byom-
  assume_role_policy = data.aws_iam_policy_document.graphn_byom_trust.json
}
```

Both `<graphn-account>` and `<external-id-from-graphn-ui>` are surfaced in the GraphN model-import screen.

## How GraphN uses the role

When GraphN needs to download your weights, the platform performs an `sts:AssumeRole` call against the role you created, passing the ExternalId. AWS hands back temporary credentials (≤ 1 hour TTL), GraphN streams the weights to its inference cluster, and the credentials expire — there is no long-lived secret of yours stored anywhere on GraphN's side. If you ever rotate or delete the role, the next deploy fails cleanly with an STS error and you can fix it without GraphN involvement.

> **Note on size.** AWS hard-caps role-chaining sessions at 1 hour. Models that finish downloading inside that window (the typical case for ≤ 200 GB archives within the same region) work first try. Larger or cross-region transfers can run into the cap; if they do, GraphN's smart-loader retries on the runtime path (a fresh chained AssumeRole at pod startup) so the deploy still succeeds, just without the proactive cache benefit. If you regularly import very large models, host the bucket in the GraphN cluster's region for best throughput.

## Reusing the same role across multiple imports

You don't need to deploy a new CloudFormation stack for every model. One role can serve many imports as long as **the role's IAM policy covers all the prefixes** you import from. Two common shapes:

- **Whole-bucket role** — at stack creation set `ObjectKeyPrefix=*`. The role can read every object in the bucket, so any prefix you point GraphN at works.
- **Parent-prefix role** — set `ObjectKeyPrefix=models/*` (or whatever parent contains your model directories). Imports from `s3://bucket/models/llama-8b/`, `s3://bucket/models/qwen-72b/`, etc. all succeed against this single role.

Tightly-scoped roles like `ObjectKeyPrefix=llama-3.1-8b/*` only allow reads under that one prefix; importing a different model means either widening the prefix on the existing stack or creating a second stack with a different `RoleName`.

To reuse a role in the wizard:

1. Pick **Reuse an existing role** in step 2.
2. Paste the role's ARN.
3. Paste the **same** ExternalId that's in the role's trust policy (find it under **CloudFormation → your stack → Parameters → ExternalId**). A fresh ExternalId would fail AssumeRole at deploy time.

The pre-deploy probe will surface a clear `AccessDenied` if the role's IAM policy doesn't cover the prefix you supplied, so you know to widen the policy (or pick a different role) without provisioning anything.

## Revoking access

Delete the CloudFormation stack from the AWS Console. GraphN's next download attempt will get an `AccessDenied` from STS, and the model deploy will fail visibly. Already-downloaded weights stay cached on GraphN's side until the model is deleted from the GraphN UI.

## Troubleshooting

| Error                                          | Cause                                                                  |
| ---------------------------------------------- | ---------------------------------------------------------------------- |
| `AccessDenied … is not authorized to perform: sts:AssumeRole` | The trust policy's ExternalId doesn't match what GraphN is sending. Re-launch the stack with the ExternalId from the current import flow. |
| `AccessDenied … s3:GetObject … on resource:`   | The `ObjectKeyPrefix` parameter is too narrow for the S3 URL. Update the stack with `*` or a wider prefix. |
| `NoSuchBucket`                                 | The `BucketArn` parameter doesn't match the bucket in the S3 URL.      |
| `Region not specified`                         | Re-trigger the deploy — GraphN auto-detects the bucket region; if that fails persistently, contact support. |
