The min and max operations return a tensor in my example, so no synchronizing item() operation is performed.
I used this code snippet:

batch_size = 3
height = 2
width = 2

A = torch.randint(2, 11, (batch_size, height, width)).float().cuda()
AA = A.clone()
print(A)

def fun1(A):
    for i in range(batch_size):
        A[i] -= torch.min(A[i])
        A[i] /= torch.max(A[i])

# Your solution
def fun2(AA):
    AA = AA.view(AA.size(0), -1)
    AA -= AA.min(1, keepdim=True)[0]
    AA /= AA.max(1, keepdim=True)[0]
    AA = AA.view(batch_size, height, width)


nb_iters = 100
torch.cuda.synchronize()
t0 = time.time()
for _ in range(nb_iters):
    fun1(A)
torch.cuda.synchronize()
t1 = time.time()
print((t1 - t0)/nb_iters)


torch.cuda.synchronize()
t0 = time.time()
for _ in range(nb_iters):
    fun2(AA)
torch.cuda.synchronize()
t1 = time.time()
print((t1 - t0)/nb_iters)

And get these results:

CPU: original: 9.6135e-05 sec/iter, mine: 2.3348e-05 s/iter

GPU: original: 0.0004302668 s/iter, mine: 7.0183e-05 s/iter


Note that the original workload is really small, so using:
batch_size = 300
height = 200
width = 200

I get:

CPU: 0.0646844482421875 s/iter vs. 0.01887556552886963 s/iter

GPU: 0.040595355033874514 s/iter vs. 0.0023879456520080567 s/iter