Repository: qiskit-community/qiskit-machine-learning (language: python)

Issue:
The FidelityQuantumKernel cannot be executed on the hardware for larger problems.
### Environment

- **Qiskit Machine Learning version**: 0.6.1
- **Python version**: 3.10.8
- **Operating system**: Linux

Using IBM Lab:
Software | Version
-- | --
qiskit | 0.44.1
qiskit-terra | 0.25.1
qiskit_aer | 0.12.2
qiskit_machine_learning | 0.6.1
qiskit_ibm_provider | 0.6.3
qiskit_ibm_runtime | 0.11.3
Python version | 3.10.8
Python compiler | GCC 10.4.0
Python build | main, Nov 22 2022 08:26:04
OS | Linux
CPUs | 8
Memory (Gb) | 31.142810821533203
Mon Oct 23 18:57:54 2023 UTC



### What is happening?

Hi,
I think there is a problem with the way `_get_kernel_entries` handles the jobs sent to hardware.

The error is:
3242 | Circuit count exceeded.
-- | --

I think the problem stems from

https://github.com/qiskit-community/qiskit-machine-learning/blob/b4402c2ea4f5c79701bf79e7b5937999abc52f17/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py#L203-L212

Also, it is really hard to train quantum fidelity kernel on the hardware with the sessions:
```IBMRuntimeError: 'Failed to run program: \'400 Client Error: Bad Request for url: https://api.quantum-computing.ibm.com/runtime/jobs. {"errors":[{"code":1217,"message":"Session has been closed.","solution":"Reduce time between submitting subsequent jobs in a session.","more_info":"https://docs.quantum-computing.ibm.com/errors"}]}\''```

### How can we reproduce the issue?

```# External imports
from pylab import cm
from sklearn import metrics
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

# Qiskit imports
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.visualization import circuit_drawer
from qiskit.algorithms.optimizers import SPSA
from qiskit.circuit.library import ZZFeatureMap
from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel
from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer
from qiskit_machine_learning.algorithms import QSVC
from qiskit_machine_learning.datasets import ad_hoc_data


class QKTCallback:
    """Callback wrapper class."""

    def __init__(self) -> None:
        self._data = [[] for i in range(5)]

    def callback(self, x0, x1=None, x2=None, x3=None, x4=None):
        """
        Args:
            x0: number of function evaluations
            x1: the parameters
            x2: the function value
            x3: the stepsize
            x4: whether the step was accepted
        """
        self._data[0].append(x0)
        self._data[1].append(x1)
        self._data[2].append(x2)
        self._data[3].append(x3)
        self._data[4].append(x4)

    def get_callback_data(self):
        return self._data

    def clear_callback_data(self):
        self._data = [[] for i in range(5)]

adhoc_dimension = 2
X_train, y_train, X_test, y_test, adhoc_total = ad_hoc_data(
    training_size=40,
    test_size=5,
    n=adhoc_dimension,
    gap=0.3,
    plot_data=False,
    one_hot=False,
    include_sample_total=True,
)

# Create a rotational layer to train. We will rotate each qubit the same amount.
training_params = ParameterVector("θ", 1)
fm0 = QuantumCircuit(2)
fm0.ry(training_params[0], 0)
fm0.ry(training_params[0], 1)

# Use ZZFeatureMap to represent input data
fm1 = ZZFeatureMap(2)

# Create the feature map, composed of our two circuits
fm = fm0.compose(fm1)

print(circuit_drawer(fm))
print(f"Trainable parameters: {training_params}")

# Instantiate quantum kernel
quant_kernel = TrainableFidelityQuantumKernel(feature_map=fm, training_parameters=training_params)

# Set up the optimizer
cb_qkt = QKTCallback()
spsa_opt = SPSA(maxiter=10, callback=cb_qkt.callback, learning_rate=0.05, perturbation=0.05)

# Instantiate a quantum kernel trainer.
qkt = QuantumKernelTrainer(
    quantum_kernel=quant_kernel, loss="svc_loss", optimizer=spsa_opt, initial_point=[np.pi / 2]
)

## Train using simulator
# Train the kernel using QKT directly
qka_results = qkt.fit(X_train, y_train)
optimized_kernel = qka_results.quantum_kernel
print(qka_results)
# Use QSVC for classification
qsvc = QSVC(quantum_kernel=optimized_kernel)

# Fit the QSVC
qsvc.fit(X_train, y_train)

# Predict the labels
labels_test = qsvc.predict(X_test)

# Evalaute the test accuracy
accuracy_test = metrics.balanced_accuracy_score(y_true=y_test, y_pred=labels_test)
print(f"accuracy test: {accuracy_test}")

## HW Settings
from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator, Options, Sampler
from qiskit.algorithms.state_fidelities import ComputeUncompute
service = QiskitRuntimeService()

backend = service.get_backend('ibm_lagos')

options = Options()
options.optimization_level = 0
options.resilience_level = 0
options.execution.shots = 100
options.initial_layout = [1,3,0,2,4,5,6]

## Train using HW
with Session(service=service, backend=backend) as session:
    sampler = Sampler(options=options)
    fidelity = ComputeUncompute(sampler=sampler, local=True)
    qk = TrainableFidelityQuantumKernel(feature_map=fm, training_parameters=training_params, fidelity=fidelity)
    qkt = QuantumKernelTrainer(quantum_kernel=qk, loss="svc_loss", optimizer=spsa_opt, initial_point=[np.pi / 2])
    qka_results = qkt.fit(X_train, y_train)
    optimized_kernel = qka_results.quantum_kernel
    print(qka_results)
    
    # Use QSVC for classification
    qsvc = QSVC(quantum_kernel=optimized_kernel)

    # Fit the QSVC
    qsvc.fit(X_train, y_train)
```


