Model Training and Evaluation¶
This lab provides an introduction to training, modifying, and evaluating machine learning models for radar perception, using a Generalizable Radar Transformer (GRT) reference implementation. In this lab, you will:
- Get access to GPUs via an HPC system, and run cluster jobs
- Set up a model training environment
- Replicate the GRT baselines, and measure how performance scales with the size of the training set
- Propose and implement a change to the model or the training methodology
- Run an ablation to evaluate your change against the baselines
Report Template
Please use this template for your lab report.
Revision History
| Date | Description |
|---|---|
| 2026-09-06 | Initial draft |
| 2026-09-12 | Added the evaluation section, report template, and rubric |
| 2026-09-15 | Officially released and assigned |
Hello World, HPC!¶
None of the models in this lab (and, most likely, in your course project) can be trained on a laptop. Instead, they require access to accelerators such as GPUs, ideally in a high-performance computing (HPC) environment.
In this checkpoint (Due 09/22), you should verify that you are able to log into PSC, use our allocation, and submit a hello_world.sh slurm job.
Bring Your Own Compute
You are welcome (and encouraged) to use your own (≥16GB VRAM) GPU instead of the PSC allocation, provided you also have at least 2TB of free storage. If you elect to do so, you may skip this checkpoint. Instead, submit the following:
- A screenshot of your compute environment and accelerators (e.g.,
nvidia-smi,scontrol show nodes, etc.). -
Download the I/Q-1M dataset using these instructions, and submit a screenshot showing that you have downloaded the dataset to your computing environment.
Note that you only need the radar and lidar range files; you can
--exclude */lidar/rfl */lidar/nir */_camera/video.aviso that the dataset only consumes ~2TB of storage.
On the flip side, if you are planning to use your own compute, you are also welcome (and encouraged) to follow these PSC setup steps regardless in case you need to fall back to the PSC allocation.
- Your GPU should have at least 16GB of VRAM, though it is possible to use GPUs with less memory with aggressive memory management (e.g., gradient accumulation); note that we will not help you do this.
- V100s will be very slow, but usable.
- You may also use TPUs, Trainium, Apple Silicon, etc.; however, software support may vary, and we will not provide technical support.
Provided that you have created an NSF Access account and submitted your username to the canvas assignment, you should have been added to the course allocation ele260011p on PSC Bridges-2:

