264 lines
11 KiB
Ruby
264 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'mime/types'
|
|
|
|
module API
|
|
# MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api
|
|
module Ml
|
|
class Mlflow < ::API::Base
|
|
include APIGuard
|
|
|
|
# The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls
|
|
MLFLOW_API_PREFIX = ':id/ml/mlflow/api/2.0/mlflow/'
|
|
|
|
allow_access_with_scope :api
|
|
allow_access_with_scope :read_api, if: -> (request) { request.get? || request.head? }
|
|
|
|
feature_category :mlops
|
|
|
|
content_type :json, 'application/json'
|
|
default_format :json
|
|
|
|
before do
|
|
# MLFlow Client considers any status code different than 200 an error, even 201
|
|
status 200
|
|
|
|
authenticate!
|
|
|
|
not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project)
|
|
end
|
|
|
|
rescue_from ActiveRecord::ActiveRecordError do |e|
|
|
invalid_parameter!(e.message)
|
|
end
|
|
|
|
helpers do
|
|
def resource_not_found!
|
|
render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404)
|
|
end
|
|
|
|
def resource_already_exists!
|
|
render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400)
|
|
end
|
|
|
|
def invalid_parameter!(message = nil)
|
|
render_structured_api_error!({ error_code: 'INVALID_PARAMETER_VALUE', message: message }, 400)
|
|
end
|
|
|
|
def experiment_repository
|
|
::Ml::ExperimentTracking::ExperimentRepository.new(user_project, current_user)
|
|
end
|
|
|
|
def candidate_repository
|
|
::Ml::ExperimentTracking::CandidateRepository.new(user_project, current_user)
|
|
end
|
|
|
|
def experiment
|
|
@experiment ||= find_experiment!(params[:experiment_id], params[:experiment_name])
|
|
end
|
|
|
|
def candidate
|
|
@candidate ||= find_candidate!(params[:run_id])
|
|
end
|
|
|
|
def find_experiment!(iid, name)
|
|
experiment_repository.by_iid_or_name(iid: iid, name: name) || resource_not_found!
|
|
end
|
|
|
|
def find_candidate!(iid)
|
|
candidate_repository.by_iid(iid) || resource_not_found!
|
|
end
|
|
|
|
def packages_url
|
|
path = api_v4_projects_packages_generic_package_version_path(
|
|
id: user_project.id, package_name: '', file_name: ''
|
|
)
|
|
path = path.delete_suffix('/package_version')
|
|
|
|
"#{request.base_url}#{path}"
|
|
end
|
|
end
|
|
|
|
params do
|
|
requires :id, type: String, desc: 'The ID of a project'
|
|
end
|
|
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
|
desc 'API to interface with MLFlow Client, REST API version 1.28.0' do
|
|
detail 'This feature is gated by :ml_experiment_tracking.'
|
|
end
|
|
namespace MLFLOW_API_PREFIX do
|
|
resource :experiments do
|
|
desc 'Fetch experiment by experiment_id' do
|
|
success Entities::Ml::Mlflow::GetExperiment
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment'
|
|
end
|
|
params do
|
|
optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project'
|
|
end
|
|
get 'get', urgency: :low do
|
|
present experiment, with: Entities::Ml::Mlflow::GetExperiment
|
|
end
|
|
|
|
desc 'Fetch experiment by experiment_name' do
|
|
success Entities::Ml::Mlflow::GetExperiment
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name'
|
|
end
|
|
params do
|
|
optional :experiment_name, type: String, default: '', desc: 'Experiment name'
|
|
end
|
|
get 'get-by-name', urgency: :low do
|
|
present experiment, with: Entities::Ml::Mlflow::GetExperiment
|
|
end
|
|
|
|
desc 'List experiments' do
|
|
success Entities::Ml::Mlflow::ListExperiment
|
|
detail 'https://www.mlflow.org/docs/latest/rest-api.html#list-experiments'
|
|
end
|
|
get 'list', urgency: :low do
|
|
response = { experiments: experiment_repository.all }
|
|
|
|
present response, with: Entities::Ml::Mlflow::ListExperiment
|
|
end
|
|
|
|
desc 'Create experiment' do
|
|
success Entities::Ml::Mlflow::NewExperiment
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment'
|
|
end
|
|
params do
|
|
requires :name, type: String, desc: 'Experiment name'
|
|
optional :artifact_location, type: String, desc: 'This will be ignored'
|
|
optional :tags, type: Array, desc: 'This will be ignored'
|
|
end
|
|
post 'create', urgency: :low do
|
|
present experiment_repository.create!(params[:name]), with: Entities::Ml::Mlflow::NewExperiment
|
|
rescue ActiveRecord::RecordInvalid
|
|
resource_already_exists!
|
|
end
|
|
end
|
|
|
|
resource :runs do
|
|
desc 'Creates a Run.' do
|
|
success Entities::Ml::Mlflow::Run
|
|
detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#create-run',
|
|
'MLFlow Runs map to GitLab Candidates']
|
|
end
|
|
params do
|
|
requires :experiment_id, type: Integer,
|
|
desc: 'Id for the experiment, relative to the project'
|
|
optional :start_time, type: Integer,
|
|
desc: 'Unix timestamp in milliseconds of when the run started.',
|
|
default: 0
|
|
optional :user_id, type: String, desc: 'This will be ignored'
|
|
optional :tags, type: Array, desc: 'This will be ignored'
|
|
end
|
|
post 'create', urgency: :low do
|
|
present candidate_repository.create!(experiment, params[:start_time]),
|
|
with: Entities::Ml::Mlflow::Run, packages_url: packages_url
|
|
end
|
|
|
|
desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do
|
|
success Entities::Ml::Mlflow::Run
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-run'
|
|
end
|
|
params do
|
|
requires :run_id, type: String, desc: 'UUID of the candidate.'
|
|
optional :run_uuid, type: String, desc: 'This parameter is ignored'
|
|
end
|
|
get 'get', urgency: :low do
|
|
present candidate, with: Entities::Ml::Mlflow::Run, packages_url: packages_url
|
|
end
|
|
|
|
desc 'Updates a Run.' do
|
|
success Entities::Ml::Mlflow::UpdateRun
|
|
detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#update-run',
|
|
'MLFlow Runs map to GitLab Candidates']
|
|
end
|
|
params do
|
|
requires :run_id, type: String, desc: 'UUID of the candidate.'
|
|
optional :status, type: String,
|
|
values: ::Ml::Candidate.statuses.keys.map(&:upcase),
|
|
desc: "Status of the run. Accepts: " \
|
|
"#{::Ml::Candidate.statuses.keys.map(&:upcase)}."
|
|
optional :end_time, type: Integer, desc: 'Ending time of the run'
|
|
end
|
|
post 'update', urgency: :low do
|
|
candidate_repository.update(candidate, params[:status], params[:end_time])
|
|
|
|
present candidate, with: Entities::Ml::Mlflow::UpdateRun, packages_url: packages_url
|
|
end
|
|
|
|
desc 'Logs a metric to a run.' do
|
|
summary 'Log a metric for a run. A metric is a key-value pair (string key, float value) with an '\
|
|
'associated timestamp. Examples include the various metrics that represent ML model accuracy. '\
|
|
'A metric can be logged multiple times.'
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-metric'
|
|
end
|
|
params do
|
|
requires :run_id, type: String, desc: 'UUID of the run.'
|
|
requires :key, type: String, desc: 'Name for the metric.'
|
|
requires :value, type: Float, desc: 'Value of the metric.'
|
|
requires :timestamp, type: Integer, desc: 'Unix timestamp in milliseconds when metric was recorded'
|
|
optional :step, type: Integer, desc: 'Step at which the metric was recorded'
|
|
end
|
|
post 'log-metric', urgency: :low do
|
|
candidate_repository.add_metric!(
|
|
candidate,
|
|
params[:key],
|
|
params[:value],
|
|
params[:timestamp],
|
|
params[:step]
|
|
)
|
|
|
|
{}
|
|
end
|
|
|
|
desc 'Logs a parameter to a run.' do
|
|
summary 'Log a param used for a run. A param is a key-value pair (string key, string value). '\
|
|
'Examples include hyperparameters used for ML model training and constant dates and values '\
|
|
'used in an ETL pipeline. A param can be logged only once for a run, duplicate will be .'\
|
|
'ignored'
|
|
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-param'
|
|
end
|
|
params do
|
|
requires :run_id, type: String, desc: 'UUID of the run.'
|
|
requires :key, type: String, desc: 'Name for the parameter.'
|
|
requires :value, type: String, desc: 'Value for the parameter.'
|
|
end
|
|
post 'log-parameter', urgency: :low do
|
|
bad_request! unless candidate_repository.add_param!(candidate, params[:key], params[:value])
|
|
|
|
{}
|
|
end
|
|
|
|
desc 'Logs multiple parameters and metrics.' do
|
|
summary 'Log a batch of metrics and params for a run. Validation errors will block the entire batch, '\
|
|
'duplicate errors will be ignored.'
|
|
|
|
detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-param'
|
|
end
|
|
params do
|
|
requires :run_id, type: String, desc: 'UUID of the run.'
|
|
optional :metrics, type: Array, default: [] do
|
|
requires :key, type: String, desc: 'Name for the metric.'
|
|
requires :value, type: Float, desc: 'Value of the metric.'
|
|
requires :timestamp, type: Integer, desc: 'Unix timestamp in milliseconds when metric was recorded'
|
|
optional :step, type: Integer, desc: 'Step at which the metric was recorded'
|
|
end
|
|
optional :params, type: Array, default: [] do
|
|
requires :key, type: String, desc: 'Name for the metric.'
|
|
requires :value, type: String, desc: 'Value of the metric.'
|
|
end
|
|
end
|
|
post 'log-batch', urgency: :low do
|
|
candidate_repository.add_metrics(candidate, params[:metrics])
|
|
candidate_repository.add_params(candidate, params[:params])
|
|
|
|
{}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|