```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

_cpp_source = r"""
#include <torch/extension.h>

torch::Tensor hinge_loss_cuda(torch::Tensor predictions, torch::Tensor targets);
"""

_cuda_source = r"""
#include <torch/extension.h>
#include <ATen/ExpandUtils.h>
#include <ATen/cuda/CUDAContext.h>
#include <c10/cuda/CUDAGuard.h>
#include <c10/cuda/CUDAException.h>
#include <cuda_runtime.h>
#include <algorithm>
#include <cstdint>

__device__ __forceinline__ float block_sum(float value) {
    __shared__ float warp_sums[8];
    const int lane = threadIdx.x & 31;
    const int warp = threadIdx.x >> 5;

    #pragma unroll
    for (int offset = 16; offset > 0; offset >>= 1)
        value += __shfl_down_sync(0xffffffff, value, offset);

    if (lane == 0)
        warp_sums[warp] = value;
    __syncthreads();

    value = threadIdx.x < 8 ? warp_sums[lane] : 0.0f;
    if (warp == 0) {
        #pragma unroll
        for (int offset = 16; offset > 0; offset >>= 1)
            value += __shfl_down_sync(0xffffffff, value, offset);
    }
    return value;
}

__global__ void hinge_partial_kernel(
    const float* __restrict__ predictions,
    const float* __restrict__ targets,
    float* __restrict__ output,
    int64_t n,
    bool scalar_prediction,
    bool scalar_target
) {
    float sum = 0.0f;
    const int64_t step = static_cast<int64_t>(blockDim.x) * gridDim.x;

    for (int64_t i = static_cast<int64_t>(blockIdx.x) * blockDim.x
                     + threadIdx.x;
         i < n;
         i += step) {
        const float p = predictions[scalar_prediction ? 0 : i];
        const float t = targets[scalar_target ? 0 : i];
        const float loss = 1.0f - p * t;
        sum += loss < 0.0f ? 0.0f : loss;
    }

    sum = block_sum(sum);
    if (threadIdx.x == 0)
        output[blockIdx.x] = gridDim.x == 1
            ? sum / static_cast<float>(n)
            : sum;
}

__global__ void hinge_finish_kernel(
    const float* __restrict__ partial,
    float* __restrict__ output,
    int count,
    int64_t n
) {
    float sum = 0.0f;
    for (int i = threadIdx.x; i < count; i += blockDim.x)
        sum += partial[i];

    sum = block_sum(sum);
    if (threadIdx.x == 0)
        output[0] = sum / static_cast<float>(n);
}

torch::Tensor hinge_loss_cuda(
    torch::Tensor predictions,
    torch::Tensor targets
) {
    TORCH_CHECK(predictions.is_cuda() && targets.is_cuda(),
                "Inputs must be CUDA tensors");
    TORCH_CHECK(predictions.device() == targets.device(),
                "Inputs must be on the same device");
    TORCH_CHECK(predictions.scalar_type() == at::kFloat &&
                targets.scalar_type() == at::kFloat,
                "Inputs must have float32 dtype");

    const c10::cuda::CUDAGuard guard(predictions.device());
    const auto shape = at::infer_size(predictions.sizes(), targets.sizes());

    const bool scalar_prediction = predictions.numel() == 1;
    const bool scalar_target = targets.numel() == 1;

    auto p = scalar_prediction
        ? predictions.contiguous()
        : predictions.expand(shape).contiguous();
    auto t = scalar_target
        ? targets.contiguous()
        : targets.expand(shape).contiguous();

    int64_t n = 1;
    for (const auto size : shape)
        n *= size;

    auto output = torch::empty({}, predictions.options());
    const auto stream = at::cuda::getCurrentCUDAStream();

    if (n == 0) {
        output.fill_(NAN);
        return output;
    }

    constexpr int threads = 256;
    const int blocks = static_cast<int>(
        std::min<int64_t>((n + 1023) / 1024, 1024));

    auto partial = blocks == 1
        ? output
        : torch::empty({blocks}, predictions.options());

    hinge_partial_kernel<<<blocks, threads, 0, stream>>>(
        p.data_ptr<float>(),
        t.data_ptr<float>(),
        partial.data_ptr<float>(),
        n,
        scalar_prediction,
        scalar_target
    );
    C10_CUDA_KERNEL_LAUNCH_CHECK();

    if (blocks > 1) {
        hinge_finish_kernel<<<1, threads, 0, stream>>>(
            partial.data_ptr<float>(),
            output.data_ptr<float>(),
            blocks,
            n
        );
        C10_CUDA_KERNEL_LAUNCH_CHECK();
    }

    return output;
}
"""

_hinge_extension = load_inline(
    name="fused_hinge_loss_cuda",
    cpp_sources=_cpp_source,
    cuda_sources=_cuda_source,
    functions=["hinge_loss_cuda"],
    extra_cflags=["-O3"],
    extra_cuda_cflags=["-O3", "--fmad=false"],
    verbose=False,
)


class ModelNew(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, predictions, targets):
        if (
            predictions.is_cuda
            and targets.is_cuda
            and predictions.dtype == torch.float32
            and targets.dtype == torch.float32
            and not (
                torch.is_grad_enabled()
                and (predictions.requires_grad or targets.requires_grad)
            )
        ):
            return _hinge_extension.hinge_loss_cuda(predictions, targets)
        return torch.mean(torch.clamp(1 - predictions * targets, min=0))
```