Shared data bucket: staging before each term
Last updated on 2026-10-21 | Edit this page
Learners create exactly one bucket of their own (S3 episode) and write every output there. Everything larger than the Titanic CSVs lives in one shared, read-only bucket that instructors stage before class, so the class holds one copy of each dataset instead of fifty. This page is the whole staging procedure; the episodes assume the layout below.
Layout
s3://<shared-bucket>/
wattbot/
metadata.csv from data/data.zip in the lesson repo
train_QA.csv from data/data.zip
corpus.zip built once with scripts/build_wattbot_corpus.py
pages.jsonl page text extracted from corpus.zip by the same script
embeddings_bedrock.npy the Titan V2 vectors from your own Bedrock run; learners download it in class
embeddings_gte_large.npy the Processing-jobs episode's embedding-job output; learners copy it instead of running that job in class
crops.zip optional, for the Bedrock episode's multimodal step: the table and figure crops (crops/ plus crops.jsonl)
embeddings_crops_multimodal.npy optional, with it: the Nova multimodal vectors of those crops
solutions/wattbot_solutions_vision_{none,image_index,text_index}.csv optional: the three vision runs' predictions, so the class scores them instead of spending twelve minutes of calls
badger-scribe/
train.csv test.csv metadata.csv metric.py
sample_submission.csv transcription_conventions.md
images/<page_id>.jpg
model/churro-3B/ the CHURRO-3B weights, copied once from Hugging Face (~7.5 GB)
The RAG and fine-tuning episodes have
shared_bucket = "ml-marathon-shared" filled in. Stage under
that name and there is nothing to tell learners; stage under another and
they change that line in the first cell.
Where to run this
Everything below runs in CloudShell (console toolbar, with the region set to the class region), where you act as your own SSO identity. It cannot be done from a notebook instance: the learner execution role there can create a bucket but is refused on tagging, the bucket policy and every upload, because the policy in step 2 deliberately gives it read-only access.
Paste the blocks in order in one CloudShell tab, so the variables set
in step 1 are still there for the later steps. CloudShell’s home
directory is 1 GB, which is why the data steps work one asset at a time
under /tmp and the model is streamed rather than
downloaded. The whole thing takes about ten minutes; a run on 2026-10-20
ended with 461 objects and 7.7 GB in the bucket.
1. Create the bucket
Same naming rules as the S3 episode, in the class region, with the
three cost-allocation tags, and all public access blocked. The account
ID is read from your identity; the echo line is there so
you can check it is the class account before anything is created.
BASH
BUCKET=ml-marathon-shared
REGION=us-east-2
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo $ACCOUNT_ID
aws s3api create-bucket --bucket $BUCKET --region $REGION --create-bucket-configuration LocationConstraint=$REGION
aws s3api put-bucket-tagging --bucket $BUCKET --tagging 'TagSet=[{Key=Project,Value=Workshop},{Key=Name,Value=Everyone},{Key=Purpose,Value=Storage}]'
aws s3api put-public-access-block --bucket $BUCKET --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
If the bucket already exists (created in the console, or left from
last term), create-bucket reports
BucketAlreadyOwnedByYou and nothing else changes; the other
two lines are safe to re-run.
2. Make it readable by the learner roles
Learners reach the bucket through the execution role their notebook
instance and jobs assume: ml-sagemaker-use, which the
episodes tell them to pick, or ml-sagemaker-bedrock-use, an
equivalent role from an earlier term. This policy lets both read and
list, and nothing else. s3:ListBucket is required because a
training job’s input channel lists the prefix before downloading it. No
PutObject: learners never write here.
BASH
cat > /tmp/policy.json <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::$ACCOUNT_ID:role/ml-sagemaker-use",
"arn:aws:iam::$ACCOUNT_ID:role/ml-sagemaker-bedrock-use"
]
},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::$BUCKET", "arn:aws:s3:::$BUCKET/*"]
}
]
}
JSON
aws s3api put-bucket-policy --bucket $BUCKET --policy file:///tmp/policy.json
aws s3api get-bucket-policy --bucket $BUCKET --output text
The same JSON can be pasted into the console instead (bucket →
Permissions → Bucket policy), with the account ID and bucket name
written in. MalformedPolicy: Invalid principal means an ARN
in the Principal list does not exist: a wrong account ID,
or a role name that differs from what IAM has
(aws iam list-roles --query 'Roles[].RoleName' lists
them).
3. Stage WattBot (RAG episodes)
Clone the lesson repository, build the corpus from the data it ships, and sync:
BASH
rm -rf /tmp/stage && mkdir -p /tmp/stage && cd /tmp/stage
pip install -q pandas requests pypdf huggingface_hub
git clone -q --depth 1 https://github.com/carpentries-incubator/ML_with_AWS_SageMaker.git lesson
unzip -q -o lesson/data/data.zip -d data
mkdir -p wattbot
cp data/data/wattbot/metadata.csv data/data/wattbot/train_QA.csv wattbot/
python3 lesson/scripts/build_wattbot_corpus.py --metadata wattbot/metadata.csv --out wattbot/corpus.zip
aws s3 sync wattbot/ s3://$BUCKET/wattbot/
The corpus script downloads every PDF listed in
metadata.csv, reports any URL that failed, then extracts
every page’s text into pages.jsonl so the Bedrock episode
loads records in seconds instead of running pypdf for
minutes on every notebook. Decide whether to drop or fix failed URLs
before staging. The corpus only needs rebuilding when
metadata.csv changes. To add pages.jsonl to a
bucket staged before the script wrote it, without re-downloading the
PDFs:
BASH
rm -rf /tmp/pages && mkdir -p /tmp/pages && cd /tmp/pages
git clone -q --depth 1 https://github.com/carpentries-incubator/ML_with_AWS_SageMaker.git lesson
pip install -q pypdf pandas
mkdir -p wattbot
aws s3 cp s3://$BUCKET/wattbot/corpus.zip wattbot/corpus.zip
aws s3 cp s3://$BUCKET/wattbot/metadata.csv wattbot/metadata.csv
python3 lesson/scripts/build_wattbot_corpus.py --metadata wattbot/metadata.csv --from-zip wattbot/corpus.zip
aws s3 cp wattbot/pages.jsonl s3://$BUCKET/wattbot/pages.jsonl
Needed for class: the 2026 corpus is about 3 million tokens and the
Titan Text Embeddings V2 quota is 300,000 tokens per minute and not
adjustable, so one notebook needs ten minutes to embed it and a room of
them would take hours. After you have run the episode once yourself with
use_instructor_embeddings = False, copy its embedding
matrix into the shared bucket; with the default True, the
episode downloads it and skips the calls. The notebook checks that the
matrix has one row per chunk and ignores it otherwise, so a copy from an
older corpus falls through to computing rather than scoring against the
wrong passages. The chunks are deterministic from
pages.jsonl, so the vectors match. Learners working on
their own embed the corpus themselves, which is the point of the
step.
The Processing-jobs episode’s first job embeds the same chunks with
gte-large on a GPU, eight minutes per learner. After your
own run, copy its output into the shared bucket and the episode’s
use_instructor_embeddings switch copies it into each
learner’s bucket in place of the job; they still launch the second,
larger job themselves:
BASH
aws s3 cp s3://<your-bucket>/wattbot/embeddings/embeddings.npy s3://ml-marathon-shared/wattbot/embeddings_gte_large.npy
The multimodal step at the end of the Bedrock episode has the same
shape: cutting the tables and figures out of the PDFs is a few minutes
of CPU per notebook, and embedding them is a thousand Bedrock calls.
After your own run of that step, stage both from the notebook’s
wattbot folder, and the episode downloads them when
use_instructor_embeddings is set:
BASH
cd /home/ec2-user/SageMaker/wattbot/data && zip -qr crops.zip crops crops.jsonl
aws s3 cp crops.zip s3://ml-marathon-shared/wattbot/crops.zip
aws s3 cp embeddings_crops_multimodal.npy s3://ml-marathon-shared/wattbot/embeddings_crops_multimodal.npy
The crop records carry paths relative to the notebook’s working
folder (data/crops/...), so the zip must be unpacked where
the episode unpacks it; the cell does that.
The three vision runs at the end of that step are twelve minutes of
Bedrock calls. The cell uploads its three result files to your bucket
under wattbot/solutions/; copy them across and the class
downloads and scores them:
BASH
aws s3 cp s3://<your-bucket>/wattbot/solutions/ s3://ml-marathon-shared/wattbot/solutions/ --recursive --exclude "*" --include "wattbot_solutions_vision_*.csv"
BASH
aws s3 cp s3://<your-bucket>/wattbot/embeddings/embeddings_bedrock.npy s3://$BUCKET/wattbot/embeddings_bedrock.npy
If the room stalls, have learners run this one line before the embedding cell; the cell then finds the file locally and skips the calls:
Redo the copy whenever pages.jsonl or the chunking
settings change.
4. Stage Badger Scribe (fine-tuning episode)
The data comes from the lesson’s own GitHub release (see Badger Scribe data: publishing a release for how versions are published). The loop handles one asset at a time: download, unzip, sync, delete, so nothing larger than one zip is ever on disk.
BASH
V=1 # Badger Scribe data release version
R=https://github.com/carpentries-incubator/ML_with_AWS_SageMaker/releases/download/badger-scribe-data-v$V
for a in labels images-train images-test; do
rm -rf /tmp/stage/bs && mkdir -p /tmp/stage/bs
curl -sSL -o /tmp/stage/bs/$a.zip $R/badger-scribe-v$V-$a.zip
unzip -q -o /tmp/stage/bs/$a.zip -d /tmp/stage/bs/badger-scribe && rm /tmp/stage/bs/$a.zip
aws s3 sync /tmp/stage/bs/badger-scribe/ s3://$BUCKET/badger-scribe/
rm -rf /tmp/stage/bs
done
5. Stage the CHURRO-3B weights (fine-tuning episode)
Every training and processing job in the episode reads the base model
from the bucket as a model channel, so that 100 jobs
starting in the same minute copy it from S3 instead of each pulling 7.5
GB from Hugging Face. This streams each file from Hugging Face straight
into S3, nothing written to disk: about 7.5 GB, which took under two
minutes from CloudShell.
BASH
python3 - <<EOF
import boto3, requests
from huggingface_hub import HfApi, hf_hub_url
s3 = boto3.client("s3")
bucket, model_id, prefix = "$BUCKET", "stanford-oval/churro-3B", "badger-scribe/model/churro-3B"
for f in [f for f in HfApi().list_repo_files(model_id) if not f.startswith(".")]:
with requests.get(hf_hub_url(model_id, f), stream=True, timeout=60) as r:
r.raise_for_status(); r.raw.decode_content = True
s3.upload_fileobj(r.raw, bucket, f"{prefix}/{f}")
print("uploaded", f)
EOF
On a machine with 10 GB free, the plain download works too:
BASH
huggingface-cli download stanford-oval/churro-3B --local-dir churro-3B
aws s3 sync churro-3B/ s3://$BUCKET/badger-scribe/model/churro-3B/
rm -rf churro-3B
It only needs redoing if the Hub repository changes or the bucket is rebuilt.
6. Check, then clean up
BASH
aws s3 ls s3://$BUCKET/ --recursive --summarize | tail -2
aws s3 ls s3://$BUCKET/wattbot/
aws s3 ls s3://$BUCKET/badger-scribe/
aws s3 ls s3://$BUCKET/badger-scribe/model/churro-3B/
cd / && rm -rf /tmp/stage
Expect three files under wattbot/, the CSVs plus
images/ under badger-scribe/, and the config,
tokenizer and safetensors files under model/churro-3B/.
7. Keep it, or rebuild it
The end-of-workshop cleanup
(instructors/instructor-notes.md, aws-nuke) deletes every
S3 bucket in the account unless excluded. The committed
nuke-config.yml excludes ml-marathon-shared;
if you named the bucket differently, change the filter there or accept
that it goes and redo steps 1 to 6 next term. Storage is ~130 MB for
Badger Scribe plus the WattBot corpus, and ~7.5 GB for the model copy:
well under a dollar a month.
Self-paced learners
Anyone working outside the shared account does steps 1, 4 and 5 (and
3 for the RAG episodes) into a bucket of their own and sets
shared_bucket to its name. The episodes say so where it
matters.