### What should happen?

```
Traceback (most recent call last):
  Cell In[11], line 6
    qka_results = qkt.fit(X_train, y_train)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py:215 in fit
    opt_results = self._optimizer.minimize(
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:558 in minimize
    fx_estimate, update = self._compute_update(fun, x, k, next(eps), lse_solver)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:489 in _compute_update
    value, gradient, hessian = self._point_estimate(loss, x, eps, num_samples)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:466 in _point_estimate
    value_sample, gradient_sample, hessian_sample = self._point_sample(
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:427 in _point_sample
    values = _batch_evaluate(loss, points, self._max_evals_grouped)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:728 in _batch_evaluate
    return [
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:729 in <listcomp>
    function(*point) if isinstance(point, tuple) else function(point) for point in points
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py:109 in evaluate
    kmatrix = quantum_kernel.evaluate(np.array(data))
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py:112 in evaluate
    return super().evaluate(x_vec, y_vec)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:105 in evaluate
    kernel_matrix = self._get_symmetric_kernel_matrix(
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:196 in _get_symmetric_kernel_matrix
    kernel_entries = self._get_kernel_entries(left_parameters, right_parameters)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:218 in _get_kernel_entries
    kernel_entries = np.real(job.result().fidelities)
  File /opt/conda/lib/python3.10/site-packages/qiskit/primitives/primitive_job.py:55 in result
    return self._future.result()
  File /opt/conda/lib/python3.10/concurrent/futures/_base.py:458 in result
    return self.__get_result()
  File /opt/conda/lib/python3.10/concurrent/futures/_base.py:403 in __get_result
    raise self._exception
  File /opt/conda/lib/python3.10/concurrent/futures/thread.py:58 in run
    result = self.fn(*self.args, **self.kwargs)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/state_fidelities/compute_uncompute.py:158 in _run
    job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__)
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/sampler.py:122 in run
    return super().run(
  File /opt/conda/lib/python3.10/site-packages/qiskit/primitives/base/base_sampler.py:147 in run
    return self._run(
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/sampler.py:152 in _run
    return self._run_primitive(
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/base_primitive.py:183 in _run_primitive
    raise IBMInputValueError(
IBMInputValueError: 'Number of circuits, 3160 exceeds the maximum for this backend, 900)'
```

### Any suggestions?

I made a workaround and I can submit a PR regarding this.:

