My code was just meant to be simple (and thus non functional),

I see! Back to the original Q: No you wouldn’t need to return an output variable inside the
    def forward(self, x):
        out = self.linear_1(x)
        out = F.relu(out)
        out = self.linear_2(out)
        out = F.relu(out)
        cost_fn = torch.nn.CrossEntropyLoss()
        self.cost = cost_fn(self.linear_out(out), targets)
        return None

and then the following training loop:
for epoch in range(num_epochs):
    for batch_idx, (features, targets) in enumerate(train_loader):
        features = features.to(device)
        targets = targets.to(device)
            
        ### FORWARD AND BACK PROP
        model(features)
        optimizer.zero_grad()
        model.cost.backward()
        
        ### UPDATE MODEL PARAMETERS
        optimizer.step()

you could even do sth like this and it would work (i.e., the code will run):
    def forward(self, x):
        self.cost = torch.tensor(99., requires_grad=True)
        return None

...

        ### FORWARD AND BACK PROP
        model(features)
        optimizer.zero_grad()
        model.cost.backward()