
中国のAI企業・DeepSeekが2025年1月20日にリリースした推論モデルの「DeepSeek-R1-Zero」と「DeepSeek-R1」は、OpenAIの推論モデル・o1相当の性能でありながらo1と比べてトレーニングコストが非常に低いことや、MITライセンスの下でオープンソース化されていることが特徴です。
DeepSeek-R1がオープンソースでなければならなかった理由、そしてなぜOpenAIを倒せないのか - GIGAZINE
AIテクノロジー企業のLagoが、「なぜDeepSeekは自社のAIモデルをオープンソース化したのか」について解説しています。
DeepSeek-R1がオープンソースでなければならなかった理由、そしてなぜOpenAIを倒せないのか - GIGAZINE
Lagoによると、DeepSeekがAIモデルをオープンソース化した理由は、DeepSeekが中国企業であるためとのこと。実際にアメリカでは中国を含む敵対国によってAI関連アプリケーションが軍事転用されるという懸念から、中国へのAIチップの輸出規制を行っています。
DeepSeek-R1がオープンソースでなければならなかった理由、そしてなぜOpenAIを倒せないのか - GIGAZINE
こうした要因から、DeepSeek-R1も欧米市場では懐疑的な目を向けられたり、利用に不安視されたりすることは避けられません。そこでDeepSeekはAIモデルをオープンソース化することで、ユーザーならびに開発者からの信用を得ようとしているというわけです。
DeepSeek-R1がオープンソースでなければならなかった理由、そしてなぜOpenAIを倒せないのか - GIGAZINE
Lagoはこうしたモデルのオープンソース化について「オープンソース化は単なる技術的な行動ではなく、市場での足場を固めるための文化的な行動でもある」と述べています。
DeepSeek-R1がオープンソースでなければならなかった理由、そしてなぜOpenAIを倒せないのか - GIGAZINE
⇧ 開発者にとっては、
- 提供されているドキュメントのファインダビリティ(Findability)が良いか
- 提供されている機能が容易に実装できるか
など、
- 目的とする情報に苦労なく辿り着くことが可能か
が解決できるかどうかが重要なんですけどね...
Oxidizedでmulti-threadに関連するissueを覗いたら闇が深すぎた話
「Ruby」製の「Oxidized」というライブラリの利用に際して、スレッド数をデフォルトの30に設定していたのだが、ふと、本当に複数スレッドで処理されているのだろうかと気になっていたのだが、
⇧ 設定で気になる説明を見つけてしまったのですが、
interval: 3600 #interval in seconds log: ~/.config/oxidized/log debug: false threads: 30 # maximum number of threads # use_max_threads: # false - the number of threads is selected automatically based on the interval option, but not more than the maximum # true - always use the maximum number of threads use_max_threads: false
⇧ とあり、「use_max_threads」という設定項目について、
- false
- the number of threads is selected automatically based on the interval option, but not more than the maximum
- true
- always use the maximum number of threads
という説明になっており、「use_max_threads: false」の設定の場合に、自動的にスレッド数を決めてくれてしまっているらしいのですが、どのような条件で決定されているのかについては全く説明が無く皆目見当が付かない...
で、「Oxidized」のスレッドが生成される仕組みについてのissueが上がっているのですが、
⇧ 具体的な算出方法については解説されていないので、ソースコードを見た感じでは、
■https://github.com/ytti/oxidized/blob/master/lib/oxidized/jobs.rb
module Oxidized
class Jobs < Array
AVERAGE_DURATION = 5 # initially presume nodes take 5s to complete
MAX_INTER_JOB_GAP = 300 # add job if more than X from last job started
attr_accessor :interval, :max, :want
def initialize(max, use_max_threads, interval, nodes)
@max = max
@use_max_threads = use_max_threads
# Set interval to 1 if interval is 0 (=disabled) so we don't break
# the 'ceil' function
@interval = interval.zero? ? 1 : interval
@nodes = nodes
@last = Time.now.utc
@durations = Array.new @nodes.size, AVERAGE_DURATION
duration AVERAGE_DURATION
super()
end
def push(arg)
@last = Time.now.utc
super
end
def duration(last)
if @durations.size > @nodes.size
@durations.slice! @nodes.size...@durations.size
elsif @durations.size < @nodes.size
@durations.fill AVERAGE_DURATION, @durations.size...@nodes.size
end
@durations.push(last).shift
@duration = @durations.inject(:+).to_f / @nodes.size # rolling average
new_count
end
def new_count
@want = if @use_max_threads
@max
else
((@nodes.size * @duration) / @interval).ceil
end
@want = 1 if @want < 1
@want = @nodes.size if @want > @nodes.size
@want = @max if @want > @max
end
def increment
# Increments the job count if safe to do so, which means:
# a) less threads running than the total amount of nodes
# b) we want less than the max specified number of threads
@want = [(@want + 1), @nodes.size, @max].min
end
def work
# if a) we want less or same amount of threads as we now running
# and b) we want less threads running than the total amount of nodes
# and c) there is more than MAX_INTER_JOB_GAP since last one was started
# then we want one more thread (rationale is to fix hanging thread causing HOLB)
return unless @want <= size && @want < @nodes.size
return unless @want <= size
increment if (Time.now.utc - @last) > MAX_INTER_JOB_GAP
end
end
end
⇧ とあって、相変わらず、「Ruby」のインスタンス変数がグローバル変数のような感じで追跡し辛過ぎるんだが、
■抜粋:https://github.com/ytti/oxidized/blob/master/lib/oxidized/jobs.rb
((@nodes.size * @duration) / @interval).ceil
⇧ の部分が、「use_max_threads: false」の設定の場合のスレッド数になるらしいのだが、このロジックだと、「対象機器」の数が少ないと、常に1スレッドでしか動作しないことになりそうなんよね...
そもそも、
■抜粋:https://github.com/ytti/oxidized/blob/master/lib/oxidized/jobs.rb
@durations = Array.new @nodes.size, AVERAGE_DURATION
⇧「@nodes.size」の値をどこで設定しているのかが分からないので「@duration」の数も定まらず何とも言えないのだが、「@interval」は設定ファイルにあった「interval: 3600 #interval in seconds」の値のことだとは思うので、仮に、この値が「1日」間隔である「interval: 86400」というような設定になっている場合、「対象機器」の数によっては、シングルスレッドでの直列処理になってしまうので、期待した時間内に処理しきれない問題が出てくる気がするんだが...
ちなみに、
■https://github.com/ytti/oxidized/blob/master/lib/oxidized/worker.rb
module Oxidized
require 'oxidized/job'
require 'oxidized/jobs'
class Worker
def initialize(nodes)
@jobs_done = 0
@nodes = nodes
@jobs = Jobs.new(Oxidized.config.threads, Oxidized.config.use_max_threads, Oxidized.config.interval, @nodes)
@nodes.jobs = @jobs
Thread.abort_on_exception = true
end
def work
ended = []
@jobs.delete_if { |job| ended << job unless job.alive? }
ended.each { |job| process job }
@jobs.work
while @jobs.size < @jobs.want
Oxidized.logger.debug "lib/oxidized/worker.rb: Jobs running: #{@jobs.size} of #{@jobs.want} - ended: #{@jobs_done} of #{@nodes.size}"
# ask for next node in queue non destructive way
nextnode = @nodes.first
unless nextnode.last.nil?
# Set unobtainable value for 'last' if interval checking is disabled
last = Oxidized.config.interval.zero? ? Time.now.utc + 10 : nextnode.last.end
break if last + Oxidized.config.interval > Time.now.utc
end
# shift nodes and get the next node
node = @nodes.get
node.running? ? next : node.running = true
@jobs.push Job.new node
Oxidized.logger.debug "lib/oxidized/worker.rb: Added #{node.group}/#{node.name} to the job queue"
end
if cycle_finished?
run_done_hook
exit 0 if Oxidized.config.run_once
end
Oxidized.logger.debug("lib/oxidized/worker.rb: #{@jobs.size} jobs running in parallel") unless @jobs.empty?
end
def process(job)
node = job.node
node.last = job
node.stats.add job
@jobs.duration job.time
node.running = false
if job.status == :success
process_success node, job
else
process_failure node, job
end
rescue NodeNotFound
Oxidized.logger.warn "#{node.group}/#{node.name} not found, removed while collecting?"
end
def reload
@nodes.load
end
private
def process_success(node, job)
@jobs_done += 1 # needed for :nodes_done hook
Oxidized.hooks.handle :node_success, node: node,
job: job
msg = "update #{node.group}/#{node.name}"
msg += " from #{node.from}" if node.from
msg += " with message '#{node.msg}'" if node.msg
output = node.output.new
if output.store node.name, job.config,
msg: msg, email: node.email, user: node.user, group: node.group
node.modified
Oxidized.logger.info "Configuration updated for #{node.group}/#{node.name}"
Oxidized.hooks.handle :post_store, node: node,
job: job,
commitref: output.commitref
end
node.reset
end
def process_failure(node, job)
msg = "#{node.group}/#{node.name} status #{job.status}"
if node.retry < Oxidized.config.retries
node.retry += 1
msg += ", retry attempt #{node.retry}"
@nodes.next node.name
else
# Only increment the @jobs_done when we give up retries for a node (or success).
# As it would otherwise cause @jobs_done to be incremented with generic retries.
# This would cause :nodes_done hook to desync from running at the end of the nodelist and
# be fired when the @jobs_done > @nodes.count (could be mid-cycle on the next cycle).
@jobs_done += 1
msg += ", retries exhausted, giving up"
node.retry = 0
Oxidized.hooks.handle :node_fail, node: node,
job: job
end
Oxidized.logger.warn msg
end
def cycle_finished?
if @jobs_done > @nodes.count
true
else
@jobs_done.positive? && (@jobs_done % @nodes.count).zero?
end
end
def run_done_hook
Oxidized.logger.debug "lib/oxidized/worker.rb: Running :nodes_done hook"
Oxidized.hooks.handle :nodes_done
rescue StandardError => e
# swallow the hook erros and continue as normal
Oxidized.logger.error "lib/oxidized/worker.rb: #{e.message}"
ensure
@jobs_done = 0
end
end
end
⇧ ログの出力を信じるならば、「Worker」クラスがnewされる時に呼び出されるらしい「initialize」メソッドの引数に「nodes」が渡されているので、「Worker」クラスがnewしている箇所を追跡してみると、
■https://github.com/ytti/oxidized/blob/master/lib/oxidized/core.rb
module Oxidized
class << self
def new(*args)
Core.new args
end
end
class Core
class NoNodesFound < OxidizedError; end
def initialize(_args)
Oxidized.mgr = Manager.new
Oxidized.hooks = HookManager.from_config(Oxidized.config)
nodes = Nodes.new
raise NoNodesFound, 'source returns no usable nodes' if nodes.empty?
@worker = Worker.new nodes
@need_reload = false
# If we receive a SIGHUP, queue a reload of the state
reload_proc = proc do
@need_reload = true
end
Signals.register_signal('HUP', reload_proc)
# Initialize REST API and webUI if requested
if Oxidized.config.rest?
begin
require 'oxidized/web'
rescue LoadError
raise OxidizedError, 'oxidized-web not found: sudo gem install oxidized-web - \
or disable web support by setting "rest: false" in your configuration'
end
@rest = API::Web.new nodes, Oxidized.config.rest
@rest.run
end
run
end
private
def reload
Oxidized.logger.info("Reloading node list and log files")
@worker.reload
Oxidized.logger.reopen
@need_reload = false
end
def run
Oxidized.logger.debug "lib/oxidized/core.rb: Starting the worker..."
loop do
reload if @need_reload
@worker.work
sleep Config::SLEEP
end
end
end
end
⇧「Core」クラスで「nodes = Nodes.new」した値が大元らしいので、「Nodes」クラスを確認しに行ってみると、
■https://github.com/ytti/oxidized/blob/master/lib/oxidized/nodes.rb
module Oxidized
require 'ipaddr'
require 'oxidized/node'
class NotSupported < OxidizedError; end
class NodeNotFound < OxidizedError; end
class Nodes < Array
attr_accessor :source, :jobs
alias put unshift
def load(node_want = nil)
with_lock do
new = []
@source = Oxidized.config.source.default
Oxidized.mgr.add_source(@source) || raise(MethodNotFound, "cannot load node source '#{@source}', not found")
Oxidized.logger.info "lib/oxidized/nodes.rb: Loading nodes"
nodes = Oxidized.mgr.source[@source].new.load node_want
nodes.each do |node|
# we want to load specific node(s), not all of them
next unless node_want? node_want, node
begin
node_obj = Node.new node
new.push node_obj
rescue ModelNotFound => e
Oxidized.logger.error "node %s raised %s with message '%s'" % [node, e.class, e.message]
rescue Resolv::ResolvError => e
Oxidized.logger.error "node %s is not resolvable, raised %s with message '%s'" % [node, e.class, e.message]
end
end
size.zero? ? replace(new) : update_nodes(new)
Oxidized.logger.info "lib/oxidized/nodes.rb: Loaded #{size} nodes"
end
end
def node_want?(node_want, node)
return true unless node_want
node_want_ip = (IPAddr.new(node_want) rescue false)
name_is_ip = (IPAddr.new(node[:name]) rescue false)
# rubocop:todo Lint/DuplicateBranch
if name_is_ip && (node_want_ip == node[:name])
true
elsif node[:ip] && (node_want_ip == node[:ip])
true
elsif node_want.match node[:name]
true unless name_is_ip
end
# rubocop:enable Lint/DuplicateBranch
end
def list
with_lock do
map { |e| e.serialize }
end
end
def show(node)
with_lock do
i = find_node_index node
self[i].serialize
end
end
# Returns the configuration of group/node_name
#
# #fetch is called by oxidzed-web
def fetch(node_name, group)
yield_node_output(node_name) do |node, output|
output.fetch node, group
end
end
# @param node [String] name of the node moved into the head of array
def next(node, opt = {})
return if running.find_index(node)
with_lock do
n = del node
n.user = opt['user']
n.email = opt['email']
n.msg = opt['msg']
n.from = opt['from']
# set last job to nil so that the node is picked for immediate update
n.last = nil
put n
jobs.increment if Oxidized.config.next_adds_job?
end
end
alias top next
# @return [String] node from the head of the array
def get
with_lock do
(self << shift).last
end
end
# @param node node whose index number in Nodes to find
# @return [Fixnum] index number of node in Nodes
def find_node_index(node)
find_index(node) || raise(NodeNotFound, "unable to find '#{node}'")
end
# Returns all stored versions of group/node_name
#
# Called by oxidized-web
def version(node_name, group)
yield_node_output(node_name) do |node, output|
output.version node, group
end
end
def get_version(node_name, group, oid)
yield_node_output(node_name) do |node, output|
output.get_version node, group, oid
end
end
def get_diff(node_name, group, oid1, oid2)
yield_node_output(node_name) do |node, output|
output.get_diff node, group, oid1, oid2
end
end
def find_index(node)
index { |e| [e.name, e.ip].include? node }
end
private
def initialize(opts = {})
super()
node = opts.delete :node
@mutex = Mutex.new # we compete for the nodes with webapi thread
if (nodes = opts.delete(:nodes))
replace nodes
else
load node
end
end
def with_lock(...)
@mutex.synchronize(...)
end
# @param node node which is removed from nodes list
# @return [Node] deleted node
def del(node)
delete_at find_node_index(node)
end
# @return [Nodes] list of nodes running now
def running
Nodes.new nodes: select { |node| node.running? }
end
# @return [Nodes] list of nodes waiting (not running)
def waiting
Nodes.new nodes: select { |node| not node.running? }
end
# walks list of new nodes, if old node contains same name, adds last and
# stats information from old to new.
#
# @todo can we trust name to be unique identifier, what about when groups are used?
# @param [Array] nodes Array of nodes used to replace+update old
def update_nodes(nodes)
old = dup
# load the Array "nodes" in self (the class Nodes inherits Array)
replace(nodes)
each do |node|
if (i = old.find_node_index(node.name))
node.stats = old[i].stats
node.last = old[i].last
end
rescue NodeNotFound
# Do nothing:
# when a node is not found, we have nothing to do:
# it has already been loaded by replace(nodes) and there are no
# stats to copy
end
sort_by! { |x| x.last.nil? ? Time.new(0) : x.last.end }
end
def yield_node_output(node_name)
with_lock do
node = find { |n| n.name == node_name }
raise(NodeNotFound, "unable to find '#{node_name}'") if node.nil?
output = node.output.new
raise NotSupported unless output.respond_to? :fetch
yield node, output
end
end
end
end
⇧「Nodes」クラスの「initialize」メソッドが引数無しで呼びされていることから、「Nodes」クラスの「load」メソッドに処理が遷移し、「@source = Oxidized.config.source.default」については、「Config」クラスを参照して欲しいのだが、「asetus.default.source.default = 'csv' # csv, sql」という記述が見られるので、「nodes = Oxidized.mgr.source[@source].new.load node_want」を実行していることから、「Csv」クラスの「load」メソッドに遷移するのだろうと仮定すると、
■https://github.com/ytti/oxidized/blob/master/lib/oxidized/source/csv.rb
module Oxidized
module Source
class CSV < Source
def initialize
@cfg = Oxidized.config.source.csv
super
end
def setup
if @cfg.empty?
Oxidized.asetus.user.source.csv.file = File.join(Config::ROOT, 'router.db')
Oxidized.asetus.user.source.csv.delimiter = /:/
Oxidized.asetus.user.source.csv.map.name = 0
Oxidized.asetus.user.source.csv.map.model = 1
Oxidized.asetus.user.source.csv.gpg = false
Oxidized.asetus.save :user
raise NoConfig, "no source csv config, edit #{Oxidized::Config.configfile}"
end
require 'gpgme' if @cfg.gpg?
# map.name is mandatory
return if @cfg.map.has_key?('name')
raise InvalidConfig, "map/name is a mandatory source attribute, edit #{Oxidized::Config.configfile}"
end
def load(_node_want = nil)
nodes = []
open_file.each_line do |line|
next if line =~ /^\s*#/
data = line.chomp.split(@cfg.delimiter, -1)
next if data.empty?
# map node parameters
keys = {}
@cfg.map.each do |key, position|
keys[key.to_sym] = node_var_interpolate data[position]
end
keys[:model] = map_model keys[:model] if keys.has_key? :model
keys[:group] = map_group keys[:group] if keys.has_key? :group
# map node specific vars
vars = {}
@cfg.vars_map.each do |key, position|
vars[key.to_sym] = node_var_interpolate data[position]
end
keys[:vars] = vars unless vars.empty?
nodes << keys
end
nodes
end
end
end
end
⇧ で計算して最終的に「nodes」を戻り値としているっぽい。
「keys[key.to_sym] = node_var_interpolate data[position]」の処理は、「Source」クラスの「node_var_interpolate」メソッドで処理されているらしい。
「継承」を利用しているらしく「Source」クラスが「親」クラスということらしいですな、他にも「open_file」というメソッドが「Source」クラスで定義されている模様、分り辛い...
で、おそらく、「open_file」というメソッドがConfigファイルを処理しているのだろうと仮定して、
■https://github.com/ytti/oxidized/blob/master/docs/Configuration.md
---
username: oxidized
password: S3cr3tx
model: junos
interval: 3600 #interval in seconds
log: ~/.config/oxidized/log
debug: false
threads: 30 # maximum number of threads
# use_max_threads:
# false - the number of threads is selected automatically based on the interval option, but not more than the maximum
# true - always use the maximum number of threads
use_max_threads: false
timeout: 20
retries: 3
prompt: !ruby/regexp /^([\w.@-]+[#>]\s?)$/
crash:
directory: ~/.config/oxidized/crashes
hostnames: false
vars:
enable: S3cr3tx
groups: {}
rest: 127.0.0.1:8888
pid: ~/.config/oxidized/oxidized.pid
input:
default: ssh, telnet
debug: false
ssh:
secure: false
output:
default: git
git:
user: Oxidized
email: oxidized@example.com
repo: "~/.config/oxidized/oxidized.git"
source:
default: csv
csv:
file: ~/.config/oxidized/router.db
delimiter: !ruby/regexp /:/
map:
name: 0
model: 1
username: 2
password: 3
vars_map:
enable: 4
model_map:
cisco: ios
juniper: junos
!ruby/regexp /procurve/: procurve
⇧ が読み込まれるファイルの内容と仮定して、結局のところ、「Csv」クラスの「load」メソッドで算出される「nodes」に最終的に格納されているっぽい「keys」の要素の数はいくつになるのかChatGPTに質問したところ、
■『「keys」の要素の数はいくつになるのか』に対してのChatGPTの回答
keys = {
:name => "router1", # ノード名
:model => "junos", # モデル名(juniper)
:username => "oxidized", # ユーザー名
:password => "S3cr3tx", # パスワード
:vars => {} # 変数(今回は空)
}
⇧ という回答が返ってきた。
で、改めて、
■抜粋:https://github.com/ytti/oxidized/blob/master/lib/oxidized/jobs.rb
((@nodes.size * @duration) / @interval).ceil
⇧ の数値がいくつぐらいになるのか見積もりしてみると、
■抜粋:https://github.com/ytti/oxidized/blob/master/lib/oxidized/jobs.rb
AVERAGE_DURATION = 5 # initially presume nodes take 5s to complete
MAX_INTER_JOB_GAP = 300 # add job if more than X from last job started
@durations = Array.new @nodes.size, AVERAGE_DURATION
duration AVERAGE_DURATION
def duration(last)
if @durations.size > @nodes.size
@durations.slice! @nodes.size...@durations.size
elsif @durations.size < @nodes.size
@durations.fill AVERAGE_DURATION, @durations.size...@nodes.size
end
@durations.push(last).shift
@duration = @durations.inject(:+).to_f / @nodes.size # rolling average
new_count
end
⇧ 更に「duration」メソッドで計算処理を実施していることから、スレッド数を算出するロジックが一か所に集約されていないのが分り非常に辛いのだが、仮に、
- @nodes.size = 5(ノードの数)
- @durations = [5, 5, 5, 5, 5]
- Array.new @nodes.size, AVERAGE_DURATION ※1
- @interval = 86400(間隔、1日の秒数です)
※1 以下、公式のマニュアルによるRubyの標準で用意されているArrayクラスの特異メソッドによる初期化を利用しているっぽい。
の場合にどうなるか、ChatGPTに質問したところ、
■「duration」メソッドの処理で算出される「@duration」の値が、
@duration = 5.0
⇧ となり、
■「((@nodes.size * @duration) / @interval).ceil」の計算
((5 × 5.0) / 86400 ≈ 0.00028935185).ceil 0.00028935185.ceil = 1
⇧ となり、スレッド数は、「1」になるらしい...
つまり、「threads: 30」の設定をしていようが「use_max_threads: false」の設定になっていると、「multi-thread」での処理にならないケースが出てくるという罠...
「Oxidized」の非機能要件がハッキリしないのだが、「対象機器」の数が膨大になる場合は、「use_max_threads: true」とかにして利用するスレッド数を固定するしかない感じなんかね?
それでも、どうしようもない場合は、「Oxidized」のプロセスを複数にする感じなんかね?
「Docker Compose」とかであれば、「Oxidized」が稼働している「Docker」のコンテナ数を増やす感じになるんかな...
コンフリクトしたりしそうなのと、運用が複雑になりそう気がするので、「Oxidized」のプロセスを複数にするのは避けたいところですが...
如何せん、「Oxidized」の開発者の方が想定している利用方法が分からないので、何とも言えませんが...
とりあえず、公式のドキュメントの説明をもう少し充実させて欲しい気はしますな...
設定項目の関係性とか暗黙知な部分があるとどうにもならんので...
毎度モヤモヤ感が半端ない…
今回はこのへんで。