```from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel
import numpy as np
from typing import Sequence

class ModifiedTrainableFidelityQuantumKernel(TrainableFidelityQuantumKernel):

    def _get_kernel_entries(
        self, 
        left_parameters: np.ndarray, 
        right_parameters: np.ndarray, 
        max_circuits_per_job: int = 300
    ) -> Sequence[float]:
        """
        Gets kernel entries by executing the underlying fidelity instance and getting the results
        back from the async job. The number of circuits per job is limited by the max_circuits_per_job parameter.
        """
        num_circuits = left_parameters.shape[0]
        kernel_entries = []
        
        # Determine the number of chunks needed
        num_chunks = (num_circuits + max_circuits_per_job - 1) // max_circuits_per_job
        
        for i in range(num_chunks):
            # Determine the range of indices for this chunk
            start_idx = i * max_circuits_per_job
            end_idx = min((i + 1) * max_circuits_per_job, num_circuits)
            
            # Extract the parameters for this chunk
            chunk_left_parameters = left_parameters[start_idx:end_idx]
            chunk_right_parameters = right_parameters[start_idx:end_idx]
            
            # Execute this chunk
            job = self._fidelity.run(
                [self._feature_map] * (end_idx - start_idx),
                [self._feature_map] * (end_idx - start_idx),
                chunk_left_parameters,
                chunk_right_parameters,
            )
            
            # Extend the kernel_entries list with the results from this chunk
            kernel_entries.extend(job.result().fidelities)
        
        return kernel_entries
QKT deepcopies kernel internally, fails with runtime primitives
### Environment

- **Qiskit Machine Learning version**: 0.6.0
- **Python version**: all
- **Operating system**: all


### What is happening?

On line 202, the quantum kernel trainer performs: `output_kernel = copy.deepcopy(self._quantum_kernel)`, which is incompatible with the runtime primitives, as they do not support `deepcopy`. If you try to run it, a recursion error is raised (this does not happen with the Terra reference or Aer primitives).

### How can we reproduce the issue?

See the original issue: https://github.com/Qiskit/qiskit-ibm-runtime/issues/745

### What should happen?

The QKT class should work seamlessly for all primitives.

### Any suggestions?

I think it's worth revising whether we need to copy the kernel, or whether we need to return a kernel at all. 

- One possibility would be for the QKT class to return the trained paremeters and have the user deal with them as they wish (downside -> breaks current interface) 
- Another possibility would be to develop a more sophisticated `kernel.copy` that creates a new kernel without copying the primitive
- Finally, we could have the QKT modify the original kernel object
Added max_circuits_per_job and removed deepcopy dependency of the quantum kernel trainer fixing #701 and #600
### Summary
Bug fixes have been made to fix issues #701 and #600.
Changes made:
#701: Added a max_circuits_per_job parameter to fidelity quantum kernel used in the case that if more circuits are submitted than the job limit for the backend, the circuits are split up and ran through separate jobs. Documentation was added and formatted for this.
#600: Removed quantum kernel trainers dependency on copy.deepcopy that was throwing an error with real backends. Now initialise a new instance of a class and assign parameters manually. As change is purely internal, no new documentation required.
 
### Details and comments
All relevant unit tests were run on code.

✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
✅ I have added notes to the CHANGELOG. 


Added max_circuits_per_job and removed deepcopy dependency of the quantum kernel trainer fixing #701 and #600
### Summary
Bug fixes have been made to fix issues #701 and #600.
Changes made:
#701: Added a max_circuits_per_job parameter to fidelity quantum kernel used in the case that if more circuits are submitted than the job limit for the backend, the circuits are split up and ran through separate jobs. Documentation was added and formatted for this.
#600: Removed quantum kernel trainers dependency on copy.deepcopy that was throwing an error with real backends. Now initialise a new instance of a class and assign parameters manually. As change is purely internal, no new documentation required.
 
### Details and comments
All relevant unit tests were run on code.

✅ I have updated the documentation accordingly.
✅ I have read the CONTRIBUTING document.
✅ I have added notes to the CHANGELOG. 


The FidelityQuantumKernel cannot be executed on the hardware for larger problems.
### Environment

- **Qiskit Machine Learning version**: 0.6.1
- **Python version**: 3.10.8
- **Operating system**: Linux

Using IBM Lab:
Software | Version
-- | --
qiskit | 0.44.1
qiskit-terra | 0.25.1
qiskit_aer | 0.12.2
qiskit_machine_learning | 0.6.1
qiskit_ibm_provider | 0.6.3
qiskit_ibm_runtime | 0.11.3
Python version | 3.10.8
Python compiler | GCC 10.4.0
Python build | main, Nov 22 2022 08:26:04
OS | Linux
CPUs | 8
Memory (Gb) | 31.142810821533203
Mon Oct 23 18:57:54 2023 UTC



### What is happening?

Hi,
I think there is a problem with the way `_get_kernel_entries` handles the jobs sent to hardware.

The error is:
3242 | Circuit count exceeded.
-- | --

I think the problem stems from

https://github.com/qiskit-community/qiskit-machine-learning/blob/b4402c2ea4f5c79701bf79e7b5937999abc52f17/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py#L203-L212

Also, it is really hard to train quantum fidelity kernel on the hardware with the sessions:
```IBMRuntimeError: 'Failed to run program: \'400 Client Error: Bad Request for url: https://api.quantum-computing.ibm.com/runtime/jobs. {"errors":[{"code":1217,"message":"Session has been closed.","solution":"Reduce time between submitting subsequent jobs in a session.","more_info":"https://docs.quantum-computing.ibm.com/errors"}]}\''```

### How can we reproduce the issue?

```# External imports
from pylab import cm
from sklearn import metrics
import numpy as np
import matplotlib
import matplotlib.pyplot as plt

# Qiskit imports
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.visualization import circuit_drawer
from qiskit.algorithms.optimizers import SPSA
from qiskit.circuit.library import ZZFeatureMap
from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel
from qiskit_machine_learning.kernels.algorithms import QuantumKernelTrainer
from qiskit_machine_learning.algorithms import QSVC
from qiskit_machine_learning.datasets import ad_hoc_data