Log in to PSC¶
Follow the instructions in the PSC Bridges-2 user guide to log in to the cluster.
- I highly recommend registering your public key; see the instructions under "Public-private keys".
- Once you log in, you should see our course allocation displayed (similar to the screenshot above); you can also pull this up at any time using the
projectscommand.
VS Code Remote Editor on PSC
PSC supports running VS Code directly on a compute node, which is far more pleasant than editing over ssh. This is not required, but we strongly recommend setting it up now, before you need it; see the remote IDE instructions in the user guide.
Submit a job¶
Following the sbatch instructions in the PSC user guide, create a "hello world" script which runs the following commands:
echo "Hello World, HPC!"
echo
echo "job id: ${SLURM_JOB_ID}"
echo "partition: ${SLURM_JOB_PARTITION}"
echo "account: ${SLURM_JOB_ACCOUNT}"
echo "node: $(hostname)"
echo "user: $(whoami)"
echo "date: $(date)"
Your script should also do the following:
- Set a reasonable job name (e.g.,
hello_world). - Use the
RM-smallpartition, and be billed to our course allocationele260011p. - Save the output to a log file (e.g.,
hello_world.%j.log)
Once you create your script, run it with
Sample Output
You should get an output that looks something like this:
[tianshu2@bridges2-login013 ~]$ cat hello_world.45378265.log
Hello World, HPC!
job id: 45378265
partition: RM-small
account: ele260011p
node: r001.ib.bridges2.psc.edu
user: tianshu2
date: Sun Sep 6 17:04:25 EDT 2026
Note that if the script is not routed through slurm via sbatch, you will instead see:
Once you are done, submit a screenshot of the projects output and the log file from your hello_world.sh job to the Lab 2 Checkpoint assignment on canvas.
Set up the Training Environment¶
Using your compute environment, you should now set up the training environment for this lab. Unless noted otherwise, these steps are the same whether you are on PSC or on your own machine.
-
Install
uv. uv is a modern python version and package manager which is open source, extremely fast, built to a high quality standard, and, for better or worse, now fully owned by OpenAI as a loss leader. -
Clone the repository, and make it your own. Create a new, empty, private repository under your personal GitHub account, then point your clone at it and push:
git clone https://github.com/RadarML/18848-lab2.git --recursive cd 18848-lab2 git remote set-url origin git@github.com:<your-username>/18848-lab2.git git push -u origin mainWarning
Make sure that your repository is private!
-
Install the dependencies. Run
uv syncin the repository root.Warning
This downloads several GB of packages, and can be painfully slow on a login node. If it is, run it from an interactive session instead:
-
Install the pre-commit hooks. This will run a number of checks on your code before each commit, helping catch errors early.
-
Link the dataset. The repository looks for the dataset at
./databy default.-
On PSC, point that at the shared copy (i.e., create a symlink):
Info
Note that the dataset lives under
ele250004p, which is not our course allocation. This is intended — the data is shared from another project, and you only need read access to it. -
On your own machine, point it to wherever you downloaded the dataset. See above, as well as the i/q-1m documentation for instructions on downloading the dataset.
It is also possible to simply pass
meta.dataset=/full/path/to/dataseteach time you runtrain.pyand--data_root=/full/path/to/dataseteach time you runevaluate.py, but it's much more convenient to use a symlink. -
Verify your Installation¶
Start an interactive session (i.e., interact -A ele260011p --ntasks-per-node=16), and run a "dry-run" of the training script from the interactive session:
- When you run
train.pywithmeta.dry_run=inst, this will instantiate all of the components without actually loading any data or running any training, allowing you to easily verify that all dependencies are installed and importable, and that there are no immediate errors during instantiation. - This process does load metadata though, providing a useful sanity check of the entire data pipeline configuration.
Sample Output
[tianshu2@r002 18848-lab2]$ uv run train.py meta.name=debug meta.version=v0 meta.dry_run=inst
Using bfloat16 Automatic Mixed Precision (AMP)
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
[09/06/26 17:35:33] INFO train: Dry run (inst): instantiation succeeded; skipping training.
Explore the Code Base¶
Before you start spending GPU hours, take some time to read through the repository — and the documentation of the libraries it builds on.
Code Structure
You may have noticed that very little of the code directly lives in this repository. Instead, the broader RadarML code base is split across several libraries:
Types
The code base is fully typed, including jaxtyping shape and dtype annotations, and passes both static type checking with pyright and runtime type checking with beartype. Both run on every commit via pre-commit, and your change will be checked too.
Software Architecture
The code base is built around dependency injection, which is a software design pattern which allows writing modular and composable code by defining shared interfaces, independently implementing components which use these interfaces, and composing together a program from these components. This code base uses hydra as the composition framework, along with a suite of interfaces defined by pytorch lightning, the abstract-dataloader, and the Neural Radar Development Kit.
This means that instead of modifying the code directly to add new functionality, you should create new components (e.g., classes) which implement the functionality you want (but using the same interfaces). You should then create configuration files which reference your components, and finally run training and evaluation using your configurations.
Configuration
The training infrastructure is based on hydra for configuration management and pytorch lightning for training, and is built around lightning's abstractions and interfaces: LightningModule, LightningDataModule, Trainer, and so on. Each of these is described by a hydra config which names a python class (_target_) and its arguments, and is instantiated at startup.
config/default.yaml composes a GRT baseline model from config groups. Group selections and individual values can both be overridden from the command line:
Running Experiments
The main entry point for this code base is train.py for training, and evaluate.py for evaluation. To see how these work, in addition to reading through the code, you can run uv run train.py --help and uv run evaluate.py --help for help text.
However, since we are using a HPC cluster, you will not (and should not!) run these scripts directly. Instead, you will need to create and submit slurm jobs by creating train.sh and test.sh scripts, and submitting them to the system with sbatch (while billing to our course allocation ele260011p).
To automate and streamline this process, we also provide a _slurm/submit.py system which you may use to automate these steps by automatically creating and submitting both a training job and a dependent evaluation job; see _slurm/README.md for details.
torch.compile does not work with jaxtyping
When running experiments, you should use torch.compile to speed up training. However, pytorch has a buggy and inconsistent type system, which prevents jaxtyping from working properly with torch.compile. As such, we must disable it with JAXTYPING_DISABLE=1 in our training scripts.
Replicate the Baseline¶
With the environment working, reproduce the GRT baseline by training a model for 3D occupancy prediction, and measure how its performance scales with the size of the training set.
One deviation from the GRT paper
config/default.yaml sets up a baseline GRT model, with one deliberate change: the complex spectrum is presented to the model as magnitude, sine, and cosine (xwr.nn.PhaseVec) instead of magnitude and phase angle (xwr.nn.PhaseAngle), which avoids a discontinuity in the phase angle representation as it wraps around from +π to −π.
Splits: I/Q-1M is split at the recording level: separate recordings, representing around 20% of the data, are held out as the test set. Within the training set, 20% of each recording is then held out for validation.
The split config group subsamples the 80% training set, with +split=p10, +split=p20, +split=p50, and +split=p100 training on 10%, 20%, 50%, and 100% of the training set respectively.
Note
Since 20% of the non-test recordings are held out for validation, the effective training set sizes are 8%, 16%, 40%, and 80% of the full dataset.
Runs: Train a model for each split, holding everything else fixed:
uv run _slurm/submit.py baseline p100 --env.gres=gpu:h100:2 --args "+split=p100"
uv run _slurm/submit.py baseline p50 --env.gres=gpu:h100:2 --args "+split=p50"
uv run _slurm/submit.py baseline p20 --env.gres=gpu:h100:2 --args "+split=p20"
uv run _slurm/submit.py baseline p10 --env.gres=gpu:h100:2 --args "+split=p10"
This will train every model "to completion": training ends on its own when the validation loss stops improving (EarlyStopping with meta.patience=3), and should take around 5 H100-hours for a single full-size (p100) run.
Example Submission Script
It may be helpful to create a submission script which runs all four splits in sequence:
#!/bin/bash
job() {
local split=$1
uv run _slurm/submit.py baseline $split \
--env.gres=gpu:h100:2 \
--args \
+split=$split;
}
for split in p100 p50 p20 p10; do
job $split;
done
Also, note that the splits are run in reverse order of size so that the larger runs start first to (at least on average) decrease the overall wall time for the entire sweep.
Scaling curve: evaluate.py writes per-sample metrics to a metrics.npz for each test trace, under results/baseline/<version>/eval/<trace>/; the test loss is stored under the loss key.
Average the test losses across all test samples, and report these results in the table in your lab report. Note that this is a pooled mean across every sample from every test trace, not an average of the per-trace means; since the traces have different lengths, these are not the same. Then, plot a simple scaling curve of log(test loss) against log(training set size) (i.e., with ax.set_yscale("log") and ax.set_xscale("log") in matplotlib). You do not need to submit this plot, as the baseline results will be referenced later.
Propose an Idea¶
With a baseline in hand, it's time to change something. Propose one modification to the model or the training methodology, and implement it.
-
Propose a change. You can reference the list of ideas below. Then, in your report, state precisely what you are changing, and argue why it could improve the model's performance. Finally, provide a high level description of how you will implement your change.
-
Implement your change. Write your implementation in this repository.
Where does my code go?
The code base is assembled by dependency injection, so a new component is a new class plus a config that points at it; there is never a need to edit a library in place.
So, if your change is a variation on an existing component, copy it and edit the copy:
-
Add your implementation under
grt/. For example,grt/tokenizer.pyholdsSpectrumTokenizer, the baseline tokenizer; a convolutional tokenizer could go in a newgrt/conv_tokenizer.py. You are welcome to copy code out ofnrdkand/or other libraries as a starting point. -
Add a config which selects it. Create a YAML file in the matching group under
config/, with_target_pointing at your class and its arguments below:config/model/tokenizer/conv.yaml_target_: grt.conv_tokenizer.ConvTokenizer d_model: ${size.d_model} ...Your change is then selectable as an ordinary override:
If your change does not fit an existing group (
model/tokenizer,model/encoder,model/decoder,objective, ...), feel free to add a new group and/or a new key toconfig/default.yaml. -
Leave the baseline selectable. Your change must be an alternative to the baseline component, not a replacement for it:
uv run train.pywith no overrides must still compose and train the original GRT baseline. This is what makes the ablation an ablation.
-
-
Test your change. Write unit tests for your implementation, and verify that they pass.
Your code must also include type annotations on all parameters and return values, pass type checking, and follow the specified code formatting rules as run by the pre-commit hooks and github actions CI.
Automated Quality Checks
- Code Formatting: Complex code bases in industry and open-source generally use a code formatter to enforce a consistent style. This code base is no exception; your code should pass a
ruffcheck with the provided rules (seepyproject.toml). - Static Type Checking: Like the rest of the code base, your code should be annotated with data types. This allows for a static type checker to catch many bugs before you even run your code.
- Unit Tests: The
tests/directory contains unit tests for the code base, includingtest_configs.py, which checks that every config inconfig/composes and instantiates correctly. In addition to adding tests for your own code, you are encouraged to run the tests to verify that your code is (mostly) bug-free before submitting jobs to the queue: - Pre-commit Hooks: Using pre-commit, the above checks are run automatically every time you commit your code (and you will not be able to commit if they fail). You can also run them manually at any time with:
- Continuous Integration: Using github actions, the above checks are also run automatically on every push to your repository. If the checks pass, you will see a green checkmark next to your commit; if they fail, you will see a red X. You can click on the checkmark or X to see the details of the checks.
- Code Formatting: Complex code bases in industry and open-source generally use a code formatter to enforce a consistent style. This code base is no exception; your code should pass a
Experiment Ideas¶
You are free to explore any of the ideas here, or even come up with your own!
Experiment Difficulty
No additional credit will be given for more difficult and complex ideas, though a nontrivial change (i.e., involving more than a configuration edit) is required for full credit.
Proposed changes that merely tweak existing hyperparameters will only receive partial credit.
Patch Size
Adjust the patch size to change its aspect ratio. How does this affect the model's performance?
Angular Spectrum Size
The GRT model takes a 2x8 elevation-azimuth angular spectrum as input. However, we know that we can zero-pad the spectrum to add more azimuth and elevation bins. While this does not actually provide any new angular information, does this help the model?
These should be fairly easy even for students without substantial ML background.
Dynamic Patching
Instead of using a fixed patch size and window, dynamically adjust the patch resolution based on the input signal. For example, you could use a smaller patch size, but only keep the top 2048 tokens by some criterion (e.g., power, CFAR power vs noise estimate, ...)
Range-Adaptive Patching
GRT uses the same patch size across the range-Doppler spectrum, even though further range-Doppler bins correspond to more area. Can we use smaller patches for distant bins to get better performance while keeping the total number of patches constant (2048)?
These ideas require a stronger grasp of machine learning concepts and architectures.
Positional Encoding
GRT uses a simple multi-dimensional sinusoidal positional encoding, which is now quite outdated for modern foundation models across both language and vision. Can we replace this with something better?
Convolutional Tokenizer
Instead of a simple linear projection of the input patches, use a convolutional neural network to better extract features before feeding them into the transformer.
Velocity Token
The I/Q-1M dataset includes ego-velocity estimates. Does passing this information to the model, perhaps using a special "velocity token", provide any benefit?
Attempt these at your own risk.
Novel Architectures
The GRT model uses an off-the-shelf transformer encoder and decoder. What if we replace the transformer encoder and/or decoder with a completely new architecture such as a state-space model?
A Better Loss Function
GRT uses a binary-cross-entropy loss for 3D occupancy. To handle the fact that there are more unoccupied voxels than occupied ones, it simply re-weights the loss to give more importance to the occupied voxels. Is there a better way to design a loss function?
Propose Your Own Idea
If you have an idea, try it!
As long as it's rigorously evaluated, there's no penalty for failure.
Evaluate Your Idea¶
Once your change is implemented and selectable from the config, repeat the scaling experiment with it, training one model on each of p10, p20, p50, and p100 with your change enabled and everything else held fixed. Use one name for all four runs, and keep the split as the version.
Compute Error Bars¶
Since each model is evaluated on a finite test set, a difference between your change and the baseline may be simply due to random chance. To gain some insight into the chances that your change is actually better, compute error bars on your estimates, and report these in the table in your lab report.
- You should use a paired comparison where each test sample from your run is compared against the same test sample from the baseline.
- These comparisons should be run independently for each data split.
Use nrdk.tss to calculate these. This library computes estimates of the effective sample size of the evaluation traces; while the statistical details of how this library works are out of scope for this course, you can read the documentation for more information.
Using nrdk.tss
Index every metrics.npz under results/, pair each run against the baseline trained on the same split, and compute statistics for all eight runs at once:
from nrdk import tss
# Each `<name>/<version>` becomes an experiment, e.g. `baseline/p10`.
index = tss.index(
"results", r"^(?P<experiment>.*)/eval/(?P<trace>.*)/metrics\.npz$")
# Pair each run against the baseline trained on the same split.
split = tss.Control.from_index_rule(
"split", index, lambda e: f"baseline/{e.rsplit('/', 1)[1]}")
df = tss.dataframe_from_index(
index, key="loss", baseline="baseline/p100", controls=[split])
Each row of df is one run, and the columns come in families, distinguished by their prefix. Each family describes a different quantity:
abs/*: the metric itself, for that run.rel/*: the paired difference between that run and the single globalbaseline=run — here,baseline/p100. Since this compares every run against a model trained on the full training set, it is not the comparison you want.rel_split/*: the paired difference between that run and the baseline trained on the same split, which is what theControladds. This is the comparison you want. Had you named the control something else, the prefix would change to match.pct/*andpct_split/*: the same differences, expressed as a percentage of the baseline's mean.p0.05andp0.05_split:Truewhere the corresponding difference is significant at the 5% level (two-sided).
Within the abs/, rel/, and rel_split/ families, the suffix selects a statistic (the pct/* families only provide mean and stderr):
mean: the average over all test samples — either of the metric (abs/mean), or of the per-sample difference from the baseline (rel_split/mean).std: the sample standard deviation.n: the number of test samples.ess: the effective sample size.stderr: the standard error of the mean, computed asstd / sqrt(ess).
Draw Scaling Plot¶
Plot both scaling curves together, and include this plot in your report. Your plot should:
- Use log-log axes.
- Show the test loss on the y axis, and the training set size on the x axis. It's fine to use relative training set sizes (i.e., 0.1, 0.2, 0.5, 1.0).
- Include both the baseline and your method.
- Show error bars on the points for your method, where the width of the error bar denotes (two-sided) 95% confidence intervals for the difference to the baseline using your estimates of the effective sample size-corrected standard error.
Example Plot
This is an example of a scaling plot comparing three methods which are not significantly different from each other.

Analysis Questions¶
First answer the following conceptual questions:
-
Why can't we compute error bars directly using the standard error,
std(X) / sqrt(n)? What will happen if we try to do this? -
Why do we need to do a paired comparison? What would happen if we instead computed the error bars for each method independently, and then compared the two?
Note
If you're not sure how to answer these questions from statistical principles, try redrawing your plot according to these alternate methods, and see what happens!
Then, answer the following questions about your proposed change:
-
Is your change significantly better? Is your change better, worse, or statistically indistinguishable from the baseline across the different training set sizes?
-
Does your change scale? Does the gap between the two curves hold, widen, or shrink as the training set grows?
-
As a whole, do you believe your change is an improvement? If so, why? If not, why not?
Finally, reflect on the procedure as a whole:
-
Do you think the evaluation set is the right size? Using your results and/or training curves, argue for whether the evaluation set is too small, too large, or about right.
-
Do you think the validation set is the right size? Using your results and/or training curves, argue for whether 20% is too small, too large, or about right.
Grading and Submission¶
Submission Checklist¶
Your submission should include the following:
- Your checkpoint submission (in the separate Canvas assignment): the
projectsoutput (ornvidia-smi, if you are using your own compute), and yourhello_world.shjob log - Your lab report, including a description of your proposed change, justification for why this could improve the model's performance, a high level description of your implementation, the scaling plot, and your answers to the analysis questions
- A
.taror.ziparchive of your entire repository, including the.gitdirectory, but omittingresults/,data/,.venv/, and any other untracked artifacts
Rubric¶
Warning
Submission of the lab report and code are prerequisites to receiving credit for any component of this lab!
This lab is worth 100 points, divided across five components:
| Component | Points |
|---|---|
| Hello World, HPC! | 5 |
| Baseline replication | 20 |
| Proposed change | 30 |
| Scaling law ablation | 20 |
| Analysis and discussion | 25 |
-
Hello World, HPC! (5 points)
- (2 points)
projectsoutput on Bridges-2, ornvidia-smion your own compute - (3 points) A
hello_world.shjob runs, with its log included; or, if you are using your own compute, the I/Q-1M dataset is downloaded
- (2 points)
-
Baseline replication (20 points)
- (20 points) Four baseline runs (
+split=p10,p20,p50,p100) are reported in your scaling law ablation table and plot (4)
- (20 points) Four baseline runs (
-
Proposed change (30 points)
- (10 points) The change is clearly specified, and a reasonable argument is made for why it could improve the model's performance
- (10 points) The implementation and configuration files for the change
- (5 points) Unit tests are provided
- (5 points) Automated quality checks (formatting, type checking, and CI)
Partial Credit
Credit for (b), (c), and (d) can only be given if the proposed change involves some substantive code addition or modification (i.e., involves more than a hyperparameter change).
-
Scaling law ablation (20 points)
- (10 points) Four experiment runs
- (5 points) Error bars are computed as a paired comparison, independently for each split
- (5 points) The scaling plot is presented, and meets the stated requirements
-
Analysis and discussion (25 points)
- (10 points) Conceptual questions
- (10 points) Analysis of the proposed change
- (5 points) Data size discussion