I have found a workaround (pseudo-code):
float* buf = (float*) malloc(bufSize);
for (int i = 0; i < 20; i++) {
    // input is a CUDA tensor
    auto res = model->forward(input);
    auto resCPU = res.cpu();
    memcpy(buf, resCPU.accessor<float, 3>().data(), bufSize);
    // resCPU goes out of scope here, so presumably some or other reference count
    // is also release, so that the CUDA memory involved in the model.forward() can now
    // be released.
}

The key difference here is that there is no long-lived object that is the destination of a CUDA -> CPU copy. In my original example, the ‘out’ object seems to cause references to the CUDA memory to never be released.
Is this intended behaviour?