class QKTCallback:
    """Callback wrapper class."""

    def __init__(self) -> None:
        self._data = [[] for i in range(5)]

    def callback(self, x0, x1=None, x2=None, x3=None, x4=None):
        """
        Args:
            x0: number of function evaluations
            x1: the parameters
            x2: the function value
            x3: the stepsize
            x4: whether the step was accepted
        """
        self._data[0].append(x0)
        self._data[1].append(x1)
        self._data[2].append(x2)
        self._data[3].append(x3)
        self._data[4].append(x4)

    def get_callback_data(self):
        return self._data

    def clear_callback_data(self):
        self._data = [[] for i in range(5)]

adhoc_dimension = 2
X_train, y_train, X_test, y_test, adhoc_total = ad_hoc_data(
    training_size=40,
    test_size=5,
    n=adhoc_dimension,
    gap=0.3,
    plot_data=False,
    one_hot=False,
    include_sample_total=True,
)

# Create a rotational layer to train. We will rotate each qubit the same amount.
training_params = ParameterVector("θ", 1)
fm0 = QuantumCircuit(2)
fm0.ry(training_params[0], 0)
fm0.ry(training_params[0], 1)

# Use ZZFeatureMap to represent input data
fm1 = ZZFeatureMap(2)

# Create the feature map, composed of our two circuits
fm = fm0.compose(fm1)

print(circuit_drawer(fm))
print(f"Trainable parameters: {training_params}")

# Instantiate quantum kernel
quant_kernel = TrainableFidelityQuantumKernel(feature_map=fm, training_parameters=training_params)

# Set up the optimizer
cb_qkt = QKTCallback()
spsa_opt = SPSA(maxiter=10, callback=cb_qkt.callback, learning_rate=0.05, perturbation=0.05)

# Instantiate a quantum kernel trainer.
qkt = QuantumKernelTrainer(
    quantum_kernel=quant_kernel, loss="svc_loss", optimizer=spsa_opt, initial_point=[np.pi / 2]
)

## Train using simulator
# Train the kernel using QKT directly
qka_results = qkt.fit(X_train, y_train)
optimized_kernel = qka_results.quantum_kernel
print(qka_results)
# Use QSVC for classification
qsvc = QSVC(quantum_kernel=optimized_kernel)

# Fit the QSVC
qsvc.fit(X_train, y_train)

# Predict the labels
labels_test = qsvc.predict(X_test)

# Evalaute the test accuracy
accuracy_test = metrics.balanced_accuracy_score(y_true=y_test, y_pred=labels_test)
print(f"accuracy test: {accuracy_test}")

## HW Settings
from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator, Options, Sampler
from qiskit.algorithms.state_fidelities import ComputeUncompute
service = QiskitRuntimeService()

backend = service.get_backend('ibm_lagos')

options = Options()
options.optimization_level = 0
options.resilience_level = 0
options.execution.shots = 100
options.initial_layout = [1,3,0,2,4,5,6]

## Train using HW
with Session(service=service, backend=backend) as session:
    sampler = Sampler(options=options)
    fidelity = ComputeUncompute(sampler=sampler, local=True)
    qk = TrainableFidelityQuantumKernel(feature_map=fm, training_parameters=training_params, fidelity=fidelity)
    qkt = QuantumKernelTrainer(quantum_kernel=qk, loss="svc_loss", optimizer=spsa_opt, initial_point=[np.pi / 2])
    qka_results = qkt.fit(X_train, y_train)
    optimized_kernel = qka_results.quantum_kernel
    print(qka_results)
    
    # Use QSVC for classification
    qsvc = QSVC(quantum_kernel=optimized_kernel)

    # Fit the QSVC
    qsvc.fit(X_train, y_train)
