Fix the following code:
import numpy as np
import time
import pyopencl as cl

def sieve_simple(limit):
    """
    Perform a simple sieve of Eratosthenes up to 'limit' using bitmasking.
    Only considers odd numbers to optimize memory usage.
    Returns an array of prime numbers up to 'limit'.
    """
    sieve_size = (limit // 2) + 1  # Only odd numbers
    sieve = np.ones(sieve_size, dtype=np.uint8)
    sieve[0] = 0  # 1 is not prime

    sqrt_limit = int(np.sqrt(limit)) + 1
    for num in range(3, sqrt_limit, 2):
        if sieve[num // 2]:
            sieve[num*num // 2 : sieve_size : num] = 0

    primes = np.nonzero(sieve)[0] * 2 + 1
    return primes

def count_primes_with_gpu(n):
    """
    Count the number of primes up to 'n' using the Sieve of Eratosthenes with GPU acceleration and advanced optimizations.
    """
    # Step 1: Find small primes up to sqrt(n)
    sqrt_n = int(np.sqrt(n)) + 1
    small_primes = sieve_simple(sqrt_n)
    print(f"Number of small primes up to sqrt(n): {len(small_primes)}")

    # Step 2: Initialize OpenCL
    platforms = cl.get_platforms()
    if not platforms:
        raise RuntimeError("No OpenCL platforms found.")

    # Select the first platform with a GPU device
    device = None
    for platform in platforms:
        devices = platform.get_devices(device_type=cl.device_type.GPU)
        if devices:
            device = devices[0]
            break

    if device is None:
        raise RuntimeError("No GPU device found on any platform.")

    ctx = cl.Context([device])
    # Create a command queue with out-of-order execution and profiling enabled
    queue = cl.CommandQueue(ctx, properties=cl.command_queue_properties.OUT_OF_ORDER_EXEC_MODE_ENABLE |
                                         cl.command_queue_properties.PROFILING_ENABLE)

    # Step 3: Define the OpenCL kernel with optimizations
    kernel_code = """
    __kernel void sieve_of_eratosthenes(__global unsigned char *segment,
                                        __global const unsigned int *small_primes,
                                        const unsigned int num_small_primes,
                                        const unsigned int segment_start)
    {
        // Each work-item handles a unique multiple of a prime
        int gid = get_global_id(0);

        for (int i = 0; i < num_small_primes; i++) {
            unsigned int p = small_primes[i];
            unsigned int multiple = segment_start + gid * p;

            if (multiple < segment_start) continue;
            if (multiple % 2 == 0) multiple += p; // Ensure multiple is odd

            if (multiple >= segment_start && multiple < (segment_start + get_global_size(0) * p)) {
                unsigned int index = (multiple - segment_start) / 2;
                unsigned int byte_idx = index / 8;
                unsigned int bit_idx = index % 8;
                unsigned char mask = ~(1 << bit_idx);
                // Regular bitwise AND to mark as non-prime
                segment[byte_idx] &= mask;
            }
        }
    }
    """

    try:
        program = cl.Program(ctx, kernel_code).build()
    except cl.RuntimeError as e:
        # Fetch and print the build log for debugging
        build_log = e.build_log
        print("Build log:")
        print(build_log.decode())
        raise

    sieve_kernel = program.sieve_of_eratosthenes

    # Step 4: Segmented Sieve Parameters
    segment_size = 10_000_000  # 10 million numbers per segment
    # Each byte represents 8 odd numbers: 10_000_000 / 8 = 1,250,000 bytes
    num_segments = (n - sqrt_n + segment_size - 1) // segment_size
    print(f"Total segments to process: {num_segments}")

    # Step 5: Precompute a Bit-Reversal Lookup Table to align bits
    reverse_lookup = np.array([int('{:08b}'.format(i)[::-1], 2) for i in range(256)], dtype=np.uint8)

    # Step 6: Allocate Small Primes Buffer (Pinned Memory)
    small_primes_np = np.array(small_primes, dtype=np.uint32)
    small_primes_buffer = cl.Buffer(ctx, cl.mem_flags.READ_ONLY | cl.mem_flags.COPY_HOST_PTR, hostbuf=small_primes_np)

    # Step 7: Initialize Total Prime Count (including prime number 2)
    total_primes = len(small_primes) + 1  # Including prime number 2

    # Step 8: Process Each Segment
    for seg_idx in range(num_segments):
        low = sqrt_n + seg_idx * segment_size
        high = min(low + segment_size, n + 1)
        current_segment_size = high - low

        # Initialize the segment sieve: only odd numbers
        sieve_bits = (current_segment_size // 2) + (current_segment_size % 2)
        sieve_bytes = (sieve_bits + 7) // 8  # Ceiling division
        sieve = np.ones(sieve_bytes, dtype=np.uint8) * 0xFF  # Set all bits to 1

        # Clear bits for even numbers (all even numbers >2 are non-prime)
        for i in range(0, sieve_bits):
            num = low + i * 2 + 1
            if num % 2 == 0:
                sieve[i // 8] &= ~(1 << (i % 8))

        # Reverse bits for correct ordering
        reversed_sieve = reverse_lookup[sieve]

        # Create buffer for the segment (Pinned Memory)
        sieve_buffer = cl.Buffer(ctx, cl.mem_flags.READ_WRITE | cl.mem_flags.COPY_HOST_PTR, hostbuf=reversed_sieve)

        # Determine global and local work sizes
        # Each work-item handles a unique prime multiple
        global_work_size = (current_segment_size // 2 + small_primes.size - 1) // small_primes.size
        local_work_size = 64  # Align with wavefront size for AMD GPUs

        if global_work_size < local_work_size:
            global_work_size = local_work_size
        else:
            global_work_size = ((global_work_size + local_work_size - 1) // local_work_size) * local_work_size

        # Set kernel arguments
        sieve_kernel.set_args(sieve_buffer,
                              small_primes_buffer,
                              np.uint32(len(small_primes)),
                              np.uint32(low))

        # Enqueue the kernel
        event = cl.enqueue_nd_range_kernel(queue, sieve_kernel, (global_work_size,), (local_work_size,))

        # Asynchronous copy: Read back the sieve data after kernel execution
        sieve_host = np.empty_like(reversed_sieve)
        cl.enqueue_copy(queue, sieve_host, sieve_buffer, is_blocking=False, wait_for=[event])

        # Wait for the copy to complete
        event.wait()

        # Reverse the bits back
        reversed_sieve_host = reverse_lookup[sieve_host]

        # Count the number of set bits (primes) in the segment
        bits = np.unpackbits(reversed_sieve_host)
        bits = bits[:sieve_bits]
        primes_in_segment = np.count_nonzero(bits)

        # Add to the total prime count
        total_primes += primes_in_segment

        # Optional: Progress update
        print(f"Processed segment [{low}, {high}): {primes_in_segment} primes found.")

    return total_primes

# Example Usage in Jupyter Notebook
# Adjust 'n' as needed (e.g., 1_000_000_000 for 1 billion)
n = 100_000_000  # 100 million for initial testing

# Measure execution time
start_time = time.time()
prime_count = count_primes_with_gpu(n)
end_time = time.time()

print(f"\nNumber of primes up to {n}: {prime_count}")
print(f"Time taken: {end_time - start_time:.2f} seconds")