```


### What should happen?

```
Traceback (most recent call last):
  Cell In[11], line 6
    qka_results = qkt.fit(X_train, y_train)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py:215 in fit
    opt_results = self._optimizer.minimize(
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:558 in minimize
    fx_estimate, update = self._compute_update(fun, x, k, next(eps), lse_solver)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:489 in _compute_update
    value, gradient, hessian = self._point_estimate(loss, x, eps, num_samples)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:466 in _point_estimate
    value_sample, gradient_sample, hessian_sample = self._point_sample(
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:427 in _point_sample
    values = _batch_evaluate(loss, points, self._max_evals_grouped)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:728 in _batch_evaluate
    return [
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/optimizers/spsa.py:729 in <listcomp>
    function(*point) if isinstance(point, tuple) else function(point) for point in points
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/utils/loss_functions/kernel_loss_functions.py:109 in evaluate
    kmatrix = quantum_kernel.evaluate(np.array(data))
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/trainable_fidelity_quantum_kernel.py:112 in evaluate
    return super().evaluate(x_vec, y_vec)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:105 in evaluate
    kernel_matrix = self._get_symmetric_kernel_matrix(
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:196 in _get_symmetric_kernel_matrix
    kernel_entries = self._get_kernel_entries(left_parameters, right_parameters)
  File /opt/conda/lib/python3.10/site-packages/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py:218 in _get_kernel_entries
    kernel_entries = np.real(job.result().fidelities)
  File /opt/conda/lib/python3.10/site-packages/qiskit/primitives/primitive_job.py:55 in result
    return self._future.result()
  File /opt/conda/lib/python3.10/concurrent/futures/_base.py:458 in result
    return self.__get_result()
  File /opt/conda/lib/python3.10/concurrent/futures/_base.py:403 in __get_result
    raise self._exception
  File /opt/conda/lib/python3.10/concurrent/futures/thread.py:58 in run
    result = self.fn(*self.args, **self.kwargs)
  File /opt/conda/lib/python3.10/site-packages/qiskit/algorithms/state_fidelities/compute_uncompute.py:158 in _run
    job = self._sampler.run(circuits=circuits, parameter_values=values, **opts.__dict__)
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/sampler.py:122 in run
    return super().run(
  File /opt/conda/lib/python3.10/site-packages/qiskit/primitives/base/base_sampler.py:147 in run
    return self._run(
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/sampler.py:152 in _run
    return self._run_primitive(
  File /opt/conda/lib/python3.10/site-packages/qiskit_ibm_runtime/base_primitive.py:183 in _run_primitive
    raise IBMInputValueError(
IBMInputValueError: 'Number of circuits, 3160 exceeds the maximum for this backend, 900)'
```

### Any suggestions?

I made a workaround and I can submit a PR regarding this.:

```from qiskit_machine_learning.kernels import TrainableFidelityQuantumKernel
import numpy as np
from typing import Sequence

class ModifiedTrainableFidelityQuantumKernel(TrainableFidelityQuantumKernel):

    def _get_kernel_entries(
        self, 
        left_parameters: np.ndarray, 
        right_parameters: np.ndarray, 
        max_circuits_per_job: int = 300
    ) -> Sequence[float]:
        """
        Gets kernel entries by executing the underlying fidelity instance and getting the results
        back from the async job. The number of circuits per job is limited by the max_circuits_per_job parameter.
        """
        num_circuits = left_parameters.shape[0]
        kernel_entries = []
        
        # Determine the number of chunks needed
        num_chunks = (num_circuits + max_circuits_per_job - 1) // max_circuits_per_job
        
        for i in range(num_chunks):
            # Determine the range of indices for this chunk
            start_idx = i * max_circuits_per_job
            end_idx = min((i + 1) * max_circuits_per_job, num_circuits)
            
            # Extract the parameters for this chunk
            chunk_left_parameters = left_parameters[start_idx:end_idx]
            chunk_right_parameters = right_parameters[start_idx:end_idx]
            
            # Execute this chunk
            job = self._fidelity.run(
                [self._feature_map] * (end_idx - start_idx),
                [self._feature_map] * (end_idx - start_idx),
                chunk_left_parameters,
                chunk_right_parameters,
            )
            
            # Extend the kernel_entries list with the results from this chunk
            kernel_entries.extend(job.result().fidelities)
        
        return kernel_entries


Discussion / hints:
Had you tried raising an issue against IBM Runtime https://github.com/Qiskit/qiskit-ibm-runtime/issues For the defined Sampler interface there is no common notion of any max circuits limit - the only limitation, as such, to calling run, as its defined, would be machine memory to hold all the circuits. I would have expected this as such to work and internally if for some reason it needed to split them to smaller batches for some internal limits it (the Sampler) should do that itself.
@OkuyanBoga can this issue be closed now?
I believe Extended sessions and #772 will solve the issue.
Hi @ElePT, 
Have you been able to solve the problem ?
Thank you !
I'm looking into the issue. Sorry for the delay
@adekusar-drl I believe this PR should fix the issue: https://github.com/Qiskit/qiskit-ibm-runtime/pull/902
Hiya, this issue should be fixed within the springv1 Hartree centre qml PR.
[![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/qiskit-community/qiskit-machine-learning?pullRequest=772) <br/>All committers have signed the CLA.
## Pull Request Test Coverage Report for [Build 8062887234](https://coveralls.io/builds/65937823)


### Details

* **0** of **19**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.04%**) to **92.607%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65937823/badge)](https://coveralls.io/builds/65937823) |
| :-- | --: |
| Change from base [Build 8008433818](https://coveralls.io/builds/65861862): |  0.04% |
| Covered Lines: | 1879 |
| Relevant Lines: | 2029 |

---
##### 💛  - [Coveralls](https://coveralls.io)

As I stated here https://github.com/qiskit-community/qiskit-machine-learning/issues/701#issuecomment-1779410494 the primitive ought to be taking care of this as far as I know. I will check, there has been a change where the circuit now has to be an ISA circuit (already conforms to backend in terms of layout and basis gates) instead of allowing logical circuits. So given such knowledge perhaps the limit is expected to be managed too now.
Also it only needs one release note that lists two fixes. The issues part is for if the release contains known issues that the user should be aware of and workaround - in this case you are fixing issues/bugs so it just needs a fixes section and both can be be specified in the one release note.
BTW - [you can reference the issues in a way that will close them](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) when this PR is merged. So for instance had you written `fixes #701` that would cause that issue to be linked and closed when this PR was merged - the earlier link has the set of keywords that can be used and more info.
- > As I stated here [#701 (comment)](https://github.com/qiskit-community/qiskit-machine-learning/issues/701#issuecomment-1779410494) the primitive ought to be taking care of this as far as I know. I will check, there has been a change where the circuit now has to be an ISA circuit (already conforms to backend in terms of layout and basis gates) instead of allowing logical circuits. So given such knowledge perhaps the limit is expected to be managed too now.

The number of max circuits per job needs to be set in the quantum fidelity kernel for the `Max circuits` hard limit on the hardware, which I believe would be useful for everyone. I reckon the extended sessions will allow users to continue their jobs to solve the latter issue. 


- > Also it only needs one release note that lists two fixes. The issues part is for if the release contains known issues that the user should be aware of and workaround - in this case you are fixing issues/bugs so it just needs a fixes section and both can be be specified in the one release note.

 Sorted, I hope it is better now.

- > You should add unit tests to cover new features

Added new unit tests to cover them.

I added https://github.com/qiskit-community/qiskit-machine-learning/labels/stable%20backport%20potential label which will cause the `mergify` bot to do a PR to backport this to the stable branch. This would be done for critical fixes we would want in bug fix release and these seem like such fixes to have ready there for whenever a bug fix release is done.
I made the requested changes. 
[![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/qiskit-community/qiskit-machine-learning?pullRequest=772) <br/>All committers have signed the CLA.
## Pull Request Test Coverage Report for [Build 8062887234](https://coveralls.io/builds/65937823)


### Details

* **0** of **19**   **(100.0%)**  changed or added relevant lines in **2** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.04%**) to **92.607%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65937823/badge)](https://coveralls.io/builds/65937823) |
| :-- | --: |
| Change from base [Build 8008433818](https://coveralls.io/builds/65861862): |  0.04% |
| Covered Lines: | 1879 |
| Relevant Lines: | 2029 |

---
##### 💛  - [Coveralls](https://coveralls.io)

As I stated here https://github.com/qiskit-community/qiskit-machine-learning/issues/701#issuecomment-1779410494 the primitive ought to be taking care of this as far as I know. I will check, there has been a change where the circuit now has to be an ISA circuit (already conforms to backend in terms of layout and basis gates) instead of allowing logical circuits. So given such knowledge perhaps the limit is expected to be managed too now.
Also it only needs one release note that lists two fixes. The issues part is for if the release contains known issues that the user should be aware of and workaround - in this case you are fixing issues/bugs so it just needs a fixes section and both can be be specified in the one release note.
BTW - [you can reference the issues in a way that will close them](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) when this PR is merged. So for instance had you written `fixes #701` that would cause that issue to be linked and closed when this PR was merged - the earlier link has the set of keywords that can be used and more info.
- > As I stated here [#701 (comment)](https://github.com/qiskit-community/qiskit-machine-learning/issues/701#issuecomment-1779410494) the primitive ought to be taking care of this as far as I know. I will check, there has been a change where the circuit now has to be an ISA circuit (already conforms to backend in terms of layout and basis gates) instead of allowing logical circuits. So given such knowledge perhaps the limit is expected to be managed too now.

The number of max circuits per job needs to be set in the quantum fidelity kernel for the `Max circuits` hard limit on the hardware, which I believe would be useful for everyone. I reckon the extended sessions will allow users to continue their jobs to solve the latter issue. 


- > Also it only needs one release note that lists two fixes. The issues part is for if the release contains known issues that the user should be aware of and workaround - in this case you are fixing issues/bugs so it just needs a fixes section and both can be be specified in the one release note.

 Sorted, I hope it is better now.

- > You should add unit tests to cover new features

Added new unit tests to cover them.

I added https://github.com/qiskit-community/qiskit-machine-learning/labels/stable%20backport%20potential label which will cause the `mergify` bot to do a PR to backport this to the stable branch. This would be done for critical fixes we would want in bug fix release and these seem like such fixes to have ready there for whenever a bug fix release is done.
I made the requested changes. 
Had you tried raising an issue against IBM Runtime https://github.com/Qiskit/qiskit-ibm-runtime/issues For the defined Sampler interface there is no common notion of any max circuits limit - the only limitation, as such, to calling run, as its defined, would be machine memory to hold all the circuits. I would have expected this as such to work and internally if for some reason it needed to split them to smaller batches for some internal limits it (the Sampler) should do that itself.
@OkuyanBoga can this issue be closed now?
I believe Extended sessions and #772 will solve the issue.

Fix patch:
diff --git a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py
index 8dc172ba4..4d272b2ac 100644
--- a/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py
+++ b/qiskit_machine_learning/kernels/algorithms/quantum_kernel_trainer.py
@@ -1,6 +1,6 @@
 # This code is part of a Qiskit project.
 #
-# (C) Copyright IBM 2021, 2023.
+# (C) Copyright IBM 2021, 2024.
 #
 # This code is licensed under the Apache License, Version 2.0. You may
 # obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -13,7 +13,6 @@
 """Quantum Kernel Trainer"""
 from __future__ import annotations
 
-import copy
 from functools import partial
 from typing import Sequence
 
@@ -96,7 +95,8 @@ def __init__(
     ):
         """
         Args:
-            quantum_kernel: a trainable quantum kernel to be trained.
+            quantum_kernel: a trainable quantum kernel to be trained. The
+                :attr:`~.TrainableKernel.parameter_values` will be modified in place after the training.
             loss: A loss function available via string is "svc_loss" which is the same as
                 :class:`~qiskit_machine_learning.utils.loss_functions.SVCLoss`. If a string is
                 passed as the loss function, then the underlying
@@ -179,7 +179,7 @@ def fit(
     ) -> QuantumKernelTrainerResult:
         """
         Train the QuantumKernel by minimizing loss over the kernel parameters. The input
-        quantum kernel will not be altered, and an optimized quantum kernel will be returned.
+        quantum kernel will be altered.
 
         Args:
             data (numpy.ndarray): ``(N, D)`` array of training data, where ``N`` is the
@@ -198,9 +198,6 @@ def fit(
             msg = "Quantum kernel cannot be fit because there are no user parameters specified."
             raise ValueError(msg)
 
-        # Bind inputs to objective function
-        output_kernel = copy.deepcopy(self._quantum_kernel)
-
         # Randomly initialize the initial point if one was not passed
         if self._initial_point is None:
             self._initial_point = algorithm_globals.random.random(num_params)
@@ -222,11 +219,13 @@ def fit(
         result.optimizer_evals = opt_results.nfev
         result.optimal_value = opt_results.fun
         result.optimal_point = opt_results.x
-        result.optimal_parameters = dict(zip(output_kernel.training_parameters, opt_results.x))
+        result.optimal_parameters = dict(
+            zip(self.quantum_kernel.training_parameters, opt_results.x)
+        )
 
         # Return the QuantumKernel in optimized state
-        output_kernel.assign_training_parameters(result.optimal_parameters)
-        result.quantum_kernel = output_kernel
+        self.quantum_kernel.assign_training_parameters(result.optimal_parameters)
+        result.quantum_kernel = self.quantum_kernel
 
         return result
 
diff --git a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py
index ddca88936..32e8a2294 100644
--- a/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py
+++ b/qiskit_machine_learning/kernels/fidelity_quantum_kernel.py
@@ -1,6 +1,6 @@
 # This code is part of a Qiskit project.
 #
-# (C) Copyright IBM 2022, 2023.
+# (C) Copyright IBM 2022, 2024.
 #
 # This code is licensed under the Apache License, Version 2.0. You may
 # obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -46,6 +46,7 @@ def __init__(
         fidelity: BaseStateFidelity | None = None,
         enforce_psd: bool = True,
         evaluate_duplicates: str = "off_diagonal",
+        max_circuits_per_job: int = None,
     ) -> None:
         """
         Args:
@@ -73,6 +74,8 @@ def __init__(
                     - ``none`` when training the diagonal is set to `1` and if two identical samples
                       are found in the dataset the corresponding matrix element is set to `1`.
                       When inferring, matrix elements for identical samples are set to `1`.
+            max_circuits_per_job: Maximum number of circuits per job for the backend. Please
+               check the backend specifications. Use ``None`` for all entries per job. Default ``None``.
         Raises:
             ValueError: When unsupported value is passed to `evaluate_duplicates`.
         """
@@ -84,10 +87,15 @@ def __init__(
                 f"Unsupported value passed as evaluate_duplicates: {evaluate_duplicates}"
             )
         self._evaluate_duplicates = eval_duplicates
-
         if fidelity is None:
             fidelity = ComputeUncompute(sampler=Sampler())
         self._fidelity = fidelity
+        if max_circuits_per_job is not None:
+            if max_circuits_per_job < 1:
+                raise ValueError(
+                    f"Unsupported value passed as max_circuits_per_job: {max_circuits_per_job}"
+                )
+        self.max_circuits_per_job = max_circuits_per_job
 
     def evaluate(self, x_vec: np.ndarray, y_vec: np.ndarray | None = None) -> np.ndarray:
         x_vec, y_vec = self._validate_input(x_vec, y_vec)
@@ -208,17 +216,38 @@ def _get_kernel_entries(
         back from the async job.
         """
         num_circuits = left_parameters.shape[0]
+        kernel_entries = []
+        # Check if it is trivial case, only identical samples
         if num_circuits != 0:
-            job = self._fidelity.run(
-                [self._feature_map] * num_circuits,
-                [self._feature_map] * num_circuits,
-                left_parameters,
-                right_parameters,
-            )
-            kernel_entries = job.result().fidelities
-        else:
-            # trivial case, only identical samples
-            kernel_entries = []
+            if self.max_circuits_per_job is None:
+                job = self._fidelity.run(
+                    [self._feature_map] * num_circuits,
+                    [self._feature_map] * num_circuits,
+                    left_parameters,
+                    right_parameters,
+                )
+                kernel_entries = job.result().fidelities
+            else:
+                # Determine the number of chunks needed
+                num_chunks = (
+                    num_circuits + self.max_circuits_per_job - 1
+                ) // self.max_circuits_per_job
+                for i in range(num_chunks):
+                    # Determine the range of indices for this chunk
+                    start_idx = i * self.max_circuits_per_job
+                    end_idx = min((i + 1) * self.max_circuits_per_job, num_circuits)
+                    # Extract the parameters for this chunk
+                    chunk_left_parameters = left_parameters[start_idx:end_idx]
+                    chunk_right_parameters = right_parameters[start_idx:end_idx]
+                    # Execute this chunk
+                    job = self._fidelity.run(
+                        [self._feature_map] * (end_idx - start_idx),
+                        [self._feature_map] * (end_idx - start_idx),
+                        chunk_left_parameters,
+                        chunk_right_parameters,
+                    )
+                    # Extend the kernel_entries list with the results from this chunk
+                    kernel_entries.extend(job.result().fidelities)
         return kernel_entries
 
     def _is_trivial(
diff --git a/releasenotes/notes/fix-701-max_circuits_per_job-and-600-deepcopy-dependency-e6eda2e5b986c1be.yaml b/releasenotes/notes/fix-701-max_circuits_per_job-and-600-deepcopy-dependency-e6eda2e5b986c1be.yaml
new file mode 100644
index 000000000..a9b504fdf
--- /dev/null
+++ b/releasenotes/notes/fix-701-max_circuits_per_job-and-600-deepcopy-dependency-e6eda2e5b986c1be.yaml
@@ -0,0 +1,14 @@
+---
+fixes:
+  - |
+    Added a `max_circuits_per_job` parameter to the :class:`.FidelityQuantumKernel` used
+    in the case that if more circuits are submitted than the job limit for the 
+    backend, the circuits are split up and run through separate jobs.
+  - |    
+    Removed :class:`.QuantumKernelTrainer` dependency on `copy.deepcopy` that was
+    throwing an error with real backends. Now, it modifies the :class:`.TrainableKernel`
+    in place. If you would like to use the initial kernel, please call 
+    :meth:`~.TrainableKernel.assign_training_parameters` of the :class:`~.TrainableKernel`
+    using the :attr:`~.QuantumKernelTrainer.initial_point` attribute of 
+    :class:`~.QuantumKernelTrainer`.
+ 


Test patch:
diff --git a/test/kernels/test_fidelity_qkernel.py b/test/kernels/test_fidelity_qkernel.py
index ffe0f56a5..6a132f14e 100644
--- a/test/kernels/test_fidelity_qkernel.py
+++ b/test/kernels/test_fidelity_qkernel.py
@@ -106,10 +106,27 @@ def test_defaults(self):
 
         self.assertGreaterEqual(score, 0.5)
 
+    def test_max_circuits_per_job(self):
+        """Test max_circuits_per_job parameters."""
+        kernel_all = FidelityQuantumKernel(feature_map=self.feature_map, max_circuits_per_job=None)
+        kernel_matrix_all = kernel_all.evaluate(x_vec=self.sample_train)
+        with self.subTest("Check when max_circuits_per_job > left_parameters"):
+            kernel_more = FidelityQuantumKernel(
+                feature_map=self.feature_map, max_circuits_per_job=20
+            )
+            kernel_matrix_more = kernel_more.evaluate(x_vec=self.sample_train)
+            np.testing.assert_equal(kernel_matrix_all, kernel_matrix_more)
+        with self.subTest("Check when max_circuits_per_job = 1"):
+            kernel_1 = FidelityQuantumKernel(feature_map=self.feature_map, max_circuits_per_job=1)
+            kernel_matrix_1 = kernel_1.evaluate(x_vec=self.sample_train)
+            np.testing.assert_equal(kernel_matrix_all, kernel_matrix_1)
+
     def test_exceptions(self):
         """Test quantum kernel raises exceptions and warnings."""
         with self.assertRaises(ValueError, msg="Unsupported value of 'evaluate_duplicates'."):
             _ = FidelityQuantumKernel(evaluate_duplicates="wrong")
+        with self.assertRaises(ValueError, msg="Unsupported value of 'max_circuits_per_job'."):
+            _ = FidelityQuantumKernel(max_circuits_per_job=-1)
 
     @idata(
         # params, fidelity, feature map, enforce_psd, duplicate